gui.py 29KB

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