ya2 · news · projects · code · about

942ab5356f759b4e9d86ee462c6791d711ee8dcb
[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 pmachines.gui.options_page import DirectOptionMenuTestable
24
25
26 class SceneEditor(DirectObject):
27
28 def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node, pos_mgr):
29 super().__init__()
30 self.__items = items
31 self.__json = json
32 self.__world = world
33 self.__mouse_plane_node = mouse_plane_node
34 self.__pos_mgr = pos_mgr
35 self.__dialog = None
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': []}}
46 self.__inspector = None
47 self.__context = context
48 self.__add_item = add_item
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')}
65 tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
66 w, h, tw, l = 1.8, 1, 30, .36
67 self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
68 frameSize=(0, w, 0, h),
69 parent=base.a2dBottomCenter,
70 pos=(-w/2, 0, 0),
71 delta_drag=LPoint3f(0, 0, h),
72 collapse_pos=(.06, 1, .94),
73 pos_mgr=self.__pos_mgr,
74 frame_name='scene')
75 OnscreenText(
76 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
77 font=self._common['text_font'],
78 scale=self._common['scale'],
79 fg=self._common['text_fg'],
80 align=TextNode.A_right)
81 self.__filenamename_entry = DirectEntry(
82 scale=self._common['scale'],
83 pos=(l, 1, h - .1),
84 entryFont=self._font,
85 width=tw,
86 frameColor=self._common['frameColor'],
87 initialText=json_name,
88 parent=self._frm,
89 text_fg=self._common['text_fg'])
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)
92 self.__pos_mgr['editor_scene_filename'] = self.__filenamename_entry.pos_pixel()
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'],
105 initialText=json['name'],
106 parent=self._frm,
107 text_fg=self._common['text_fg'])
108 self.__name_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
109 self.__name_entry.set_tooltip(_('The title of the scene'), *tooltip_args)
110 self.__pos_mgr['editor_scene_name'] = self.__name_entry.pos_pixel()
111 OnscreenText(
112 _('Description'), pos=(l - .03, h - .3), parent=self._frm,
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'],
124 pos=(l, 1, h - .3),
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]
135 self.__instructions_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
136 self.__instructions_entry.set_tooltip(_('The description of the scene'), *tooltip_args)
137 self.__pos_mgr['editor_scene_instructions'] = self.__instructions_entry.pos_pixel()
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)
152 b = DirectButton(
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'))
159 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
160 self.__pos_mgr['editor_close'] = b.pos_pixel()
161 b.set_tooltip(_('Close the scene editor'), *tooltip_args)
162 b = DirectButton(
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'))
169 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
170 self.__pos_mgr['editor_save'] = b.pos_pixel()
171 b.set_tooltip(_('Save the scene'), *tooltip_args)
172 b = DirectButton(
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'))
179 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
180 self.__pos_mgr['editor_sorting'] = b.pos_pixel()
181 b.set_tooltip(_('Set the sorting of the scenes'), *tooltip_args)
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)
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(
208 scale=.05,
209 text=_('new item'), pos=(.02, 1, .4), items=items,
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'))
220 b.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
221 b._show_cb = new_item_test_set
222 b.set_tooltip(_('Create a new item'), *tooltip_args)
223 self.__pos_mgr['editor_new_item'] = b.pos_pixel()
224 b = DirectButton(
225 image=load_images_btn('start_items', 'gray'), scale=.05,
226 pos=(.06, 1, .58),
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'))
231 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
232 self.__pos_mgr['editor_start'] = b.pos_pixel()
233 b.set_tooltip(_('Initial items'), *tooltip_args)
234 b = DirectButton(
235 image=load_images_btn('plus', 'gray'), scale=.05,
236 pos=(.06, 1, .7),
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'))
241 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
242 self.__pos_mgr['editor_new'] = b.pos_pixel()
243 b.set_tooltip(_('New scene'), *tooltip_args)
244 self.__test_items = []
245 self.__set_test_items()
246 self.__start_items = self.__scene_list = None
247 messenger.send('editor-start')
248 self.accept('editor-item-click', self.__on_item_click)
249 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
250 self.accept('editor-start-items-save', self.__on_start_items_save)
251 self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
252
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)
260 p_from, p_to = Point(pos_win).from_to_points()
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
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']:
285 self.destroy()
286 else:
287 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
288 text=_('You have unsaved changes. Really quit?'),
289 command=self.__actually_close)
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
302
303 def __on_new_item(self, item):
304 info(f'new {item}')
305 item_json = {}
306 scale = 1
307 if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
308 scale = .2
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,
315 item_json,
316 model_scale=scale)
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())
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]
329
330 def __actually_close(self, arg):
331 if arg:
332 self.destroy()
333 messenger.send('editor-stop')
334 self.__dialog.cleanup()
335 self.__dialog = None
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()
341 json_name = self.__filenamename_entry.get()
342 with open('assets/scenes/%s.json' % json_name, 'w') as f:
343 f.write(dumps(self.__json, indent=2, sort_keys=True))
344
345 def __on_scene_list(self):
346 self.__scene_list = SceneList(self.__pos_mgr)
347
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]
362
363 def __on_item_click(self, item):
364 if self.__inspector and self.__inspector.item == item: return
365 if self.__inspector:
366 self.__inspector.destroy()
367 if item.__class__ == PixelSpaceTestItem:
368 self.__inspector = PixelSpaceInspector(item, self.__items)
369 elif item.__class__ == WorldSpaceTestItem:
370 self.__inspector = WorldSpaceInspector(item, self.__items, self.__pos_mgr)
371 else:
372 self.__inspector = Inspector(item, self.__items, self.__pos_mgr)
373
374 def __on_inspector_destroy(self):
375 self.__inspector = None
376
377 def __on_new_scene(self):
378 self.destroy()
379 messenger.send('editor-stop')
380 messenger.send('new_scene')
381
382 def __on_start_items(self):
383 self.__start_items = StartItems(self.__json['start_items'], self.__pos_mgr)
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
392 @property
393 def test_items(self):
394 return self.__test_items
395
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')
402 self.ignore('editor-start-items-save')
403 self.ignore('editor-start-items-destroy')
404 if self.__dialog: self.__actually_close(False)
405 for t in self.__test_items: t.destroy()
406 self.__test_items = []
407 if self.__start_items:
408 self.__start_items.destroy()
409 if self.__scene_list:
410 self.__scene_list.destroy()