ya2 · news · projects · code · about

housekeeping: ya2 module
[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
FC
8import hashlib
9from panda3d.core import Texture, TextNode
10from direct.gui.OnscreenImage import OnscreenImage
2aeb9f68
FC
11from direct.gui.DirectGui import DirectButton, DirectFrame, DirectEntry, \
12 YesNoDialog, DirectOptionMenu
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
bf77b5d5 21from ya2.utils.gfx import Point
d3a2e50a
FC
22
23
ee65fee0 24class SceneEditor(DirectObject):
d3a2e50a 25
3133e30f 26 def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node):
ee65fee0 27 super().__init__()
2aeb9f68 28 self.__items = items
d3a2e50a 29 self.__json = json
3133e30f
FC
30 self.__world = world
31 self.__mouse_plane_node = mouse_plane_node
3e3c4caf
FC
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': []}}
ee65fee0 42 self.__inspector = None
2aeb9f68
FC
43 self.__context = context
44 self.__add_item = add_item
d3a2e50a
FC
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')}
3e3c4caf 61 w, h, tw, l = 1.8, 1, 30, .36
d3a2e50a
FC
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(
3e3c4caf 67 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
d3a2e50a
FC
68 font=self._common['text_font'],
69 scale=self._common['scale'],
70 fg=self._common['text_fg'],
71 align=TextNode.A_right)
3e3c4caf 72 self.__filenamename_entry = DirectEntry(
d3a2e50a
FC
73 scale=self._common['scale'],
74 pos=(l, 1, h - .1),
75 entryFont=self._font,
76 width=tw,
77 frameColor=self._common['frameColor'],
3e3c4caf
FC
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'],
d3a2e50a
FC
93 initialText=json['name'],
94 parent=self._frm,
95 text_fg=self._common['text_fg'])
96 OnscreenText(
3e3c4caf 97 _('Description'), pos=(l - .03, h - .3), parent=self._frm,
d3a2e50a
FC
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'],
3e3c4caf 109 pos=(l, 1, h - .3),
d3a2e50a
FC
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)
012cf969 134 DirectButton(
d3a2e50a
FC
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'))
012cf969 141 DirectButton(
d3a2e50a
FC
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'))
012cf969
FC
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'))
2aeb9f68
FC
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'))
3e3c4caf 183 DirectButton(
9981d472 184 image=load_images_btn('start_items', 'gray'), scale=.05,
3e3c4caf 185 pos=(.06, 1, .58),
9981d472
FC
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),
3e3c4caf
FC
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'))
3133e30f
FC
197 self.__test_items = []
198 self.__set_test_items()
ee65fee0 199 messenger.send('editor-start')
2aeb9f68 200 self.accept('editor-item-click', self.__on_item_click)
ee65fee0 201 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
9981d472
FC
202 self.accept('editor-start-items-save', self.__on_start_items_save)
203 self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
d3a2e50a 204
3133e30f
FC
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)
bf77b5d5 212 p_from, p_to = Point(pos_win).from_to_points()
3133e30f
FC
213 for hit in self.__world.ray_test_all(p_from, p_to).get_hits():
214 if hit.get_node() == self.__mouse_plane_node:
215 pos = hit.get_hit_pos()
216 self.__set_test_item(pos, pixel_space_item, PixelSpaceTestItem)
217 for world_space_item in self.__json['test_items']['world_space']:
218 print(world_space_item['id'], world_space_item['position'])
219 self.__set_test_item(world_space_item['position'], world_space_item, WorldSpaceTestItem)
220
221 def __set_test_item(self, pos, json, item_class):
222 test_item = item_class(
223 self.__context.world,
224 self.__context.plane_node,
225 self.__context.cb_inst,
226 self.__context.curr_bottom,
227 self.__context.repos,
228 json,
229 pos=(pos[0], 0, pos[-1]),
230 model_scale=.2)
231 self.__test_items += [test_item]
232
d3a2e50a
FC
233 def __on_close(self):
234 self.__json['name'] = self.__name_entry.get()
235 self.__json['instructions'] = self.__instructions_entry.get()
236 if self.__compute_hash() == self.__json['version']:
3133e30f 237 self.destroy()
d3a2e50a
FC
238 else:
239 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
240 text=_('You have unsaved changes. Really quit?'),
241 command=self.__actually_close)
012cf969
FC
242 self.__dialog['frameColor'] = (.4, .4, .4, .14)
243 self.__dialog['relief'] = FLAT
244 self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
245 self.__dialog.component('text0')['font'] = self._font
246 for b in self.__dialog.buttonList:
247 b['frameColor'] = (.4, .4, .4, .14)
248 b.component('text0')['fg'] = (.9, .9, .9, 1)
249 b.component('text0')['font'] = self._font
250 b.component('text1')['fg'] = (.9, .1, .1, 1)
251 b.component('text1')['font'] = self._font
252 b.component('text2')['fg'] = (.9, .9, .1, 1)
253 b.component('text2')['font'] = self._font
d3a2e50a 254
2aeb9f68
FC
255 def __on_new_item(self, item):
256 info(f'new {item}')
257 item_json = {}
3133e30f
FC
258 scale = 1
259 if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
260 scale = .2
2aeb9f68
FC
261 _item = self.__new_items[item](
262 self.__context.world,
263 self.__context.plane_node,
264 self.__context.cb_inst,
265 self.__context.curr_bottom,
266 self.__context.repos,
3133e30f
FC
267 item_json,
268 model_scale=scale)
2aeb9f68
FC
269 _item._Item__editing = True
270 self.__add_item(_item)
271 item_json['class'] = _item.__class__.__name__
272 item_json['position'] = list(_item._np.get_pos())
3133e30f
FC
273 if item == 'PixelSpaceTestItem':
274 del item_json['class']
275 self.__json['test_items']['pixel_space'] += [item_json]
276 elif item == 'WorldSpaceTestItem':
277 del item_json['class']
278 self.__json['test_items']['world_space'] += [item_json]
279 else:
280 self.__json['items'] += [item_json]
2aeb9f68 281
d3a2e50a
FC
282 def __actually_close(self, arg):
283 if arg:
3e3c4caf 284 self.destroy()
ee65fee0 285 messenger.send('editor-stop')
d3a2e50a
FC
286 self.__dialog.cleanup()
287
288 def __on_save(self):
289 self.__json['name'] = self.__name_entry.get()
290 self.__json['instructions'] = self.__instructions_entry.get()
291 self.__json['version'] = self.__compute_hash()
3e3c4caf
FC
292 json_name = self.__filenamename_entry.get()
293 with open('assets/scenes/%s.json' % json_name, 'w') as f:
d3a2e50a
FC
294 f.write(dumps(self.__json, indent=2, sort_keys=True))
295
012cf969
FC
296 def __on_scene_list(self):
297 SceneList()
298
d3a2e50a
FC
299 def __load_img_btn(self, path, col):
300 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
301 img.set_transparency(True)
302 img.set_color(col)
303 img.detach_node()
304 return img
305
306 def __compute_hash(self):
307 new_dict = deepcopy(self.__json)
308 del new_dict['version']
309 new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
310 h = hashlib.new('sha256')
311 h.update(new_dict_str.encode())
312 return h.hexdigest()[:12]
ee65fee0 313
2aeb9f68 314 def __on_item_click(self, item):
ee65fee0
FC
315 if self.__inspector and self.__inspector.item == item: return
316 if self.__inspector:
317 self.__inspector.destroy()
3133e30f
FC
318 if item.__class__ == PixelSpaceTestItem:
319 self.__inspector = PixelSpaceInspector(item, self.__items)
320 elif item.__class__ == WorldSpaceTestItem:
321 self.__inspector = WorldSpaceInspector(item, self.__items)
322 else:
323 self.__inspector = Inspector(item, self.__items)
ee65fee0
FC
324
325 def __on_inspector_destroy(self):
326 self.__inspector = None
3e3c4caf
FC
327
328 def __on_new_scene(self):
329 self.destroy()
330 messenger.send('editor-stop')
331 messenger.send('new_scene')
332
9981d472
FC
333 def __on_start_items(self):
334 self.__start_items = StartItems(self.__json['start_items'])
335
336 def __on_start_items_save(self, start_items):
337 self.__json['start_items'] = start_items
338 self.__on_save()
339
340 def __on_start_items_destroy(self):
341 self.__start_items = None
342
3133e30f
FC
343 @property
344 def test_items(self):
345 return self.__test_items
346
3e3c4caf
FC
347 def destroy(self):
348 self._frm.destroy()
349 if self.__inspector:
350 self.__inspector.destroy()
351 self.ignore('editor-item-click')
352 self.ignore('editor-inspector-destroy')
9981d472
FC
353 self.ignore('editor-start-items-save')
354 self.ignore('editor-start-items-destroy')
3133e30f
FC
355 for t in self.__test_items: t.destroy()
356 self.__test_items = []