ya2 · news · projects · code · about

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