gui.py 28KB

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