ya2 · news · projects · code · about

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