ya2 · news · projects · code · about

127cee682688b898e56336761ed1ff5bc6a48e47
[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
10 from direct.gui.OnscreenImage import OnscreenImage
11 from direct.gui.DirectGui import DirectButton, DirectFrame, DirectEntry, \
12 YesNoDialog, DirectOptionMenu
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.p3d.gfx import P3dGfxMgr
22
23
24 class SceneEditor(DirectObject):
25
26 def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node):
27 super().__init__()
28 self.__items = items
29 self.__json = json
30 self.__world = world
31 self.__mouse_plane_node = mouse_plane_node
32 if not json_name:
33 self.__json = json = {
34 'name': '',
35 'instructions': '',
36 'version': '',
37 'items': [],
38 'start_items': [],
39 'test_items': {
40 'pixel_space': [],
41 'world_space': []}}
42 self.__inspector = None
43 self.__context = context
44 self.__add_item = add_item
45 self._font = base.loader.load_font(
46 'assets/fonts/Hanken-Book.ttf')
47 self._font.clear()
48 self._font.set_pixels_per_unit(60)
49 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
50 self._font.set_outline((0, 0, 0, 1), .8, .2)
51 self._common = {
52 'scale': .046,
53 'text_font': self._font,
54 'text_fg': (.9, .9, .9, 1),
55 'relief': FLAT,
56 'frameColor': (.4, .4, .4, .14),
57 'rolloverSound': loader.load_sfx(
58 'assets/audio/sfx/rollover.ogg'),
59 'clickSound': loader.load_sfx(
60 'assets/audio/sfx/click.ogg')}
61 w, h, tw, l = 1.8, 1, 30, .36
62 self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
63 frameSize=(0, w, 0, h),
64 parent=base.a2dBottomCenter,
65 pos=(-w/2, 0, 0))
66 OnscreenText(
67 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
68 font=self._common['text_font'],
69 scale=self._common['scale'],
70 fg=self._common['text_fg'],
71 align=TextNode.A_right)
72 self.__filenamename_entry = DirectEntry(
73 scale=self._common['scale'],
74 pos=(l, 1, h - .1),
75 entryFont=self._font,
76 width=tw,
77 frameColor=self._common['frameColor'],
78 initialText=json_name,
79 parent=self._frm,
80 text_fg=self._common['text_fg'])
81 OnscreenText(
82 _('Name'), pos=(l - .03, h - .2), parent=self._frm,
83 font=self._common['text_font'],
84 scale=self._common['scale'],
85 fg=self._common['text_fg'],
86 align=TextNode.A_right)
87 self.__name_entry = DirectEntry(
88 scale=self._common['scale'],
89 pos=(l, 1, h - .2),
90 entryFont=self._font,
91 width=tw,
92 frameColor=self._common['frameColor'],
93 initialText=json['name'],
94 parent=self._frm,
95 text_fg=self._common['text_fg'])
96 OnscreenText(
97 _('Description'), pos=(l - .03, h - .3), parent=self._frm,
98 font=self._common['text_font'],
99 scale=self._common['scale'],
100 fg=self._common['text_fg'],
101 align=TextNode.A_right)
102 def add_line_break(txt, entry):
103 curpos = entry.node().getCursorPosition()
104 entry.set(txt[:curpos]+ "\n" + txt[curpos:])
105 entry.node().setCursorPosition(curpos+1)
106 entry['focus']=1
107 self.__instructions_entry = DirectEntry(
108 scale=self._common['scale'],
109 pos=(l, 1, h - .3),
110 entryFont=self._font,
111 width=tw,
112 numLines=12,
113 cursorKeys=True,
114 frameColor=self._common['frameColor'],
115 initialText=json['instructions'].replace('\\n', '\n'),
116 parent=self._frm,
117 text_fg=self._common['text_fg'])
118 self.__instructions_entry['command'] = add_line_break
119 self.__instructions_entry['extraArgs'] = [self.__instructions_entry]
120 def load_images_btn(path, col):
121 colors = {
122 'gray': [
123 (.6, .6, .6, 1), # ready
124 (1, 1, 1, 1), # press
125 (.8, .8, .8, 1), # rollover
126 (.4, .4, .4, .4)],
127 'green': [
128 (.1, .68, .1, 1),
129 (.1, 1, .1, 1),
130 (.1, .84, .1, 1),
131 (.4, .1, .1, .4)]}[col]
132 return [self.__load_img_btn(path, col) for col in colors]
133 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
134 DirectButton(
135 image=load_images_btn('exitRight', 'gray'), scale=.05,
136 pos=(.06, 1, .06),
137 parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
138 frameColor=fcols[0],
139 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
140 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
141 DirectButton(
142 image=load_images_btn('save', 'gray'), scale=.05,
143 pos=(.06, 1, .18),
144 parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
145 frameColor=fcols[0],
146 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
147 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
148 DirectButton(
149 image=load_images_btn('menuList', 'gray'), scale=.05,
150 pos=(.06, 1, .30),
151 parent=self._frm, command=self.__on_scene_list, state=NORMAL, relief=FLAT,
152 frameColor=fcols[0],
153 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
154 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
155 item_modules = glob('pmachines/items/*.py')
156 item_modules = [basename(i)[:-3] for i in item_modules]
157 self.__new_items = {}
158 for item_module in item_modules:
159 mod_name = 'pmachines.items.' + item_module
160 for member in import_module(mod_name).__dict__.values():
161 if isclass(member) and issubclass(member, Item) and \
162 member != Item:
163 self.__new_items[member.__name__] = member
164 OnscreenText(
165 _('new item'), pos=(.02, .46), parent=self._frm,
166 font=self._common['text_font'],
167 scale=self._common['scale'],
168 fg=self._common['text_fg'],
169 align=TextNode.A_left)
170 DirectOptionMenu(
171 scale=.05,
172 text=_('new item'), pos=(.02, 1, .4), items=list(self.__new_items.keys()),
173 parent=self._frm, command=self.__on_new_item, state=NORMAL,
174 relief=FLAT, item_relief=FLAT,
175 frameColor=fcols[0], item_frameColor=fcols[0],
176 popupMenu_frameColor=fcols[0],
177 popupMarker_frameColor=fcols[0],
178 text_font=self._font, text_fg=(.9, .9, .9, 1),
179 highlightColor=(.9, .9, .9, .9),
180 item_text_font=self._font, item_text_fg=(.9, .9, .9, 1),
181 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
182 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
183 DirectButton(
184 image=load_images_btn('start_items', 'gray'), scale=.05,
185 pos=(.06, 1, .58),
186 parent=self._frm, command=self.__on_start_items, 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 DirectButton(
191 image=load_images_btn('plus', 'gray'), scale=.05,
192 pos=(.06, 1, .7),
193 parent=self._frm, command=self.__on_new_scene, state=NORMAL, relief=FLAT,
194 frameColor=fcols[0],
195 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
196 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
197 self.__test_items = []
198 self.__set_test_items()
199 messenger.send('editor-start')
200 self.accept('editor-item-click', self.__on_item_click)
201 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
202 self.accept('editor-start-items-save', self.__on_start_items_save)
203 self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
204
205 def __set_test_items(self):
206 for pixel_space_item in self.__json['test_items']['pixel_space']:
207 print(pixel_space_item['id'], pixel_space_item['position'])
208 pos_pixel = pixel_space_item['position']
209 win_res = base.win.getXSize(), base.win.getYSize()
210 pos_win = (pos_pixel[0] / win_res[0] * 2 - 1,
211 1 - pos_pixel[1] / win_res[1] * 2)
212 p_from, p_to = P3dGfxMgr.world_from_to(pos_win)
213 hits = self.__world.ray_test_all(p_from, p_to).get_hits()
214 for hit in self.__world.ray_test_all(p_from, p_to).get_hits():
215 if hit.get_node() == self.__mouse_plane_node:
216 pos = hit.get_hit_pos()
217 self.__set_test_item(pos, pixel_space_item, PixelSpaceTestItem)
218 for world_space_item in self.__json['test_items']['world_space']:
219 print(world_space_item['id'], world_space_item['position'])
220 self.__set_test_item(world_space_item['position'], world_space_item, WorldSpaceTestItem)
221
222 def __set_test_item(self, pos, json, item_class):
223 test_item = item_class(
224 self.__context.world,
225 self.__context.plane_node,
226 self.__context.cb_inst,
227 self.__context.curr_bottom,
228 self.__context.repos,
229 json,
230 pos=(pos[0], 0, pos[-1]),
231 model_scale=.2)
232 self.__test_items += [test_item]
233
234 def __on_close(self):
235 self.__json['name'] = self.__name_entry.get()
236 self.__json['instructions'] = self.__instructions_entry.get()
237 if self.__compute_hash() == self.__json['version']:
238 self.destroy()
239 else:
240 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
241 text=_('You have unsaved changes. Really quit?'),
242 command=self.__actually_close)
243 self.__dialog['frameColor'] = (.4, .4, .4, .14)
244 self.__dialog['relief'] = FLAT
245 self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
246 self.__dialog.component('text0')['font'] = self._font
247 for b in self.__dialog.buttonList:
248 b['frameColor'] = (.4, .4, .4, .14)
249 b.component('text0')['fg'] = (.9, .9, .9, 1)
250 b.component('text0')['font'] = self._font
251 b.component('text1')['fg'] = (.9, .1, .1, 1)
252 b.component('text1')['font'] = self._font
253 b.component('text2')['fg'] = (.9, .9, .1, 1)
254 b.component('text2')['font'] = self._font
255
256 def __on_new_item(self, item):
257 info(f'new {item}')
258 item_json = {}
259 scale = 1
260 if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
261 scale = .2
262 _item = self.__new_items[item](
263 self.__context.world,
264 self.__context.plane_node,
265 self.__context.cb_inst,
266 self.__context.curr_bottom,
267 self.__context.repos,
268 item_json,
269 model_scale=scale)
270 _item._Item__editing = True
271 self.__add_item(_item)
272 item_json['class'] = _item.__class__.__name__
273 item_json['position'] = list(_item._np.get_pos())
274 if item == 'PixelSpaceTestItem':
275 del item_json['class']
276 self.__json['test_items']['pixel_space'] += [item_json]
277 elif item == 'WorldSpaceTestItem':
278 del item_json['class']
279 self.__json['test_items']['world_space'] += [item_json]
280 else:
281 self.__json['items'] += [item_json]
282
283 def __actually_close(self, arg):
284 if arg:
285 self.destroy()
286 messenger.send('editor-stop')
287 self.__dialog.cleanup()
288
289 def __on_save(self):
290 self.__json['name'] = self.__name_entry.get()
291 self.__json['instructions'] = self.__instructions_entry.get()
292 self.__json['version'] = self.__compute_hash()
293 json_name = self.__filenamename_entry.get()
294 with open('assets/scenes/%s.json' % json_name, 'w') as f:
295 f.write(dumps(self.__json, indent=2, sort_keys=True))
296
297 def __on_scene_list(self):
298 SceneList()
299
300 def __load_img_btn(self, path, col):
301 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
302 img.set_transparency(True)
303 img.set_color(col)
304 img.detach_node()
305 return img
306
307 def __compute_hash(self):
308 new_dict = deepcopy(self.__json)
309 del new_dict['version']
310 new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
311 h = hashlib.new('sha256')
312 h.update(new_dict_str.encode())
313 return h.hexdigest()[:12]
314
315 def __on_item_click(self, item):
316 if self.__inspector and self.__inspector.item == item: return
317 if self.__inspector:
318 self.__inspector.destroy()
319 if item.__class__ == PixelSpaceTestItem:
320 self.__inspector = PixelSpaceInspector(item, self.__items)
321 elif item.__class__ == WorldSpaceTestItem:
322 self.__inspector = WorldSpaceInspector(item, self.__items)
323 else:
324 self.__inspector = Inspector(item, self.__items)
325
326 def __on_inspector_destroy(self):
327 self.__inspector = None
328
329 def __on_new_scene(self):
330 self.destroy()
331 messenger.send('editor-stop')
332 messenger.send('new_scene')
333
334 def __on_start_items(self):
335 self.__start_items = StartItems(self.__json['start_items'])
336
337 def __on_start_items_save(self, start_items):
338 self.__json['start_items'] = start_items
339 self.__on_save()
340
341 def __on_start_items_destroy(self):
342 self.__start_items = None
343
344 @property
345 def test_items(self):
346 return self.__test_items
347
348 def destroy(self):
349 self._frm.destroy()
350 if self.__inspector:
351 self.__inspector.destroy()
352 self.ignore('editor-item-click')
353 self.ignore('editor-inspector-destroy')
354 self.ignore('editor-start-items-save')
355 self.ignore('editor-start-items-destroy')
356 for t in self.__test_items: t.destroy()
357 self.__test_items = []