ya2 · news · projects · code · about

housekeeping: ya2.utils.gui
[pmachines.git] / pmachines / editor / scene.py
1 from copy import deepcopy
2 from json import dumps
3 from logging import info
4 import hashlib
5 from panda3d.core import Texture, TextNode, LPoint3f
6 from direct.gui.OnscreenImage import OnscreenImage
7 from direct.gui.DirectGui import DirectButton, DirectEntry, \
8 YesNoDialog, DirectOptionMenu, DirectFrame
9 from direct.gui.DirectGuiGlobals import FLAT, NORMAL
10 from direct.gui.OnscreenText import OnscreenText
11 from direct.showbase.DirectObject import DirectObject
12 from pmachines.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
13 from pmachines.editor.scene_list import SceneList
14 from pmachines.editor.inspector import Inspector, PixelSpaceInspector, WorldSpaceInspector
15 from pmachines.editor.start_items import StartItems
16 from ya2.utils.gfx import Point, DirectGuiMixin
17 from pmachines.editor.augmented_frame import AugmentedDirectFrame
18 from ya2.utils.gui.base_page import DirectOptionMenuTestable
19 from pmachines.items.basketball import Basketball
20 from pmachines.items.box import Box
21 from pmachines.items.domino import Domino
22 from pmachines.items.shelf import Shelf
23 from pmachines.items.teetertooter import TeeterTooter
24 from pmachines.items.item import FixedStrategy, StillStrategy
25 from pmachines.items.domino import UpStrategy, DownStrategy
26 from pmachines.items.box import HitStrategy
27
28
29 class SceneEditor(DirectObject):
30
31 def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node, pos_mgr):
32 super().__init__()
33 self.__items = items
34 self.__json = json
35 self.__world = world
36 self.__mouse_plane_node = mouse_plane_node
37 self.__pos_mgr = pos_mgr
38 self.__dialog = None
39 if not json_name:
40 self.__json = json = {
41 'name': '',
42 'instructions': '',
43 'version': '',
44 'items': [],
45 'start_items': [],
46 'test_items': {
47 'pixel_space': [],
48 'world_space': []}}
49 self.__inspector = None
50 self.__context = context
51 self.__add_item = add_item
52 self._font = base.loader.load_font(
53 'assets/fonts/Hanken-Book.ttf')
54 self._font.clear()
55 self._font.set_pixels_per_unit(60)
56 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
57 self._font.set_outline((0, 0, 0, 1), .8, .2)
58 self.__item_classes = [Basketball, Box, Domino, Shelf, TeeterTooter]
59 self.__item_strategy_classes = [FixedStrategy, StillStrategy, HitStrategy, UpStrategy, DownStrategy]
60 self._common = {
61 'scale': .046,
62 'text_font': self._font,
63 'text_fg': (.9, .9, .9, 1),
64 'relief': FLAT,
65 'frameColor': (.4, .4, .4, .14),
66 'rolloverSound': loader.load_sfx(
67 'assets/audio/sfx/rollover.ogg'),
68 'clickSound': loader.load_sfx(
69 'assets/audio/sfx/click.ogg')}
70 tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
71 w, h, tw, l = 1.8, 1, 30, .36
72 self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
73 frameSize=(0, w, 0, h),
74 parent=base.a2dBottomCenter,
75 pos=(-w/2, 0, 0),
76 delta_drag=LPoint3f(0, 0, h),
77 collapse_pos=(.06, 1, .94),
78 pos_mgr=self.__pos_mgr,
79 frame_name='scene')
80 OnscreenText(
81 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
82 font=self._common['text_font'],
83 scale=self._common['scale'],
84 fg=self._common['text_fg'],
85 align=TextNode.A_right)
86 self.__filenamename_entry = DirectEntry(
87 scale=self._common['scale'],
88 pos=(l, 1, h - .1),
89 entryFont=self._font,
90 width=tw,
91 frameColor=self._common['frameColor'],
92 initialText=json_name,
93 parent=self._frm,
94 text_fg=self._common['text_fg'])
95 self.__filenamename_entry['text_fg'] = (1, 0, 0, 1)
96 self.__filenamename_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
97 self.__filenamename_entry.set_tooltip(_('The name of the file without the extension'), *tooltip_args)
98 self.__pos_mgr['editor_scene_filename'] = self.__filenamename_entry.pos_pixel()
99 OnscreenText(
100 _('Name'), pos=(l - .03, h - .2), parent=self._frm,
101 font=self._common['text_font'],
102 scale=self._common['scale'],
103 fg=self._common['text_fg'],
104 align=TextNode.A_right)
105 self.__name_entry = DirectEntry(
106 scale=self._common['scale'],
107 pos=(l, 1, h - .2),
108 entryFont=self._font,
109 width=tw,
110 frameColor=self._common['frameColor'],
111 initialText=json['name'],
112 parent=self._frm,
113 text_fg=self._common['text_fg'])
114 self.__name_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
115 self.__name_entry.set_tooltip(_('The title of the scene'), *tooltip_args)
116 self.__pos_mgr['editor_scene_name'] = self.__name_entry.pos_pixel()
117 OnscreenText(
118 _('Description'), pos=(l - .03, h - .3), parent=self._frm,
119 font=self._common['text_font'],
120 scale=self._common['scale'],
121 fg=self._common['text_fg'],
122 align=TextNode.A_right)
123 def add_line_break(txt, entry):
124 curpos = entry.node().getCursorPosition()
125 entry.set(txt[:curpos]+ "\n" + txt[curpos:])
126 entry.node().setCursorPosition(curpos+1)
127 entry['focus']=1
128 self.__instructions_entry = DirectEntry(
129 scale=self._common['scale'],
130 pos=(l, 1, h - .3),
131 entryFont=self._font,
132 width=tw,
133 numLines=12,
134 cursorKeys=True,
135 frameColor=self._common['frameColor'],
136 initialText=json['instructions'].replace('\\n', '\n'),
137 parent=self._frm,
138 text_fg=self._common['text_fg'])
139 self.__instructions_entry['command'] = add_line_break
140 self.__instructions_entry['extraArgs'] = [self.__instructions_entry]
141 self.__instructions_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
142 self.__instructions_entry.set_tooltip(_('The description of the scene'), *tooltip_args)
143 self.__pos_mgr['editor_scene_instructions'] = self.__instructions_entry.pos_pixel()
144 def load_images_btn(path, col):
145 colors = {
146 'gray': [
147 (.6, .6, .6, 1), # ready
148 (1, 1, 1, 1), # press
149 (.8, .8, .8, 1), # rollover
150 (.4, .4, .4, .4)],
151 'green': [
152 (.1, .68, .1, 1),
153 (.1, 1, .1, 1),
154 (.1, .84, .1, 1),
155 (.4, .1, .1, .4)]}[col]
156 return [self.__load_img_btn(path, col) for col in colors]
157 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
158 b = DirectButton(
159 image=load_images_btn('exitRight', 'gray'), scale=.05,
160 pos=(.06, 1, .06),
161 parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
162 frameColor=fcols[0],
163 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
164 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
165 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
166 self.__pos_mgr['editor_close'] = b.pos_pixel()
167 b.set_tooltip(_('Close the scene editor'), *tooltip_args)
168 b = DirectButton(
169 image=load_images_btn('save', 'gray'), scale=.05,
170 pos=(.06, 1, .18),
171 parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
172 frameColor=fcols[0],
173 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
174 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
175 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
176 self.__pos_mgr['editor_save'] = b.pos_pixel()
177 b.set_tooltip(_('Save the scene'), *tooltip_args)
178 b = DirectButton(
179 image=load_images_btn('menuList', 'gray'), scale=.05,
180 pos=(.06, 1, .30),
181 parent=self._frm, command=self.__on_scene_list, state=NORMAL, relief=FLAT,
182 frameColor=fcols[0],
183 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
184 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
185 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
186 self.__pos_mgr['editor_sorting'] = b.pos_pixel()
187 b.set_tooltip(_('Set the sorting of the scenes'), *tooltip_args)
188 # item_modules = glob('pmachines/items/*.py')
189 # item_modules = [basename(i)[:-3] for i in item_modules]
190 self.__new_items = {}
191 # for item_module in item_modules:
192 # mod_name = 'pmachines.items.' + item_module
193 # for member in import_module(mod_name).__dict__.values():
194 # if isclass(member) and issubclass(member, Item) and \
195 # member != Item:
196 # self.__new_items[member.__name__] = member
197 for i in self.__item_classes: self.__new_items[i.__name__] = i
198 OnscreenText(
199 _('new item'), pos=(.02, .46), parent=self._frm,
200 font=self._common['text_font'],
201 scale=self._common['scale'],
202 fg=self._common['text_fg'],
203 align=TextNode.A_left)
204 items = list(self.__new_items.keys())
205 def new_item_test_set(comps):
206 item_labels = [f'new_item_{i.lower()}' for i in items]
207 for i in item_labels:
208 if i in self.__pos_mgr:
209 del self.__pos_mgr[i]
210 for l, b in zip(item_labels, comps):
211 b.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
212 p = b.pos_pixel()
213 self.__pos_mgr[l] = (p[0] + 5, p[1])
214 b = DirectOptionMenuTestable(
215 scale=.05,
216 text=_('new item'), pos=(.02, 1, .4), items=items,
217 parent=self._frm, command=self.__on_new_item, state=NORMAL,
218 relief=FLAT, item_relief=FLAT,
219 frameColor=fcols[0], item_frameColor=fcols[0],
220 popupMenu_frameColor=fcols[0],
221 popupMarker_frameColor=fcols[0],
222 text_font=self._font, text_fg=(.9, .9, .9, 1),
223 highlightColor=(.9, .9, .9, .9),
224 item_text_font=self._font, item_text_fg=(.9, .9, .9, 1),
225 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
226 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
227 b.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
228 b._show_cb = new_item_test_set
229 b.set_tooltip(_('Create a new item'), *tooltip_args)
230 self.__pos_mgr['editor_new_item'] = b.pos_pixel()
231 b = DirectButton(
232 image=load_images_btn('start_items', 'gray'), scale=.05,
233 pos=(.06, 1, .58),
234 parent=self._frm, command=self.__on_start_items, state=NORMAL, relief=FLAT,
235 frameColor=fcols[0],
236 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
237 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
238 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
239 self.__pos_mgr['editor_start'] = b.pos_pixel()
240 b.set_tooltip(_('Initial items'), *tooltip_args)
241 b = DirectButton(
242 image=load_images_btn('plus', 'gray'), scale=.05,
243 pos=(.06, 1, .7),
244 parent=self._frm, command=self.__on_new_scene, state=NORMAL, relief=FLAT,
245 frameColor=fcols[0],
246 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
247 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
248 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
249 self.__pos_mgr['editor_new'] = b.pos_pixel()
250 b.set_tooltip(_('New scene'), *tooltip_args)
251 self.__test_items = []
252 self.__set_test_items()
253 self.__start_items = self.__scene_list = None
254 messenger.send('editor-start')
255 self.accept('editor-item-click', self.__on_item_click)
256 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
257 self.accept('editor-start-items-save', self.__on_start_items_save)
258 self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
259
260 def __set_test_items(self):
261 for pixel_space_item in self.__json['test_items']['pixel_space']:
262 print(pixel_space_item['id'], pixel_space_item['position'])
263 pos_pixel = pixel_space_item['position']
264 win_res = base.win.getXSize(), base.win.getYSize()
265 pos_win = (pos_pixel[0] / win_res[0] * 2 - 1,
266 1 - pos_pixel[1] / win_res[1] * 2)
267 p_from, p_to = Point(pos_win).from_to_points()
268 for hit in self.__world.ray_test_all(p_from, p_to).get_hits():
269 if hit.get_node() == self.__mouse_plane_node:
270 pos = hit.get_hit_pos()
271 self.__set_test_item(pos, pixel_space_item, PixelSpaceTestItem)
272 for world_space_item in self.__json['test_items']['world_space']:
273 print(world_space_item['id'], world_space_item['position'])
274 self.__set_test_item(world_space_item['position'], world_space_item, WorldSpaceTestItem)
275
276 def __set_test_item(self, pos, json, item_class):
277 test_item = item_class(
278 self.__context.world,
279 self.__context.plane_node,
280 self.__context.cb_inst,
281 self.__context.curr_bottom,
282 self.__context.repos,
283 json,
284 pos=(pos[0], 0, pos[-1]),
285 model_scale=.2)
286 self.__test_items += [test_item]
287
288 def __on_close(self):
289 self.__json['name'] = self.__name_entry.get()
290 self.__json['instructions'] = self.__instructions_entry.get()
291 if self.__compute_hash() == self.__json['version']:
292 self.destroy()
293 else:
294 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
295 text=_('You have unsaved changes. Really quit?'),
296 command=self.__actually_close)
297 self.__dialog['frameColor'] = (.4, .4, .4, .14)
298 self.__dialog['relief'] = FLAT
299 self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
300 self.__dialog.component('text0')['font'] = self._font
301 for b in self.__dialog.buttonList:
302 b['frameColor'] = (.4, .4, .4, .14)
303 b.component('text0')['fg'] = (.9, .9, .9, 1)
304 b.component('text0')['font'] = self._font
305 b.component('text1')['fg'] = (.9, .1, .1, 1)
306 b.component('text1')['font'] = self._font
307 b.component('text2')['fg'] = (.9, .9, .1, 1)
308 b.component('text2')['font'] = self._font
309
310 def __on_new_item(self, item):
311 info(f'new {item}')
312 item_json = {}
313 scale = 1
314 if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
315 scale = .2
316 _item = self.__new_items[item](
317 self.__context.world,
318 self.__context.plane_node,
319 self.__context.cb_inst,
320 self.__context.curr_bottom,
321 self.__context.repos,
322 item_json,
323 model_scale=scale)
324 _item._Item__editing = True
325 self.__add_item(_item)
326 item_json['class'] = _item.__class__.__name__
327 item_json['position'] = list(_item._np.get_pos())
328 if item == 'PixelSpaceTestItem':
329 del item_json['class']
330 self.__json['test_items']['pixel_space'] += [item_json]
331 elif item == 'WorldSpaceTestItem':
332 del item_json['class']
333 self.__json['test_items']['world_space'] += [item_json]
334 else:
335 self.__json['items'] += [item_json]
336
337 def __actually_close(self, arg):
338 if arg:
339 self.destroy()
340 messenger.send('editor-stop')
341 self.__dialog.cleanup()
342 self.__dialog = None
343
344 def __on_save(self):
345 self.__json['name'] = self.__name_entry.get()
346 self.__json['instructions'] = self.__instructions_entry.get()
347 self.__json['version'] = self.__compute_hash()
348 json_name = self.__filenamename_entry.get()
349 with open('assets/scenes/%s.json' % json_name, 'w') as f:
350 f.write(dumps(self.__json, indent=2, sort_keys=True))
351
352 def __on_scene_list(self):
353 self.__scene_list = SceneList(self.__pos_mgr)
354
355 def __load_img_btn(self, path, col):
356 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
357 img.set_transparency(True)
358 img.set_color(col)
359 img.detach_node()
360 return img
361
362 def __compute_hash(self):
363 new_dict = deepcopy(self.__json)
364 del new_dict['version']
365 new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
366 h = hashlib.new('sha256')
367 h.update(new_dict_str.encode())
368 return h.hexdigest()[:12]
369
370 def __on_item_click(self, item):
371 if self.__inspector and self.__inspector.item == item: return
372 if self.__inspector:
373 self.__inspector.destroy()
374 if item.__class__ == PixelSpaceTestItem:
375 self.__inspector = PixelSpaceInspector(item, self.__items)
376 elif item.__class__ == WorldSpaceTestItem:
377 self.__inspector = WorldSpaceInspector(item, self.__items, self.__pos_mgr)
378 else:
379 self.__inspector = Inspector(item, self.__items, self.__pos_mgr, self.__item_strategy_classes)
380
381 def __on_inspector_destroy(self):
382 self.__inspector = None
383
384 def __on_new_scene(self):
385 self.destroy()
386 messenger.send('editor-stop')
387 messenger.send('new_scene')
388
389 def __on_start_items(self):
390 self.__start_items = StartItems(self.__json['start_items'], self.__pos_mgr, self.__item_classes, self.__item_strategy_classes)
391
392 def __on_start_items_save(self, start_items):
393 self.__json['start_items'] = start_items
394 self.__on_save()
395
396 def __on_start_items_destroy(self):
397 self.__start_items = None
398
399 @property
400 def test_items(self):
401 return self.__test_items
402
403 def destroy(self):
404 self._frm.destroy()
405 if self.__inspector:
406 self.__inspector.destroy()
407 self.ignore('editor-item-click')
408 self.ignore('editor-inspector-destroy')
409 self.ignore('editor-start-items-save')
410 self.ignore('editor-start-items-destroy')
411 if self.__dialog: self.__actually_close(False)
412 for t in self.__test_items: t.destroy()
413 self.__test_items = []
414 if self.__start_items:
415 self.__start_items.destroy()
416 if self.__scene_list:
417 self.__scene_list.destroy()