gui.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. # coding: utf-8
  2. import typing
  3. import weakref
  4. from math import floor
  5. import pyglet
  6. import time
  7. from pyglet.window import mouse
  8. import cocos
  9. from cocos import collision_model
  10. from cocos import euclid
  11. from cocos.audio.pygame import mixer
  12. from cocos.layer import ScrollableLayer
  13. from synergine2.config import Config
  14. from synergine2.exceptions import SynergineException
  15. from synergine2.log import get_logger
  16. from synergine2.terminals import Terminal
  17. from synergine2.terminals import TerminalPackage
  18. from synergine2_cocos2d.actor import Actor
  19. from synergine2_cocos2d.const import SELECTION_COLOR_RGB
  20. from synergine2_cocos2d.const import DEFAULT_SELECTION_COLOR_RGB
  21. from synergine2_cocos2d.exception import InteractionNotFound
  22. from synergine2_cocos2d.exception import OuterWorldPosition
  23. from synergine2_cocos2d.gl import draw_rectangle
  24. from synergine2_cocos2d.gl import rectangle_positions_type
  25. from synergine2_cocos2d.interaction import InteractionManager
  26. from synergine2_cocos2d.layer import LayerManager
  27. from synergine2_cocos2d.middleware import MapMiddleware
  28. from synergine2_cocos2d.middleware import TMXMiddleware
  29. from synergine2_cocos2d.user_action import UserAction
  30. from synergine2_cocos2d.util import ensure_dir_exist
  31. from synergine2_xyz.physics import Physics
  32. from synergine2_xyz.xyz import XYZSubjectMixin
  33. class GridManager(object):
  34. def __init__(
  35. self,
  36. cell_width: int,
  37. cell_height: int,
  38. world_width: int,
  39. world_height: int,
  40. ) -> None:
  41. self.cell_width = cell_width
  42. self.cell_height = cell_height
  43. self.world_width = world_width
  44. self.world_height = world_height
  45. def get_grid_position(self, pixel_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  46. pixel_x, pixel_y = pixel_position
  47. cell_x = int(floor(pixel_x / self.cell_width))
  48. cell_y = int(floor(pixel_y / self.cell_height))
  49. if cell_x > self.world_width or cell_y > self.world_height or cell_x < 0 or cell_y < 0:
  50. raise OuterWorldPosition('Position "{}" is outer world ({}x{})'.format(
  51. (cell_x, cell_y),
  52. self.world_width,
  53. self.world_height,
  54. ))
  55. return cell_x, cell_y
  56. def get_world_position_of_grid_position(self, grid_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  57. return grid_position[0] * self.cell_width + (self.cell_width // 2),\
  58. grid_position[1] * self.cell_height + (self.cell_height // 2)
  59. def get_rectangle_positions(
  60. self,
  61. grid_position: typing.Tuple[int, int],
  62. ) -> rectangle_positions_type:
  63. """
  64. A<---D
  65. | |
  66. B--->C
  67. :param grid_position:grid position to exploit
  68. :return: grid pixel corners positions
  69. """
  70. grid_x, grid_y = grid_position
  71. a = grid_x * self.cell_width, grid_y * self.cell_height + self.cell_height
  72. b = grid_x * self.cell_width, grid_y * self.cell_height
  73. c = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height
  74. d = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height + self.cell_height
  75. return a, d, c, b
  76. class MinMaxRect(cocos.cocosnode.CocosNode):
  77. def __init__(self, layer_manager: LayerManager):
  78. super(MinMaxRect, self).__init__()
  79. self.layer_manager = layer_manager
  80. self.color3 = (20, 20, 20)
  81. self.color3f = (0, 0, 0, 0.2)
  82. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  83. self.visible = False
  84. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  85. # asumes world to screen preserves order
  86. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  87. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  88. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  89. def draw(self):
  90. if not self.visible:
  91. return
  92. draw_rectangle(
  93. self.vertexes,
  94. self.color3,
  95. self.color3f,
  96. )
  97. def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
  98. self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
  99. class FinishedCallback(Exception):
  100. pass
  101. class Callback(object):
  102. def __init__(
  103. self,
  104. func: typing.Callable[[], None],
  105. duration: float,
  106. delay: float=None,
  107. ) -> None:
  108. self.func = func
  109. self.duration = duration
  110. # Started timestamp
  111. self.started = None # type: float
  112. self.require_delay = False
  113. self.delay = delay
  114. if delay is not None:
  115. self.require_delay = True
  116. def execute(self) -> None:
  117. if self.require_delay and not self.started:
  118. self.started = time.time()
  119. return
  120. elif self.require_delay and time.time() - self.started < self.delay:
  121. return
  122. elif self.require_delay:
  123. self.started = None
  124. self.require_delay = False
  125. if self.started is None:
  126. self.started = time.time()
  127. if time.time() - self.started <= self.duration:
  128. self.func()
  129. elif not self.duration:
  130. self.func()
  131. raise FinishedCallback()
  132. else:
  133. raise FinishedCallback()
  134. class EditLayer(cocos.layer.Layer):
  135. is_event_handler = True
  136. def __init__(
  137. self,
  138. config: Config,
  139. layer_manager: LayerManager,
  140. grid_manager: GridManager,
  141. worldview,
  142. bindings=None,
  143. fastness=None,
  144. autoscroll_border=10,
  145. autoscroll_fastness=None,
  146. wheel_multiplier=None,
  147. zoom_min=None,
  148. zoom_max=None,
  149. zoom_fastness=None,
  150. mod_modify_selection=None,
  151. mod_restricted_mov=None,
  152. ):
  153. # TODO: Clean init params
  154. super().__init__()
  155. self.config = config
  156. self.logger = get_logger('EditLayer', config)
  157. self.layer_manager = layer_manager
  158. self.grid_manager = grid_manager
  159. self.bindings = bindings
  160. buttons = {}
  161. modifiers = {}
  162. for k in bindings:
  163. buttons[bindings[k]] = 0
  164. modifiers[bindings[k]] = 0
  165. self.buttons = buttons
  166. self.modifiers = modifiers
  167. self.fastness = fastness
  168. self.autoscroll_border = autoscroll_border
  169. self.autoscroll_fastness = autoscroll_fastness
  170. self.wheel_multiplier = wheel_multiplier
  171. self.zoom_min = zoom_min
  172. self.zoom_max = zoom_max
  173. self.zoom_fastness = zoom_fastness
  174. self.mod_modify_selection = mod_modify_selection
  175. self.mod_restricted_mov = mod_restricted_mov
  176. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  177. self.weak_worldview = weakref.ref(worldview)
  178. self.wwidth = worldview.width
  179. self.wheight = worldview.height
  180. self.autoscrolling = False
  181. self.drag_selecting = False
  182. self.drag_moving = False
  183. self.restricted_mov = False
  184. self.wheel = 0
  185. self.dragging = False
  186. self.keyscrolling = False
  187. self.keyscrolling_descriptor = (0, 0)
  188. self.wdrag_start_point = (0, 0)
  189. self.elastic_box = None # type: MinMaxRect
  190. self.elastic_box_wminmax = 0, 0, 0, 0
  191. self.selection = {} # type: typing.List[Actor]
  192. self.screen_mouse = (0, 0)
  193. self.world_mouse = (0, 0)
  194. self.sleft = None
  195. self.sright = None
  196. self.sbottom = None
  197. self.s_top = None
  198. self.user_action_pending = None # type: UserAction
  199. # opers that change cshape must ensure it goes to False,
  200. # selection opers must ensure it goes to True
  201. self.selection_in_collman = True
  202. # TODO: Hardcoded here, should be obtained from level properties or calc
  203. # from available actors or current actors in worldview
  204. gsize = 32 * 1.25
  205. self.collision_manager = collision_model.CollisionManagerGrid(
  206. -gsize,
  207. self.wwidth + gsize,
  208. -gsize,
  209. self.wheight + gsize,
  210. gsize,
  211. gsize,
  212. )
  213. self.schedule(self.update)
  214. self.selectable_actors = []
  215. self.callbacks = [] # type: typing.List[Callback]
  216. def append_callback(self, callback: typing.Callable[[], None], duration: float, delay: float=None) -> None:
  217. self.callbacks.append(Callback(
  218. callback,
  219. duration,
  220. delay=delay,
  221. ))
  222. def set_selectable(self, actor: Actor) -> None:
  223. self.selectable_actors.append(actor)
  224. self.collision_manager.add(actor)
  225. def unset_selectable(self, actor: Actor) -> None:
  226. self.selectable_actors.remove(actor)
  227. self.collision_manager.remove_tricky(actor)
  228. def draw(self, *args, **kwargs) -> None:
  229. self.draw_update_cshapes()
  230. self.draw_selection()
  231. self.draw_interactions()
  232. self.execute_callbacks()
  233. def execute_callbacks(self) -> None:
  234. for callback in self.callbacks[:]:
  235. try:
  236. callback.execute()
  237. except FinishedCallback:
  238. self.callbacks.remove(callback)
  239. def draw_update_cshapes(self) -> None:
  240. for actor in self.selectable_actors:
  241. if actor.need_update_cshape:
  242. if self.collision_manager.knows(actor):
  243. self.collision_manager.remove_tricky(actor)
  244. actor.update_cshape()
  245. self.collision_manager.add(actor)
  246. def draw_selection(self) -> None:
  247. for actor, cshape in self.selection.items():
  248. grid_position = self.grid_manager.get_grid_position(actor.position)
  249. rect_positions = self.grid_manager.get_rectangle_positions(grid_position)
  250. draw_rectangle(
  251. self.layer_manager.scrolling_manager.world_to_screen_positions(rect_positions),
  252. actor.subject.properties.get(
  253. SELECTION_COLOR_RGB,
  254. self.config.get(DEFAULT_SELECTION_COLOR_RGB, (0, 81, 211))
  255. ),
  256. )
  257. def draw_interactions(self) -> None:
  258. if self.user_action_pending:
  259. try:
  260. interaction = self.layer_manager.interaction_manager.get_for_user_action(self.user_action_pending)
  261. interaction.draw_pending()
  262. except InteractionNotFound:
  263. pass
  264. def on_enter(self):
  265. super().on_enter()
  266. scene = self.get_ancestor(cocos.scene.Scene)
  267. if self.elastic_box is None:
  268. self.elastic_box = MinMaxRect(self.layer_manager)
  269. scene.add(self.elastic_box, z=10)
  270. def update(self, dt):
  271. mx = self.buttons['right'] - self.buttons['left']
  272. my = self.buttons['up'] - self.buttons['down']
  273. dz = self.buttons['zoomin'] - self.buttons['zoomout']
  274. # scroll
  275. if self.autoscrolling:
  276. self.update_autoscroll(dt)
  277. else:
  278. # care for keyscrolling
  279. new_keyscrolling = ((len(self.selection) == 0) and
  280. (mx != 0 or my != 0))
  281. new_keyscrolling_descriptor = (mx, my)
  282. if ((new_keyscrolling != self.keyscrolling) or
  283. (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
  284. self.keyscrolling = new_keyscrolling
  285. self.keyscrolling_descriptor = new_keyscrolling_descriptor
  286. fastness = 1.0
  287. if mx != 0 and my != 0:
  288. fastness *= 0.707106 # 1/sqrt(2)
  289. self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
  290. if self.keyscrolling:
  291. self.update_autoscroll(dt)
  292. # selection move
  293. if self.drag_moving:
  294. # update positions
  295. wx, wy = self.world_mouse
  296. dx = wx - self.wdrag_start_point[0]
  297. dy = wy - self.wdrag_start_point[1]
  298. if self.restricted_mov:
  299. if abs(dy) > abs(dx):
  300. dx = 0
  301. else:
  302. dy = 0
  303. dpos = euclid.Vector2(dx, dy)
  304. for actor in self.selection:
  305. old_pos = self.selection[actor].center
  306. new_pos = old_pos + dpos
  307. try:
  308. grid_pos = self.grid_manager.get_grid_position(new_pos)
  309. grid_pixel_pos = self.grid_manager.get_world_position_of_grid_position(grid_pos)
  310. actor.update_position(grid_pixel_pos)
  311. except OuterWorldPosition:
  312. # don't update position
  313. pass
  314. scroller = self.weak_scroller()
  315. # zoom
  316. zoom_change = (dz != 0 or self.wheel != 0)
  317. if zoom_change:
  318. if self.mouse_into_world():
  319. wzoom_center = self.world_mouse
  320. szoom_center = self.screen_mouse
  321. else:
  322. # decay to scroller unadorned
  323. wzoom_center = None
  324. if self.wheel != 0:
  325. dt_dz = 0.01666666 * self.wheel
  326. self.wheel = 0
  327. else:
  328. dt_dz = dt * dz
  329. zoom = scroller.scale + dt_dz * self.zoom_fastness
  330. if zoom < self.zoom_min:
  331. zoom = self.zoom_min
  332. elif zoom > self.zoom_max:
  333. zoom = self.zoom_max
  334. scroller.scale = zoom
  335. if wzoom_center is not None:
  336. # postprocess toward 'world point under mouse the same before
  337. # and after zoom' ; other restrictions may prevent fully comply
  338. wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
  339. fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
  340. fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
  341. scroller.set_focus(fx, fy)
  342. def update_mouse_position(self, sx, sy):
  343. self.screen_mouse = sx, sy
  344. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  345. # handle autoscroll
  346. border = self.autoscroll_border
  347. if border is not None:
  348. # sleft and companions includes the border
  349. scroller = self.weak_scroller()
  350. self.update_view_bounds()
  351. sdx = 0.0
  352. if sx < self.sleft:
  353. sdx = sx - self.sleft
  354. elif sx > self.sright:
  355. sdx = sx - self.sright
  356. sdy = 0.0
  357. if sy < self.sbottom:
  358. sdy = sy - self.sbottom
  359. elif sy > self.s_top:
  360. sdy = sy - self.s_top
  361. self.autoscrolling = sdx != 0.0 or sdy != 0.0
  362. if self.autoscrolling:
  363. self.autoscrolling_sdelta = (sdx / border, sdy / border)
  364. def update_autoscroll(self, dt):
  365. fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
  366. scroller = self.weak_scroller()
  367. worldview = self.weak_worldview()
  368. f = self.autoscroll_fastness
  369. wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
  370. wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
  371. # ask scroller to try scroll (wdx, wdy)
  372. fx = scroller.restricted_fx + wdx
  373. fy = scroller.restricted_fy + wdy
  374. scroller.set_focus(fx, fy)
  375. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
  376. self.adjust_elastic_box()
  377. # self.update_view_bounds()
  378. def update_view_bounds(self):
  379. scroller = self.weak_scroller()
  380. scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
  381. scroller.restricted_fx,
  382. scroller.restricted_fy,
  383. )
  384. hw = scroller.view_w / 2.0
  385. hh = scroller.view_h / 2.0
  386. border = self.autoscroll_border
  387. self.sleft = scx - hw + border
  388. self.sright = scx + hw - border
  389. self.sbottom = scy - hh + border
  390. self.s_top = scy + hh - border
  391. def mouse_into_world(self):
  392. worldview = self.weak_worldview()
  393. # TODO: allow lower limits != 0 ?
  394. return ((0 <= self.world_mouse[0] <= worldview.width) and
  395. (0 <= self.world_mouse[1] <= worldview.height))
  396. def on_key_press(self, k, m):
  397. binds = self.bindings
  398. self._on_key_press(k, m)
  399. if k in binds:
  400. self.buttons[binds[k]] = 1
  401. self.modifiers[binds[k]] = 1
  402. return True
  403. return False
  404. def _on_key_press(self, k, m):
  405. pass
  406. def on_key_release(self, k, m):
  407. binds = self.bindings
  408. if k in binds:
  409. self.buttons[binds[k]] = 0
  410. self.modifiers[binds[k]] = 0
  411. return True
  412. return False
  413. def on_mouse_motion(self, sx, sy, dx, dy):
  414. self.update_mouse_position(sx, sy)
  415. def on_mouse_leave(self, sx, sy):
  416. self.autoscrolling = False
  417. def on_mouse_press(self, x, y, buttons, modifiers):
  418. rx, ry = self.layer_manager.scrolling_manager.screen_to_world(x, y)
  419. self.logger.debug(
  420. 'GUI click: x: {}, y: {}, rx: {}, ry: {} ({}|{})'.format(x, y, rx, ry, buttons, modifiers)
  421. )
  422. if mouse.LEFT:
  423. # Non action pending case
  424. if not self.user_action_pending:
  425. actor = self.single_actor_from_mouse()
  426. if actor:
  427. self.selection.clear()
  428. self.selection_add(actor)
  429. # Action pending case
  430. else:
  431. try:
  432. interaction = self.layer_manager.interaction_manager.get_for_user_action(self.user_action_pending)
  433. interaction.execute()
  434. except InteractionNotFound:
  435. pass
  436. if mouse.RIGHT:
  437. if self.user_action_pending:
  438. self.user_action_pending = None
  439. def on_mouse_release(self, sx, sy, button, modifiers):
  440. # should we handle here mod_restricted_mov ?
  441. wx, wy = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  442. modify_selection = modifiers & self.mod_modify_selection
  443. if self.dragging:
  444. # ignore all buttons except left button
  445. if button != mouse.LEFT:
  446. return
  447. if self.drag_selecting:
  448. self.end_drag_selection(wx, wy, modify_selection)
  449. elif self.drag_moving:
  450. self.end_drag_move(wx, wy)
  451. self.dragging = False
  452. else:
  453. if button == mouse.LEFT:
  454. self.end_click_selection(wx, wy, modify_selection)
  455. def end_click_selection(self, wx, wy, modify_selection):
  456. under_mouse_unique = self.single_actor_from_mouse()
  457. if modify_selection:
  458. # toggle selected status for unique
  459. if under_mouse_unique in self.selection:
  460. self.selection_remove(under_mouse_unique)
  461. elif under_mouse_unique is not None:
  462. self.selection_add(under_mouse_unique)
  463. else:
  464. # new_selected becomes the current selected
  465. self.selection.clear()
  466. self.user_action_pending = None
  467. if under_mouse_unique is not None:
  468. self.selection_add(under_mouse_unique)
  469. def selection_add(self, actor):
  470. self.selection[actor] = actor.cshape.copy()
  471. def selection_remove(self, actor):
  472. del self.selection[actor]
  473. def end_drag_selection(self, wx, wy, modify_selection):
  474. new_selection = self.collision_manager.objs_into_box(*self.elastic_box_wminmax)
  475. if not modify_selection:
  476. # new_selected becomes the current selected
  477. self.selection.clear()
  478. for actor in new_selection:
  479. self.selection_add(actor)
  480. self.elastic_box.visible = False
  481. self.drag_selecting = False
  482. def on_mouse_drag(self, sx, sy, dx, dy, buttons, modifiers):
  483. # TODO: inhibir esta llamada si estamos fuera de la client area / viewport
  484. self.update_mouse_position(sx, sy)
  485. if not buttons & mouse.LEFT:
  486. # ignore except for left-btn-drag
  487. return
  488. if not self.dragging:
  489. print("begin drag")
  490. self.begin_drag()
  491. return
  492. if self.drag_selecting:
  493. # update elastic box
  494. self.adjust_elastic_box()
  495. elif self.drag_moving:
  496. self.restricted_mov = (modifiers & self.mod_restricted_mov)
  497. def adjust_elastic_box(self):
  498. # when elastic_box visible this method needs to be called any time
  499. # world_mouse changes or screen_to_world results changes (scroll, etc)
  500. wx0, wy0 = self.wdrag_start_point
  501. wx1, wy1 = self.world_mouse
  502. wminx = min(wx0, wx1)
  503. wmaxx = max(wx0, wx1)
  504. wminy = min(wy0, wy1)
  505. wmaxy = max(wy0, wy1)
  506. self.elastic_box_wminmax = wminx, wmaxx, wminy, wmaxy
  507. self.elastic_box.adjust_from_w_minmax(*self.elastic_box_wminmax)
  508. def begin_drag(self):
  509. self.dragging = True
  510. self.wdrag_start_point = self.world_mouse
  511. under_mouse_unique = self.single_actor_from_mouse()
  512. if under_mouse_unique is None:
  513. # begin drag selection
  514. self.drag_selecting = True
  515. self.adjust_elastic_box()
  516. self.elastic_box.visible = True
  517. print("begin drag selection: drag_selecting, drag_moving",
  518. self.drag_selecting, self.drag_moving)
  519. else:
  520. # want drag move
  521. if under_mouse_unique in self.selection:
  522. # want to move current selection
  523. pass
  524. else:
  525. # change selection before moving
  526. self.selection.clear()
  527. self.selection_add(under_mouse_unique)
  528. self.begin_drag_move()
  529. def begin_drag_move(self):
  530. # begin drag move
  531. self.drag_moving = True
  532. # how-to update collman: remove/add vs clear/add all
  533. # when total number of actors is low anyone will be fine,
  534. # with high numbers, most probably we move only a small fraction
  535. # For simplicity I choose remove/add, albeit a hybrid aproach
  536. # can be implemented later
  537. self.set_selection_in_collman(False)
  538. # print "begin drag: drag_selecting, drag_moving", self.drag_selecting, self.drag_moving
  539. def end_drag_move(self, wx, wy):
  540. self.set_selection_in_collman(True)
  541. for actor in self.selection:
  542. self.selection[actor] = actor.cshape.copy()
  543. self.drag_moving = False
  544. def single_actor_from_mouse(self):
  545. under_mouse = self.collision_manager.objs_touching_point(*self.world_mouse)
  546. if len(under_mouse) == 0:
  547. return None
  548. # return the one with the center most near to mouse, if tie then
  549. # an arbitrary in the tie
  550. nearest = None
  551. near_d = None
  552. p = euclid.Vector2(*self.world_mouse)
  553. for actor in under_mouse:
  554. d = (actor.cshape.center - p).magnitude_squared()
  555. if nearest is None or (d < near_d):
  556. nearest = actor
  557. near_d = d
  558. return nearest
  559. def set_selection_in_collman(self, bool_value):
  560. if self.selection_in_collman == bool_value:
  561. return
  562. self.selection_in_collman = bool_value
  563. if bool_value:
  564. for actor in self.selection:
  565. self.collision_manager.add(actor)
  566. else:
  567. for actor in self.selection:
  568. self.collision_manager.remove_tricky(actor)
  569. def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
  570. # TODO: check if mouse over scroller viewport?
  571. self.wheel += scroll_y * self.wheel_multiplier
  572. class MainLayer(ScrollableLayer):
  573. is_event_handler = True
  574. def __init__(
  575. self,
  576. layer_manager: LayerManager,
  577. grid_manager: GridManager,
  578. width: int,
  579. height: int,
  580. scroll_step: int=100,
  581. ) -> None:
  582. super().__init__()
  583. self.layer_manager = layer_manager
  584. self.scroll_step = scroll_step
  585. self.grid_manager = grid_manager
  586. self.width = width
  587. self.height = height
  588. self.px_width = width
  589. self.px_height = height
  590. class SubjectMapper(object):
  591. def __init__(
  592. self,
  593. config: Config,
  594. actor_class: typing.Type[Actor],
  595. ) -> None:
  596. self.config = config
  597. self.actor_class = actor_class
  598. def append(
  599. self,
  600. subject: XYZSubjectMixin,
  601. layer_manager: LayerManager,
  602. ) -> None:
  603. actor = self.actor_class(self.config, subject)
  604. pixel_position = layer_manager.grid_manager.get_world_position_of_grid_position(
  605. (subject.position[0], subject.position[1]),
  606. )
  607. actor.update_position(euclid.Vector2(*pixel_position))
  608. # TODO: Selectable nature must be configurable
  609. layer_manager.add_subject(actor)
  610. layer_manager.set_selectable(actor)
  611. class SubjectMapperFactory(object):
  612. def __init__(self) -> None:
  613. self.mapping = {} # type: typing.Dict[typing.Type[XYZSubjectMixin], SubjectMapper]
  614. def register_mapper(self, subject_class: typing.Type[XYZSubjectMixin], mapper: SubjectMapper) -> None:
  615. if subject_class not in self.mapping:
  616. self.mapping[subject_class] = mapper
  617. else:
  618. raise ValueError('subject_class already register with "{}"'.format(str(self.mapping[subject_class])))
  619. def get_subject_mapper(self, subject: XYZSubjectMixin) -> SubjectMapper:
  620. for subject_class, mapper in self.mapping.items():
  621. if isinstance(subject, subject_class):
  622. return mapper
  623. raise KeyError('No mapper for subject "{}"'.format(str(subject)))
  624. class Gui(object):
  625. layer_manager_class = LayerManager
  626. def __init__(
  627. self,
  628. config: Config,
  629. terminal: Terminal,
  630. physics: Physics,
  631. read_queue_interval: float= 1/60.0,
  632. ):
  633. self.config = config
  634. self.logger = get_logger('Gui', config)
  635. self.physics = physics
  636. self._read_queue_interval = read_queue_interval
  637. self.terminal = terminal
  638. self.cycle_duration = self.config.resolve('core.cycle_duration')
  639. # Manager cache directory
  640. cache_dir_path = self.config.resolve('global.cache_dir_path')
  641. if not cache_dir_path:
  642. raise SynergineException(
  643. 'This code require the "global.cache_dir_path" config',
  644. )
  645. ensure_dir_exist(cache_dir_path)
  646. cocos.director.director.init(
  647. width=640,
  648. height=480,
  649. vsync=True,
  650. resizable=False
  651. )
  652. mixer.init()
  653. self.interaction_manager = InteractionManager(
  654. config=self.config,
  655. terminal=self.terminal,
  656. )
  657. self.layer_manager = self.layer_manager_class(
  658. self.config,
  659. middleware=self.get_layer_middleware(),
  660. interaction_manager=self.interaction_manager,
  661. gui=self,
  662. )
  663. self.layer_manager.init()
  664. self.layer_manager.connect_layers()
  665. self.layer_manager.center()
  666. # Enable blending
  667. pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
  668. pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
  669. # Enable transparency
  670. pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
  671. pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
  672. self.subject_mapper_factory = SubjectMapperFactory()
  673. def get_layer_middleware(self) -> MapMiddleware:
  674. raise NotImplementedError()
  675. def run(self):
  676. self.before_run()
  677. pyglet.clock.schedule_interval(
  678. lambda *_, **__: self.terminal.read(),
  679. self._read_queue_interval,
  680. )
  681. cocos.director.director.run(self.get_main_scene())
  682. def before_run(self) -> None:
  683. pass
  684. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  685. raise NotImplementedError()
  686. def before_received(self, package: TerminalPackage):
  687. pass
  688. def after_received(self, package: TerminalPackage):
  689. pass
  690. class TMXGui(Gui):
  691. def __init__(
  692. self,
  693. config: Config,
  694. terminal: Terminal,
  695. physics: Physics,
  696. read_queue_interval: float = 1 / 60.0,
  697. map_dir_path: str=None,
  698. ):
  699. assert map_dir_path
  700. self.map_dir_path = map_dir_path
  701. super(TMXGui, self).__init__(
  702. config,
  703. terminal,
  704. physics=physics,
  705. read_queue_interval=read_queue_interval,
  706. )
  707. self.physics = physics
  708. def get_layer_middleware(self) -> MapMiddleware:
  709. return TMXMiddleware(
  710. self.config,
  711. self.map_dir_path,
  712. )
  713. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  714. return self.layer_manager.main_scene
  715. def before_received(self, package: TerminalPackage):
  716. super().before_received(package)
  717. if package.subjects: # They are new subjects in the simulation
  718. for subject in package.subjects:
  719. self.append_subject(subject)
  720. def append_subject(self, subject: XYZSubjectMixin) -> None:
  721. subject_mapper = self.subject_mapper_factory.get_subject_mapper(subject)
  722. subject_mapper.append(subject, self.layer_manager)