ya2 · news · projects · code · about

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