gui.py 30KB

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