gui.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  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.log import SynergineLogger
  15. from synergine2.terminals import Terminal
  16. from synergine2.terminals import TerminalPackage
  17. from synergine2_cocos2d.actor import Actor
  18. from synergine2_cocos2d.const import SELECTION_COLOR_RGB
  19. from synergine2_cocos2d.const import DEFAULT_SELECTION_COLOR_RGB
  20. from synergine2_cocos2d.exception import InteractionNotFound
  21. from synergine2_cocos2d.exception import OuterWorldPosition
  22. from synergine2_cocos2d.gl import draw_rectangle
  23. from synergine2_cocos2d.gl import rectangle_positions_type
  24. from synergine2_cocos2d.interaction import InteractionManager
  25. from synergine2_cocos2d.layer import LayerManager
  26. from synergine2_cocos2d.middleware import MapMiddleware
  27. from synergine2_cocos2d.middleware import TMXMiddleware
  28. from synergine2_cocos2d.user_action import UserAction
  29. from synergine2_xyz.physics import Physics
  30. from synergine2_xyz.xyz import XYZSubjectMixin
  31. class GridManager(object):
  32. def __init__(
  33. self,
  34. cell_width: int,
  35. cell_height: int,
  36. world_width: int,
  37. world_height: int,
  38. ) -> None:
  39. self.cell_width = cell_width
  40. self.cell_height = cell_height
  41. self.world_width = world_width
  42. self.world_height = world_height
  43. def get_grid_position(self, pixel_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  44. pixel_x, pixel_y = pixel_position
  45. cell_x = int(floor(pixel_x / self.cell_width))
  46. cell_y = int(floor(pixel_y / self.cell_height))
  47. if cell_x > self.world_width or cell_y > self.world_height or cell_x < 0 or cell_y < 0:
  48. raise OuterWorldPosition('Position "{}" is outer world ({}x{})'.format(
  49. (cell_x, cell_y),
  50. self.world_width,
  51. self.world_height,
  52. ))
  53. return cell_x, cell_y
  54. def get_world_position_of_grid_position(self, grid_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  55. return grid_position[0] * self.cell_width + (self.cell_width // 2),\
  56. grid_position[1] * self.cell_height + (self.cell_height // 2)
  57. def get_rectangle_positions(
  58. self,
  59. grid_position: typing.Tuple[int, int],
  60. ) -> rectangle_positions_type:
  61. """
  62. A<---D
  63. | |
  64. B--->C
  65. :param grid_position:grid position to exploit
  66. :return: grid pixel corners positions
  67. """
  68. grid_x, grid_y = grid_position
  69. a = grid_x * self.cell_width, grid_y * self.cell_height + self.cell_height
  70. b = grid_x * self.cell_width, grid_y * self.cell_height
  71. c = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height
  72. d = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height + self.cell_height
  73. return a, d, c, b
  74. class MinMaxRect(cocos.cocosnode.CocosNode):
  75. def __init__(self, layer_manager: LayerManager):
  76. super(MinMaxRect, self).__init__()
  77. self.layer_manager = layer_manager
  78. self.color3 = (20, 20, 20)
  79. self.color3f = (0, 0, 0, 0.2)
  80. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  81. self.visible = False
  82. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  83. # asumes world to screen preserves order
  84. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  85. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  86. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  87. def draw(self):
  88. if not self.visible:
  89. return
  90. draw_rectangle(
  91. self.vertexes,
  92. self.color3,
  93. self.color3f,
  94. )
  95. def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
  96. self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
  97. class FinishedCallback(Exception):
  98. pass
  99. class Callback(object):
  100. def __init__(
  101. self,
  102. func: typing.Callable[[], None],
  103. duration: float,
  104. delay: float=None,
  105. ) -> None:
  106. self.func = func
  107. self.duration = duration
  108. # Started timestamp
  109. self.started = None # type: float
  110. self.require_delay = False
  111. self.delay = delay
  112. if delay is not None:
  113. self.require_delay = True
  114. def execute(self) -> None:
  115. if self.require_delay and not self.started:
  116. self.started = time.time()
  117. return
  118. elif self.require_delay and time.time() - self.started < self.delay:
  119. return
  120. elif self.require_delay:
  121. self.started = None
  122. self.require_delay = False
  123. if self.started is None:
  124. self.started = time.time()
  125. if time.time() - self.started <= self.duration:
  126. self.func()
  127. elif not self.duration:
  128. self.func()
  129. raise FinishedCallback()
  130. else:
  131. raise FinishedCallback()
  132. class EditLayer(cocos.layer.Layer):
  133. is_event_handler = True
  134. def __init__(
  135. self,
  136. config: Config,
  137. logger: SynergineLogger,
  138. layer_manager: LayerManager,
  139. grid_manager: GridManager,
  140. worldview,
  141. bindings=None,
  142. fastness=None,
  143. autoscroll_border=10,
  144. autoscroll_fastness=None,
  145. wheel_multiplier=None,
  146. zoom_min=None,
  147. zoom_max=None,
  148. zoom_fastness=None,
  149. mod_modify_selection=None,
  150. mod_restricted_mov=None,
  151. ):
  152. # TODO: Clean init params
  153. super().__init__()
  154. self.config = config
  155. self.logger = logger
  156. self.layer_manager = layer_manager
  157. self.grid_manager = grid_manager
  158. self.bindings = bindings
  159. buttons = {}
  160. modifiers = {}
  161. for k in bindings:
  162. buttons[bindings[k]] = 0
  163. modifiers[bindings[k]] = 0
  164. self.buttons = buttons
  165. self.modifiers = modifiers
  166. self.fastness = fastness
  167. self.autoscroll_border = autoscroll_border
  168. self.autoscroll_fastness = autoscroll_fastness
  169. self.wheel_multiplier = wheel_multiplier
  170. self.zoom_min = zoom_min
  171. self.zoom_max = zoom_max
  172. self.zoom_fastness = zoom_fastness
  173. self.mod_modify_selection = mod_modify_selection
  174. self.mod_restricted_mov = mod_restricted_mov
  175. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  176. self.weak_worldview = weakref.ref(worldview)
  177. self.wwidth = worldview.width
  178. self.wheight = worldview.height
  179. self.autoscrolling = False
  180. self.drag_selecting = False
  181. self.drag_moving = False
  182. self.restricted_mov = False
  183. self.wheel = 0
  184. self.dragging = False
  185. self.keyscrolling = False
  186. self.keyscrolling_descriptor = (0, 0)
  187. self.wdrag_start_point = (0, 0)
  188. self.elastic_box = None # type: MinMaxRect
  189. self.elastic_box_wminmax = 0, 0, 0, 0
  190. self.selection = {} # type: typing.List[Actor]
  191. self.screen_mouse = (0, 0)
  192. self.world_mouse = (0, 0)
  193. self.sleft = None
  194. self.sright = None
  195. self.sbottom = None
  196. self.s_top = None
  197. self.user_action_pending = None # type: UserAction
  198. # opers that change cshape must ensure it goes to False,
  199. # selection opers must ensure it goes to True
  200. self.selection_in_collman = True
  201. # TODO: Hardcoded here, should be obtained from level properties or calc
  202. # from available actors or current actors in worldview
  203. gsize = 32 * 1.25
  204. self.collision_manager = collision_model.CollisionManagerGrid(
  205. -gsize,
  206. self.wwidth + gsize,
  207. -gsize,
  208. self.wheight + gsize,
  209. gsize,
  210. gsize,
  211. )
  212. self.schedule(self.update)
  213. self.selectable_actors = []
  214. # TODO: In top level class: to be available in all layers
  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.info(
  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. actor_class: typing.Type[Actor],
  594. ) -> None:
  595. self.actor_class = actor_class
  596. def append(
  597. self,
  598. subject: XYZSubjectMixin,
  599. layer_manager: LayerManager,
  600. ) -> None:
  601. actor = self.actor_class(subject)
  602. pixel_position = layer_manager.grid_manager.get_world_position_of_grid_position(
  603. (subject.position[0], subject.position[1]),
  604. )
  605. actor.update_position(euclid.Vector2(*pixel_position))
  606. # TODO: Selectable nature must be configurable
  607. layer_manager.add_subject(actor)
  608. layer_manager.set_selectable(actor)
  609. class SubjectMapperFactory(object):
  610. def __init__(self) -> None:
  611. self.mapping = {} # type: typing.Dict[typing.Type[XYZSubjectMixin], SubjectMapper]
  612. def register_mapper(self, subject_class: typing.Type[XYZSubjectMixin], mapper: SubjectMapper) -> None:
  613. if subject_class not in self.mapping:
  614. self.mapping[subject_class] = mapper
  615. else:
  616. raise ValueError('subject_class already register with "{}"'.format(str(self.mapping[subject_class])))
  617. def get_subject_mapper(self, subject: XYZSubjectMixin) -> SubjectMapper:
  618. for subject_class, mapper in self.mapping.items():
  619. if isinstance(subject, subject_class):
  620. return mapper
  621. raise KeyError('No mapper for subject "{}"'.format(str(subject)))
  622. class Gui(object):
  623. layer_manager_class = LayerManager
  624. def __init__(
  625. self,
  626. config: Config,
  627. logger: SynergineLogger,
  628. terminal: Terminal,
  629. physics: Physics,
  630. read_queue_interval: float= 1/60.0,
  631. ):
  632. self.config = config
  633. self.logger = logger
  634. self.physics = physics
  635. self._read_queue_interval = read_queue_interval
  636. self.terminal = terminal
  637. self.cycle_duration = self.config.resolve('core.cycle_duration')
  638. cocos.director.\
  639. director.init(
  640. width=640,
  641. height=480,
  642. vsync=True,
  643. resizable=False
  644. )
  645. mixer.init()
  646. self.interaction_manager = InteractionManager(
  647. config=self.config,
  648. logger=self.logger,
  649. terminal=self.terminal,
  650. )
  651. self.layer_manager = self.layer_manager_class(
  652. self.config,
  653. self.logger,
  654. middleware=self.get_layer_middleware(),
  655. interaction_manager=self.interaction_manager,
  656. gui=self,
  657. )
  658. self.layer_manager.init()
  659. self.layer_manager.center()
  660. # Enable blending
  661. pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
  662. pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
  663. # Enable transparency
  664. pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
  665. pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
  666. self.subject_mapper_factory = SubjectMapperFactory()
  667. def get_layer_middleware(self) -> MapMiddleware:
  668. raise NotImplementedError()
  669. def run(self):
  670. self.before_run()
  671. pyglet.clock.schedule_interval(
  672. lambda *_, **__: self.terminal.read(),
  673. self._read_queue_interval,
  674. )
  675. cocos.director.director.run(self.get_main_scene())
  676. def before_run(self) -> None:
  677. pass
  678. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  679. raise NotImplementedError()
  680. def before_received(self, package: TerminalPackage):
  681. pass
  682. def after_received(self, package: TerminalPackage):
  683. pass
  684. class TMXGui(Gui):
  685. def __init__(
  686. self,
  687. config: Config,
  688. logger: SynergineLogger,
  689. terminal: Terminal,
  690. physics: Physics,
  691. read_queue_interval: float = 1 / 60.0,
  692. map_dir_path: str=None,
  693. ):
  694. assert map_dir_path
  695. self.map_dir_path = map_dir_path
  696. super(TMXGui, self).__init__(
  697. config,
  698. logger,
  699. terminal,
  700. physics=physics,
  701. read_queue_interval=read_queue_interval,
  702. )
  703. self.physics = physics
  704. def get_layer_middleware(self) -> MapMiddleware:
  705. return TMXMiddleware(
  706. self.config,
  707. self.logger,
  708. self.map_dir_path,
  709. )
  710. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  711. return self.layer_manager.main_scene
  712. def before_received(self, package: TerminalPackage):
  713. super().before_received(package)
  714. if package.subjects: # They are new subjects in the simulation
  715. for subject in package.subjects:
  716. self.append_subject(subject)
  717. def append_subject(self, subject: XYZSubjectMixin) -> None:
  718. subject_mapper = self.subject_mapper_factory.get_subject_mapper(subject)
  719. subject_mapper.append(subject, self.layer_manager)