ya2 · news · projects · code · about

undo/redo
[pmachines.git] / pmachines / scene.py
1 from panda3d.core import AmbientLight, DirectionalLight, Point3, Texture, \
2 TextPropertiesManager, TextNode, Spotlight, PerspectiveLens, BitMask32
3 from panda3d.bullet import BulletPlaneShape, BulletGhostNode
4 from direct.gui.OnscreenImage import OnscreenImage
5 from direct.gui.OnscreenText import OnscreenText
6 from direct.gui.DirectGui import DirectButton, DirectFrame
7 from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
8 from direct.showbase.DirectObject import DirectObject
9 from pmachines.items.background import Background
10 from pmachines.items.box import Box
11 from pmachines.sidepanel import SidePanel
12 from lib.engine.gui.cursor import MouseCursor
13 from lib.lib.p3d.gfx import P3dGfxMgr
14
15
16 class Scene(DirectObject):
17
18 def __init__(self, world, exit_cb, auto_close_instr):
19 super().__init__()
20 self._world = world
21 self._exit_cb = exit_cb
22 self._set_camera()
23 self._cursor = MouseCursor(
24 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
25 (.01, .01))
26 self._set_gui()
27 self._set_lights()
28 self._set_input()
29 self._set_mouse_plane()
30 self.items = [Box(world, self._mouse_plane_node, 3, self.cb_inst)]
31 self._commands = []
32 self._command_idx = 0
33 self._paused = False
34 self._item_active = None
35 if auto_close_instr:
36 self.__store_state()
37 self.__restore_state()
38 else:
39 self._set_instructions()
40 self._bg = Background()
41 self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.items)
42 self._scene_tsk = taskMgr.add(self.on_frame, 'on_frame')
43
44 def reset(self):
45 self._commands = []
46 self._command_idx = 0
47 [itm.destroy() for itm in self.items]
48 self.items = [Box(self._world, self._mouse_plane_node, 3, self.cb_inst)]
49
50 def destroy(self):
51 self._unset_gui()
52 self._unset_lights()
53 self._unset_input()
54 self._unset_mouse_plane()
55 [itm.destroy() for itm in self.items]
56 self._bg.destroy()
57 self._side_panel.destroy()
58 self._cursor.destroy()
59 taskMgr.remove(self._scene_tsk)
60
61 def _set_camera(self):
62 base.camera.set_pos(0, -20, 0)
63 base.camera.look_at(0, 0, 0)
64
65 def __load_img_btn(self, path, col):
66 img = OnscreenImage('assets/buttons/%s.png' % path)
67 img.set_transparency(True)
68 img.set_color(col)
69 img.detach_node()
70 return img
71
72 def _set_gui(self):
73 def load_images_btn(path, col):
74 colors = {
75 'gray': [
76 (.6, .6, .6, 1), # ready
77 (1, 1, 1, 1), # press
78 (.8, .8, .8, 1), # rollover
79 (.4, .4, .4, .4)],
80 'green': [
81 (.1, .68, .1, 1),
82 (.1, 1, .1, 1),
83 (.1, .84, .1, 1),
84 (.4, .1, .1, .4)]}[col]
85 return [self.__load_img_btn(path, col) for col in colors]
86 abl, abr = base.a2dBottomLeft, base.a2dBottomRight
87 btn_info = [
88 ('home', self.on_home, NORMAL, abl, 'gray'),
89 ('information', self._set_instructions, NORMAL, abl, 'gray'),
90 ('right', self.on_play, NORMAL, abr, 'green'),
91 ('next', self.on_next, DISABLED, abr, 'gray'),
92 ('previous', self.on_prev, DISABLED, abr, 'gray'),
93 ('rewind', self.reset, NORMAL, abr, 'gray')]
94 num_l = num_r = 0
95 btns = []
96 for binfo in btn_info:
97 imgs = load_images_btn(binfo[0], binfo[4])
98 if binfo[3] == base.a2dBottomLeft:
99 sign, num = 1, num_l
100 num_l += 1
101 else:
102 sign, num = -1, num_r
103 num_r += 1
104 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
105 btn = DirectButton(
106 image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
107 parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
108 frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
109 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
110 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
111 btn.set_transparency(True)
112 btns += [btn]
113 self.__home_btn, self.__info_btn, self.__right_btn, self.__next_btn, \
114 self.__prev_btn, self.__rewind_btn = btns
115
116 def _unset_gui(self):
117 btns = [
118 self.__home_btn, self.__info_btn, self.__right_btn,
119 self.__next_btn, self.__prev_btn, self.__rewind_btn]
120 [btn.destroy() for btn in btns]
121
122 def _set_spotlight(self, name, pos, look_at, color, shadows=False):
123 light = Spotlight(name)
124 if shadows:
125 light.setLens(PerspectiveLens())
126 light_np = render.attach_new_node(light)
127 light_np.set_pos(pos)
128 light_np.look_at(look_at)
129 light.set_color(color)
130 render.set_light(light_np)
131 return light_np
132
133 def _set_lights(self):
134 alight = AmbientLight('alight') # for ao
135 alight.set_color((.15, .15, .15, 1))
136 self._alnp = render.attach_new_node(alight)
137 render.set_light(self._alnp)
138 self._key_light = self._set_spotlight(
139 'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
140 self._shadow_light = self._set_spotlight(
141 'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
142 self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
143 self._shadow_light.node().get_lens().set_film_size(2048, 2048)
144 self._shadow_light.node().get_lens().set_near_far(1, 256)
145 self._shadow_light.node().set_camera_mask(BitMask32(0x01))
146
147 def _unset_lights(self):
148 for light in [self._alnp, self._key_light, self._shadow_light]:
149 render.clear_light(light)
150 light.remove_node()
151
152 def _set_input(self):
153 self.accept('mouse1', self.on_click_l)
154 self.accept('mouse1-up', self.on_release)
155 self.accept('mouse3', self.on_click_r)
156 self.accept('mouse3-up', self.on_release)
157
158 def _unset_input(self):
159 for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
160 self.ignore(evt)
161
162 def _set_mouse_plane(self):
163 shape = BulletPlaneShape((0, -1, 0), 0)
164 #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
165 self._mouse_plane_node = BulletGhostNode('mouse plane')
166 self._mouse_plane_node.addShape(shape)
167 #np = render.attachNewNode(self._mouse_plane_node)
168 #self._world.attachRigidBody(self._mouse_plane_node)
169 self._world.attach_ghost(self._mouse_plane_node)
170
171 def _unset_mouse_plane(self):
172 self._world.remove_ghost(self._mouse_plane_node)
173
174 def _get_hits(self):
175 if not base.mouseWatcherNode.has_mouse(): return []
176 p_from, p_to = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
177 return self._world.ray_test_all(p_from, p_to).get_hits()
178
179 def _on_click(self, method):
180 if self._paused:
181 return
182 for hit in self._get_hits():
183 if hit.get_node() == self._mouse_plane_node:
184 pos = hit.get_hit_pos()
185 for hit in self._get_hits():
186 for item in [i for i in self.items if hit.get_node() == i.node]:
187 if not self._item_active:
188 self._item_active = item
189 getattr(item, method)(pos)
190 img = 'move' if method == 'on_click_l' else 'rotate'
191 self._cursor.set_image('assets/buttons/%s.png' % img)
192
193 def on_click_l(self):
194 self._on_click('on_click_l')
195
196 def on_click_r(self):
197 self._on_click('on_click_r')
198
199 def on_release(self):
200 if self._item_active and not self._item_active._first_command:
201 self._commands = self._commands[:self._command_idx]
202 self._commands += [self._item_active]
203 self._command_idx += 1
204 self.__prev_btn['state'] = NORMAL
205 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
206 self.__prev_btn['frameColor'] = fcols[0]
207 if self._item_active._command_idx == len(self._item_active._commands) - 1:
208 self.__next_btn['state'] = DISABLED
209 self.__next_btn['frameColor'] = fcols[1]
210 self._item_active = None
211 [item.on_release() for item in self.items]
212 self._cursor.set_image('assets/buttons/arrowUpLeft.png')
213
214 def on_aspect_ratio_changed(self):
215 [item.on_aspect_ratio_changed() for item in self.items]
216 self._side_panel.update(self.items)
217
218 def on_frame(self, task):
219 hits = self._get_hits()
220 pos = None
221 for hit in self._get_hits():
222 if hit.get_node() == self._mouse_plane_node:
223 pos = hit.get_hit_pos()
224 hit_nodes = [hit.get_node() for hit in hits]
225 if self._item_active:
226 items_hit = [self._item_active]
227 else:
228 items_hit = [itm for itm in self.items if itm.node in hit_nodes]
229 items_no_hit = [itm for itm in self.items if itm not in items_hit]
230 [itm.on_mouse_on() for itm in items_hit]
231 [itm.on_mouse_off() for itm in items_no_hit]
232 if pos and self._item_active:
233 self._item_active.on_mouse_move(pos)
234 return task.cont
235
236 def cb_inst(self, item):
237 self.items += [item]
238
239 def on_play(self):
240 [itm.play() for itm in self.items]
241
242 def on_next(self):
243 self._commands[self._command_idx].redo()
244 self._command_idx += 1
245 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
246 self.__prev_btn['state'] = NORMAL
247 self.__prev_btn['frameColor'] = fcols[0]
248 more_commands = self._command_idx < len(self._commands)
249 self.__next_btn['state'] = NORMAL if more_commands else DISABLED
250 self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
251
252 def on_prev(self):
253 self._command_idx -= 1
254 self._commands[self._command_idx].undo()
255 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
256 self.__next_btn['state'] = NORMAL
257 self.__next_btn['frameColor'] = fcols[0]
258 self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
259 self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
260
261 def on_home(self):
262 self._exit_cb()
263
264 def _set_instructions(self):
265 self._paused = True
266 self.__store_state()
267 mgr = TextPropertiesManager.get_global_ptr()
268 for name in ['mouse_l', 'mouse_r']:
269 graphic = OnscreenImage('assets/buttons/%s.png' % name)
270 graphic.set_scale(.5)
271 graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
272 graphic.get_texture().set_anisotropic_degree(2)
273 mgr.set_graphic(name, graphic)
274 graphic.set_z(-.2)
275 graphic.set_transparency(True)
276 graphic.detach_node()
277 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
278 frameSize=(-.6, .6, -.3, .3))
279 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
280 font.clear()
281 font.set_pixels_per_unit(60)
282 font.set_minfilter(Texture.FTLinearMipmapLinear)
283 font.set_outline((0, 0, 0, 1), .8, .2)
284 txt = _('keep \5mouse_l\5 pressed to drag an item\n\n'
285 'keep \5mouse_r\5 pressed to rotate an item')
286 self._txt = OnscreenText(
287 txt, parent=frm, font=font, scale=0.06, fg=(.9, .9, .9, 1),
288 align=TextNode.A_left)
289 u_l = self._txt.textNode.get_upper_left_3d()
290 l_r = self._txt.textNode.get_lower_right_3d()
291 w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
292 btn_scale = .05
293 mar = .06 # margin
294 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
295 z += (btn_scale + 2 * mar) / 2
296 self._txt['pos'] = -w / 2, z
297 u_l = self._txt.textNode.get_upper_left_3d()
298 l_r = self._txt.textNode.get_lower_right_3d()
299 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
300 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
301 frm['frameSize'] = fsz
302 colors = [
303 (.6, .6, .6, 1), # ready
304 (1, 1, 1, 1), # press
305 (.8, .8, .8, 1), # rollover
306 (.4, .4, .4, .4)]
307 imgs = [self.__load_img_btn('exitRight', col) for col in colors]
308 btn = DirectButton(
309 image=imgs, scale=btn_scale,
310 pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
311 parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
312 relief=FLAT, frameColor=(.6, .6, .6, .08),
313 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
314 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
315 btn.set_transparency(True)
316
317 def __store_state(self):
318 btns = [
319 self.__home_btn, self.__info_btn, self.__right_btn,
320 self.__next_btn, self.__prev_btn, self.__rewind_btn]
321 self.__btn_state = [btn['state'] for btn in btns]
322 for btn in btns:
323 btn['state'] = DISABLED
324 [itm.store_state() for itm in self.items]
325
326 def __restore_state(self):
327 btns = [
328 self.__home_btn, self.__info_btn, self.__right_btn,
329 self.__next_btn, self.__prev_btn, self.__rewind_btn]
330 for btn, state in zip(btns, self.__btn_state):
331 btn['state'] = state
332 [itm.restore_state() for itm in self.items]
333 self._paused = False
334
335 def __on_close_instructions(self, frm):
336 frm.remove_node()
337 self.__restore_state()