gui.py 21KB

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