gui.py 30KB

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