gui.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. # coding: utf-8
  2. import weakref
  3. import cocos
  4. import pyglet
  5. from pyglet.window import key as wkey, mouse
  6. from cocos import collision_model, euclid
  7. from cocos.director import director
  8. from cocos.layer import ScrollableLayer, Layer
  9. from cocos.sprite import Sprite
  10. from synergine2.config import Config
  11. from synergine2.log import SynergineLogger
  12. from synergine2.terminals import Terminal
  13. from synergine2.terminals import TerminalPackage
  14. from synergine2_cocos2d.layer import LayerManager
  15. from synergine2_cocos2d.middleware import TMXMiddleware
  16. class GridManager(object):
  17. def __init__(
  18. self,
  19. layer: Layer,
  20. square_width: int,
  21. border: int=0,
  22. ):
  23. self.layer = layer
  24. self.square_width = square_width
  25. self.border = border
  26. @property
  27. def final_width(self):
  28. return self.square_width + self.border
  29. def scale_sprite(self, sprite: Sprite):
  30. sprite.scale_x = self.final_width / sprite.image.width
  31. sprite.scale_y = self.final_width / sprite.image.height
  32. def position_sprite(self, sprite: Sprite, grid_position):
  33. grid_x = grid_position[0]
  34. grid_y = grid_position[1]
  35. sprite.position = grid_x * self.final_width, grid_y * self.final_width
  36. def get_window_position(self, grid_position_x, grid_position_y):
  37. grid_x = grid_position_x
  38. grid_y = grid_position_y
  39. return grid_x * self.final_width, grid_y * self.final_width
  40. def get_grid_position(self, window_x, window_y, z=0) -> tuple:
  41. window_size = director.get_window_size()
  42. window_center_x = window_size[0] // 2
  43. window_center_y = window_size[1] // 2
  44. window_relative_x = window_x - window_center_x
  45. window_relative_y = window_y - window_center_y
  46. real_width = self.final_width * self.layer.scale
  47. return int(window_relative_x // real_width),\
  48. int(window_relative_y // real_width),\
  49. z
  50. class GridLayerMixin(object):
  51. def __init__(self, *args, **kwargs):
  52. square_width = kwargs.pop('square_width', 32)
  53. square_border = kwargs.pop('square_border', 2)
  54. self.grid_manager = GridManager(
  55. self,
  56. square_width=square_width,
  57. border=square_border,
  58. )
  59. super().__init__(*args, **kwargs)
  60. class ProbeQuad(cocos.cocosnode.CocosNode):
  61. def __init__(self, r, color4):
  62. super(ProbeQuad, self).__init__()
  63. self.color4 = color4
  64. self.vertexes = [(r, 0, 0), (0, r, 0), (-r, 0, 0), (0, -r, 0)]
  65. def draw(self):
  66. pyglet.gl.glPushMatrix()
  67. self.transform()
  68. pyglet.gl.glBegin(pyglet.gl.GL_QUADS)
  69. pyglet.gl.glColor4ub(*self.color4)
  70. for v in self.vertexes:
  71. pyglet.gl.glVertex3i(*v)
  72. pyglet.gl.glEnd()
  73. pyglet.gl.glPopMatrix()
  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 = (0, 0, 255)
  79. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  80. self.visible = False
  81. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  82. # asumes world to screen preserves order
  83. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  84. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  85. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  86. class EditLayer(cocos.layer.Layer):
  87. is_event_handler = True
  88. def __init__(
  89. self,
  90. layer_manager: LayerManager,
  91. worldview,
  92. bindings=None,
  93. fastness=None,
  94. autoscroll_border=10,
  95. autoscroll_fastness=None,
  96. wheel_multiplier=None,
  97. zoom_min=None,
  98. zoom_max=None,
  99. zoom_fastness=None,
  100. mod_modify_selection=None,
  101. mod_restricted_mov=None,
  102. ):
  103. super(EditLayer, self).__init__()
  104. self.layer_manager = layer_manager
  105. self.bindings = bindings
  106. buttons = {}
  107. modifiers = {}
  108. for k in bindings:
  109. buttons[bindings[k]] = 0
  110. modifiers[bindings[k]] = 0
  111. self.buttons = buttons
  112. self.modifiers = modifiers
  113. self.fastness = fastness
  114. self.autoscroll_border = autoscroll_border
  115. self.autoscroll_fastness = autoscroll_fastness
  116. self.wheel_multiplier = wheel_multiplier
  117. self.zoom_min = zoom_min
  118. self.zoom_max = zoom_max
  119. self.zoom_fastness = zoom_fastness
  120. self.mod_modify_selection = mod_modify_selection
  121. self.mod_restricted_mov = mod_restricted_mov
  122. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  123. self.weak_worldview = weakref.ref(worldview)
  124. self.wwidth = worldview.width
  125. self.wheight = worldview.height
  126. self.autoscrolling = False
  127. self.drag_selecting = False
  128. self.drag_moving = False
  129. self.restricted_mov = False
  130. self.wheel = 0
  131. self.dragging = False
  132. self.keyscrolling = False
  133. self.keyscrolling_descriptor = (0, 0)
  134. self.wdrag_start_point = (0, 0)
  135. self.elastic_box = None # type: MinMaxRect
  136. self.elastic_box_wminmax = 0, 0, 0, 0
  137. self.mouse_mark = None # type: ProbeQuad
  138. self.selection = {}
  139. self.screen_mouse = (0, 0)
  140. self.world_mouse = (0, 0)
  141. self.sleft = None
  142. self.sright = None
  143. self.sbottom = None
  144. self.s_top = None
  145. # opers that change cshape must ensure it goes to False,
  146. # selection opers must ensure it goes to True
  147. self.selection_in_collman = True
  148. # TODO: Hardcoded here, should be obtained from level properties or calc
  149. # from available actors or current actors in worldview
  150. gsize = 32 * 1.25
  151. self.collman = collision_model.CollisionManagerGrid(
  152. -gsize,
  153. self.wwidth + gsize,
  154. -gsize,
  155. self.wheight + gsize,
  156. gsize,
  157. gsize,
  158. )
  159. for actor in worldview.actors:
  160. self.collman.add(actor)
  161. self.schedule(self.update)
  162. def on_enter(self):
  163. super(EditLayer, self).on_enter()
  164. scene = self.get_ancestor(cocos.scene.Scene)
  165. if self.elastic_box is None:
  166. self.elastic_box = MinMaxRect(self.layer_manager)
  167. scene.add(self.elastic_box, z=10)
  168. if self.mouse_mark is None:
  169. self.mouse_mark = ProbeQuad(4, (0, 0, 0, 255))
  170. scene.add(self.mouse_mark, z=11)
  171. def update(self, dt):
  172. mx = self.buttons['right'] - self.buttons['left']
  173. my = self.buttons['up'] - self.buttons['down']
  174. dz = self.buttons['zoomin'] - self.buttons['zoomout']
  175. # scroll
  176. if self.autoscrolling:
  177. self.update_autoscroll(dt)
  178. else:
  179. # care for keyscrolling
  180. new_keyscrolling = ((len(self.selection) == 0) and
  181. (mx != 0 or my != 0))
  182. new_keyscrolling_descriptor = (mx, my)
  183. if ((new_keyscrolling != self.keyscrolling) or
  184. (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
  185. self.keyscrolling = new_keyscrolling
  186. self.keyscrolling_descriptor = new_keyscrolling_descriptor
  187. fastness = 1.0
  188. if mx != 0 and my != 0:
  189. fastness *= 0.707106 # 1/sqrt(2)
  190. self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
  191. if self.keyscrolling:
  192. self.update_autoscroll(dt)
  193. # selection move
  194. if self.drag_moving:
  195. # update positions
  196. wx, wy = self.world_mouse
  197. dx = wx - self.wdrag_start_point[0]
  198. dy = wy - self.wdrag_start_point[1]
  199. if self.restricted_mov:
  200. if abs(dy) > abs(dx):
  201. dx = 0
  202. else:
  203. dy = 0
  204. dpos = euclid.Vector2(dx, dy)
  205. for actor in self.selection:
  206. old_pos = self.selection[actor].center
  207. new_pos = old_pos + dpos
  208. # TODO: clamp new_pos so actor into world boundaries ?
  209. actor.update_position(new_pos)
  210. scroller = self.weak_scroller()
  211. # zoom
  212. zoom_change = (dz != 0 or self.wheel != 0)
  213. if zoom_change:
  214. if self.mouse_into_world():
  215. wzoom_center = self.world_mouse
  216. szoom_center = self.screen_mouse
  217. else:
  218. # decay to scroller unadorned
  219. wzoom_center = None
  220. if self.wheel != 0:
  221. dt_dz = 0.01666666 * self.wheel
  222. self.wheel = 0
  223. else:
  224. dt_dz = dt * dz
  225. zoom = scroller.scale + dt_dz * self.zoom_fastness
  226. if zoom < self.zoom_min:
  227. zoom = self.zoom_min
  228. elif zoom > self.zoom_max:
  229. zoom = self.zoom_max
  230. scroller.scale = zoom
  231. if wzoom_center is not None:
  232. # postprocess toward 'world point under mouse the same before
  233. # and after zoom' ; other restrictions may prevent fully comply
  234. wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
  235. fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
  236. fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
  237. scroller.set_focus(fx, fy)
  238. def update_mouse_position(self, sx, sy):
  239. self.screen_mouse = sx, sy
  240. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  241. # handle autoscroll
  242. border = self.autoscroll_border
  243. if border is not None:
  244. # sleft and companions includes the border
  245. scroller = self.weak_scroller()
  246. self.update_view_bounds()
  247. sdx = 0.0
  248. if sx < self.sleft:
  249. sdx = sx - self.sleft
  250. elif sx > self.sright:
  251. sdx = sx - self.sright
  252. sdy = 0.0
  253. if sy < self.sbottom:
  254. sdy = sy - self.sbottom
  255. elif sy > self.s_top:
  256. sdy = sy - self.s_top
  257. self.autoscrolling = sdx != 0.0 or sdy != 0.0
  258. if self.autoscrolling:
  259. self.autoscrolling_sdelta = (sdx / border, sdy / border)
  260. self.mouse_mark.position = self.layer_manager.scrolling_manager.world_to_screen(*self.world_mouse)
  261. def update_autoscroll(self, dt):
  262. fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
  263. scroller = self.weak_scroller()
  264. worldview = self.weak_worldview()
  265. f = self.autoscroll_fastness
  266. wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
  267. wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
  268. # ask scroller to try scroll (wdx, wdy)
  269. fx = scroller.restricted_fx + wdx
  270. fy = scroller.restricted_fy + wdy
  271. scroller.set_focus(fx, fy)
  272. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
  273. self.adjust_elastic_box()
  274. # self.update_view_bounds()
  275. def update_view_bounds(self):
  276. scroller = self.weak_scroller()
  277. scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
  278. scroller.restricted_fx,
  279. scroller.restricted_fy,
  280. )
  281. hw = scroller.view_w / 2.0
  282. hh = scroller.view_h / 2.0
  283. border = self.autoscroll_border
  284. self.sleft = scx - hw + border
  285. self.sright = scx + hw - border
  286. self.sbottom = scy - hh + border
  287. self.s_top = scy + hh - border
  288. def mouse_into_world(self):
  289. worldview = self.weak_worldview()
  290. # TODO: allow lower limits != 0 ?
  291. return ((0 <= self.world_mouse[0] <= worldview.width) and
  292. (0 <= self.world_mouse[1] <= worldview.height))
  293. def on_key_press(self, k, m):
  294. binds = self.bindings
  295. if k in binds:
  296. self.buttons[binds[k]] = 1
  297. self.modifiers[binds[k]] = 1
  298. return True
  299. return False
  300. def on_key_release(self, k, m):
  301. binds = self.bindings
  302. if k in binds:
  303. self.buttons[binds[k]] = 0
  304. self.modifiers[binds[k]] = 0
  305. return True
  306. return False
  307. def on_mouse_motion(self, sx, sy, dx, dy):
  308. self.update_mouse_position(sx, sy)
  309. def on_mouse_leave(self, sx, sy):
  310. self.autoscrolling = False
  311. def on_mouse_press(self, x, y, buttons, modifiers):
  312. pass
  313. def on_mouse_release(self, sx, sy, button, modifiers):
  314. # should we handle here mod_restricted_mov ?
  315. wx, wy = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  316. modify_selection = modifiers & self.mod_modify_selection
  317. if self.dragging:
  318. # ignore all buttons except left button
  319. if button != mouse.LEFT:
  320. return
  321. if self.drag_selecting:
  322. self.end_drag_selection(wx, wy, modify_selection)
  323. elif self.drag_moving:
  324. self.end_drag_move(wx, wy)
  325. self.dragging = False
  326. else:
  327. if button == mouse.LEFT:
  328. self.end_click_selection(wx, wy, modify_selection)
  329. def end_click_selection(self, wx, wy, modify_selection):
  330. under_mouse_unique = self.single_actor_from_mouse()
  331. if modify_selection:
  332. # toggle selected status for unique
  333. if under_mouse_unique in self.selection:
  334. self.selection_remove(under_mouse_unique)
  335. elif under_mouse_unique is not None:
  336. self.selection_add(under_mouse_unique)
  337. else:
  338. # new_selected becomes the current selected
  339. self.selection.clear()
  340. if under_mouse_unique is not None:
  341. self.selection_add(under_mouse_unique)
  342. def selection_add(self, actor):
  343. self.selection[actor] = actor.cshape.copy()
  344. def selection_remove(self, actor):
  345. del self.selection[actor]
  346. def end_drag_selection(self, wx, wy, modify_selection):
  347. new_selection = self.collman.objs_into_box(*self.elastic_box_wminmax)
  348. if not modify_selection:
  349. # new_selected becomes the current selected
  350. self.selection.clear()
  351. for actor in new_selection:
  352. self.selection_add(actor)
  353. self.elastic_box.visible = False
  354. self.drag_selecting = False
  355. def on_mouse_drag(self, sx, sy, dx, dy, buttons, modifiers):
  356. # TODO: inhibir esta llamada si estamos fuera de la client area / viewport
  357. self.update_mouse_position(sx, sy)
  358. if not buttons & mouse.LEFT:
  359. # ignore except for left-btn-drag
  360. return
  361. if not self.dragging:
  362. print("begin drag")
  363. self.begin_drag()
  364. return
  365. if self.drag_selecting:
  366. # update elastic box
  367. self.adjust_elastic_box()
  368. elif self.drag_moving:
  369. self.restricted_mov = (modifiers & self.mod_restricted_mov)
  370. def adjust_elastic_box(self):
  371. # when elastic_box visible this method needs to be called any time
  372. # world_mouse changes or screen_to_world results changes (scroll, etc)
  373. wx0, wy0 = self.wdrag_start_point
  374. wx1, wy1 = self.world_mouse
  375. wminx = min(wx0, wx1)
  376. wmaxx = max(wx0, wx1)
  377. wminy = min(wy0, wy1)
  378. wmaxy = max(wy0, wy1)
  379. self.elastic_box_wminmax = wminx, wmaxx, wminy, wmaxy
  380. self.elastic_box.adjust_from_w_minmax(*self.elastic_box_wminmax)
  381. def begin_drag(self):
  382. self.dragging = True
  383. self.wdrag_start_point = self.world_mouse
  384. under_mouse_unique = self.single_actor_from_mouse()
  385. if under_mouse_unique is None:
  386. # begin drag selection
  387. self.drag_selecting = True
  388. self.adjust_elastic_box()
  389. self.elastic_box.visible = True
  390. print("begin drag selection: drag_selecting, drag_moving",
  391. self.drag_selecting, self.drag_moving)
  392. else:
  393. # want drag move
  394. if under_mouse_unique in self.selection:
  395. # want to move current selection
  396. pass
  397. else:
  398. # change selection before moving
  399. self.selection.clear()
  400. self.selection_add(under_mouse_unique)
  401. self.begin_drag_move()
  402. def begin_drag_move(self):
  403. # begin drag move
  404. self.drag_moving = True
  405. # how-to update collman: remove/add vs clear/add all
  406. # when total number of actors is low anyone will be fine,
  407. # with high numbers, most probably we move only a small fraction
  408. # For simplicity I choose remove/add, albeit a hybrid aproach
  409. # can be implemented later
  410. self.set_selection_in_collman(False)
  411. # print "begin drag: drag_selecting, drag_moving", self.drag_selecting, self.drag_moving
  412. def end_drag_move(self, wx, wy):
  413. self.set_selection_in_collman(True)
  414. for actor in self.selection:
  415. self.selection[actor] = actor.cshape.copy()
  416. self.drag_moving = False
  417. def single_actor_from_mouse(self):
  418. under_mouse = self.collman.objs_touching_point(*self.world_mouse)
  419. if len(under_mouse) == 0:
  420. return None
  421. # return the one with the center most near to mouse, if tie then
  422. # an arbitrary in the tie
  423. nearest = None
  424. near_d = None
  425. p = euclid.Vector2(*self.world_mouse)
  426. for actor in under_mouse:
  427. d = (actor.cshape.center - p).magnitude_squared()
  428. if nearest is None or (d < near_d):
  429. nearest = actor
  430. near_d = d
  431. return nearest
  432. def set_selection_in_collman(self, bool_value):
  433. if self.selection_in_collman == bool_value:
  434. return
  435. self.selection_in_collman = bool_value
  436. if bool_value:
  437. for actor in self.selection:
  438. self.collman.add(actor)
  439. else:
  440. for actor in self.selection:
  441. self.collman.remove_tricky(actor)
  442. def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
  443. # TODO: check if mouse over scroller viewport?
  444. self.wheel += scroll_y * self.wheel_multiplier
  445. class MainLayer(ScrollableLayer):
  446. is_event_handler = True
  447. def __init__(
  448. self,
  449. layer_manager: LayerManager,
  450. width: int,
  451. height: int,
  452. scroll_step: int=100,
  453. ) -> None:
  454. super().__init__()
  455. self.layer_manager = layer_manager
  456. self.scroll_step = scroll_step
  457. self.grid_manager = GridManager(self, 32, border=2)
  458. self.actors = [] # TODO type list of ? Sprite ?
  459. self.width = width
  460. self.height = height
  461. self.px_width = width
  462. self.px_height = height
  463. # Set scene center on center of screen
  464. window_size = director.get_window_size()
  465. self.position = window_size[0] // 2, window_size[1] // 2
  466. def on_key_press(self, key, modifiers):
  467. if key == wkey.LEFT:
  468. self.position = (self.position[0] + self.scroll_step, self.position[1])
  469. if key == wkey.RIGHT:
  470. self.position = (self.position[0] - self.scroll_step, self.position[1])
  471. if key == wkey.UP:
  472. self.position = (self.position[0], self.position[1] - self.scroll_step)
  473. if key == wkey.DOWN:
  474. self.position = (self.position[0], self.position[1] + self.scroll_step)
  475. if key == wkey.A:
  476. if self.scale >= 0.3:
  477. self.scale -= 0.2
  478. if key == wkey.Z:
  479. if self.scale <= 4:
  480. self.scale += 0.2
  481. class Gui(object):
  482. def __init__(
  483. self,
  484. config: Config,
  485. logger: SynergineLogger,
  486. terminal: Terminal,
  487. read_queue_interval: float= 1/60.0,
  488. ):
  489. self.config = config
  490. self.logger = logger
  491. self._read_queue_interval = read_queue_interval
  492. self.terminal = terminal
  493. self.cycle_duration = self.config.core.cycle_duration
  494. cocos.director.director.init(
  495. width=640,
  496. height=480,
  497. vsync=True,
  498. resizable=True,
  499. )
  500. def run(self):
  501. self.before_run()
  502. pyglet.clock.schedule_interval(
  503. lambda *_, **__: self.terminal.read(),
  504. self._read_queue_interval,
  505. )
  506. cocos.director.director.run(self.get_main_scene())
  507. def before_run(self) -> None:
  508. pass
  509. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  510. raise NotImplementedError()
  511. def before_received(self, package: TerminalPackage):
  512. pass
  513. def after_received(self, package: TerminalPackage):
  514. pass
  515. class TMXGui(Gui):
  516. def __init__(
  517. self,
  518. config: Config,
  519. logger: SynergineLogger,
  520. terminal: Terminal,
  521. read_queue_interval: float = 1 / 60.0,
  522. map_dir_path: str=None,
  523. ):
  524. assert map_dir_path
  525. super(TMXGui, self).__init__(
  526. config,
  527. logger,
  528. terminal,
  529. read_queue_interval,
  530. )
  531. self.map_dir_path = map_dir_path
  532. self.layer_manager = LayerManager(
  533. self.config,
  534. self.logger,
  535. middleware=TMXMiddleware(
  536. self.config,
  537. self.logger,
  538. self.map_dir_path,
  539. ),
  540. )
  541. self.layer_manager.init()
  542. self.layer_manager.center()
  543. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  544. return self.layer_manager.main_scene