ya2 · news · projects · code · about

inspector
[pmachines.git] / pmachines / items / item.py
CommitLineData
c991401b 1from panda3d.core import CullFaceAttrib, Point3, Texture, \
d5932612 2 Plane, Vec3, BitMask32
c991401b 3from panda3d.bullet import BulletRigidBodyNode, BulletGhostNode
d5932612 4from direct.gui.OnscreenText import OnscreenText
ee65fee0 5from direct.showbase.DirectObject import DirectObject
b35b1f62 6from ya2.p3d.gfx import P3dGfxMgr, set_srgb
d5932612
FC
7
8
9class Command:
10
11 def __init__(self, pos, rot):
12 self.pos = pos
13 self.rot = rot
14
15
0e86689f
FC
16class FixedStrategy:
17
0a0994e4 18 def win_condition(self):
0e86689f
FC
19 return True
20
21
22class StillStrategy:
23
24 def __init__(self, np):
25 self._np = np
26 self._positions = []
27 self._rotations = []
28
0a0994e4 29 def win_condition(self):
0e86689f
FC
30 self._positions += [self._np.get_pos()]
31 self._rotations += [self._np.get_hpr()]
32cd89ca 32 if len(self._positions) > 30:
0e86689f 33 self._positions.pop(0)
32cd89ca 34 if len(self._rotations) > 30:
0e86689f 35 self._rotations.pop(0)
32cd89ca 36 if len(self._positions) < 28:
0e86689f
FC
37 return
38 avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
39 avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
40 avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
41 avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
42 avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
43 avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
44 avg_pos = Point3(avg_x, avg_y, avg_z)
45 avg_rot = Point3(avg_h, avg_p, avg_r)
46 return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
47 all((rot - avg_rot).length() < 1 for rot in self._rotations)
48
49
ee65fee0 50class Item(DirectObject):
d5932612 51
98741d67 52 def __init__(self, world, plane_node, cb_inst, curr_bottom, scene_repos, model_path, json, model_scale=1, exp_num_contacts=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5):
ee65fee0 53 super().__init__()
d5932612
FC
54 self._world = world
55 self._plane_node = plane_node
56 self._count = count
57 self._cb_inst = cb_inst
58 self._paused = False
59 self._overlapping = False
60 self._first_command = True
61 self.repos_done = False
ef3c36bf
FC
62 self._mass = mass
63 self._pos = pos
64 self._r = r
98741d67 65 self.json = json
0e86689f 66 self.strategy = FixedStrategy()
ea38777c 67 self._exp_num_contacts = exp_num_contacts
d5932612 68 self._curr_bottom = curr_bottom
e1438449 69 self._scene_repos = scene_repos
8254721d 70 self._model_scale = model_scale
d5932612
FC
71 self._model_path = model_path
72 self._commands = []
73 self._command_idx = -1
e67dbe53 74 self._restitution = restitution
32cd89ca 75 self._friction = friction
0a0994e4
FC
76 self._positions = []
77 self._rotations = []
ef3c36bf
FC
78 if count:
79 self.node = BulletGhostNode(self.__class__.__name__)
80 else:
81 self.node = BulletRigidBodyNode(self.__class__.__name__)
80d579b1 82 self._set_shape(count)
d5932612 83 self._np = render.attach_new_node(self.node)
ef3c36bf
FC
84 if count:
85 world.attach_ghost(self.node)
86 else:
87 world.attach_rigid_body(self.node)
d5932612
FC
88 self._model = loader.load_model(model_path)
89 set_srgb(self._model)
90 self._model.flatten_light()
91 self._model.reparent_to(self._np)
8254721d 92 self._np.set_scale(model_scale)
80d579b1 93 self._np.flatten_strong()
ee65fee0
FC
94 self._set_outline_model()
95 set_srgb(self._outline_model)
96 self._model.hide(BitMask32(0x01))
97 self._outline_model.hide(BitMask32(0x01))
d5932612
FC
98 self._start_drag_pos = None
99 self._prev_rot_info = None
100 self._last_nonoverlapping_pos = None
101 self._last_nonoverlapping_rot = None
ef3c36bf 102 self._instantiated = not count
d5932612 103 self._box_tsk = taskMgr.add(self.on_frame, 'on_frame')
ef3c36bf
FC
104 if count:
105 self._repos()
106 else:
107 self._np.set_pos(pos)
108 self._np.set_r(r)
ee65fee0
FC
109 self.__editing = False
110 self.accept('editor-start', self.__editor_start)
111 self.accept('editor-stop', self.__editor_stop)
d5932612 112
80d579b1 113 def _set_shape(self, apply_scale=True):
ea38777c 114 pass
d5932612 115
ee65fee0
FC
116 def __editor_start(self):
117 self.__editing = True
118
119 def __editor_stop(self):
120 self.__editing = False
121
122 @property
123 def interactable(self):
124 return self.__editing or self.count
125
0e86689f
FC
126 def set_strategy(self, strategy):
127 self.strategy = strategy
128
d5932612
FC
129 def _repos(self):
130 p_from, p_to = P3dGfxMgr.world_from_to((-1, 1))
131 for hit in self._world.ray_test_all(p_from, p_to).get_hits():
132 if hit.get_node() == self._plane_node:
133 pos = hit.get_hit_pos()
c991401b 134 #corner = P3dGfxMgr.screen_coord(pos)
d5932612
FC
135 bounds = self._np.get_tight_bounds()
136 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
137 self._np.set_pos(pos)
138 plane = Plane(Vec3(0, 1, 0), bounds[0][1])
139 pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
140 margin, ar = .04, base.get_aspect_ratio()
141 margin_x = margin / ar if ar >= 1 else margin
142 margin_z = margin * ar if ar < 1 else margin
143 top = self._curr_bottom()
144 base.camLens.extrude((-1 + margin_x, top - margin_z), near_pt, far_pt)
145 plane.intersects_line(
146 pos3d, render.get_relative_point(base.camera, near_pt),
147 render.get_relative_point(base.camera, far_pt))
148 delta = Vec3(bounds[1][0], bounds[1][1], bounds[0][2])
149 self._np.set_pos(pos3d + delta)
150 if not hasattr(self, '_txt'):
151 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
152 font.clear()
153 font.set_pixels_per_unit(60)
154 font.set_minfilter(Texture.FTLinearMipmapLinear)
155 font.set_outline((0, 0, 0, 1), .8, .2)
156 self._txt = OnscreenText(
157 str(self._count), font=font, scale=0.06, fg=(.9, .9, .9, 1))
158 pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
159 p2d = P3dGfxMgr.screen_coord(pos)
160 self._txt['pos'] = p2d
161 self.repos_done = True
162
e1438449
FC
163 def repos_x(self, x):
164 self._np.set_x(x)
165 bounds = self._np.get_tight_bounds()
166 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
167 pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
168 p2d = P3dGfxMgr.screen_coord(pos)
169 self._txt['pos'] = p2d
170
d5932612
FC
171 def get_bottom(self):
172 bounds = self._np.get_tight_bounds()
173 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
174 pos = self._np.get_pos() + (bounds[1][0], bounds[1][1], bounds[0][2])
175 p2d = P3dGfxMgr.screen_coord(pos)
176 ar = base.get_aspect_ratio()
177 return p2d[1] if ar >= 1 else (p2d[1] * ar)
178
179 def get_corner(self):
180 bounds = self._np.get_tight_bounds()
181 return bounds[1][0], bounds[1][1], bounds[0][2]
182
183 def _set_outline_model(self):
184 self._outline_model = loader.load_model(self._model_path)
ea38777c
FC
185 #clockw = CullFaceAttrib.MCullClockwise
186 #self._outline_model.set_attrib(CullFaceAttrib.make(clockw))
187 self._outline_model.set_attrib(CullFaceAttrib.make_reverse())
d5932612 188 self._outline_model.reparent_to(self._np)
ea38777c 189 self._outline_model.set_scale(1.08)
d5932612
FC
190 self._outline_model.set_light_off()
191 self._outline_model.set_color(.4, .4, .4, 1)
192 self._outline_model.set_color_scale(.4, .4, .4, 1)
193 self._outline_model.hide()
194
195 def on_frame(self, task):
196 self._np.set_y(0)
197 return task.cont
198
199 def undo(self):
200 self._command_idx -= 1
201 self._np.set_pos(self._commands[self._command_idx].pos)
202 self._np.set_hpr(self._commands[self._command_idx].rot)
203
204 def redo(self):
205 self._command_idx += 1
206 self._np.set_pos(self._commands[self._command_idx].pos)
207 self._np.set_hpr(self._commands[self._command_idx].rot)
208
209 def play(self):
210 if not self._instantiated:
211 return
212 self._world.remove_rigid_body(self.node)
ef3c36bf 213 self.node.set_mass(self._mass)
d5932612 214 self._world.attach_rigid_body(self.node)
e67dbe53 215 self.node.set_restitution(self._restitution)
32cd89ca 216 self.node.set_friction(self._friction)
d5932612
FC
217
218 def on_click_l(self, pos):
219 if self._paused: return
ee65fee0
FC
220 if self.__editing:
221 messenger.send('editor-item-click-l', [self])
d5932612
FC
222 self._start_drag_pos = pos, self._np.get_pos()
223 loader.load_sfx('assets/audio/sfx/grab.ogg').play()
224 if not self._instantiated:
225 self._world.remove_ghost(self.node)
226 pos = self._np.get_pos()
227 self._np.remove_node()
228 self.node = BulletRigidBodyNode('box')
ea38777c 229 self._set_shape()
d5932612
FC
230 self._np = render.attach_new_node(self.node)
231 self._world.attach_rigid_body(self.node)
232 self._model.reparent_to(self._np)
233 self._np.set_pos(pos)
234 self._set_outline_model()
8254721d 235 self._np.set_scale(self._model_scale)
d5932612
FC
236 self._model.show(BitMask32(0x01))
237 self._outline_model.hide(BitMask32(0x01))
238 self._instantiated = True
239 self._txt.destroy()
240 self._count -= 1
241 if self._count:
98741d67 242 item = self.__class__(self._world, self._plane_node, self._cb_inst, self._curr_bottom, self._scene_repos, None, count=self._count, mass=self._mass, pos=self._pos, r=self._r) # pylint: disable=no-value-for-parameter
d5932612 243 self._cb_inst(item)
e1438449 244 self._scene_repos()
d5932612
FC
245
246 def on_click_r(self, pos):
a6843832 247 if self._paused or not self._instantiated: return
d5932612
FC
248 self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
249 loader.load_sfx('assets/audio/sfx/grab.ogg').play()
250
251 def on_release(self):
252 if self._start_drag_pos or self._prev_rot_info:
253 loader.load_sfx('assets/audio/sfx/release.ogg').play()
254 self._command_idx += 1
255 self._commands = self._commands[:self._command_idx]
256 self._commands += [Command(self._np.get_pos(), self._np.get_hpr())]
257 self._first_command = False
258 self._start_drag_pos = self._prev_rot_info = None
259 if self._overlapping:
260 self._np.set_pos(self._last_nonoverlapping_pos)
261 self._np.set_hpr(self._last_nonoverlapping_rot)
a6843832
FC
262 self._outline_model.set_color(.4, .4, .4, 1)
263 self._outline_model.set_color_scale(.4, .4, .4, 1)
93cf86b2 264 self._overlapping = False
d5932612
FC
265
266 def on_mouse_on(self):
ef3c36bf 267 if not self._paused and self.interactable:
d5932612
FC
268 self._outline_model.show()
269
270 def on_mouse_off(self):
271 if self._start_drag_pos or self._prev_rot_info: return
ef3c36bf
FC
272 if self.interactable:
273 self._outline_model.hide()
d5932612
FC
274
275 def on_mouse_move(self, pos):
276 if self._start_drag_pos:
277 d_pos = pos - self._start_drag_pos[0]
278 self._np.set_pos(self._start_drag_pos[1] + d_pos)
279 if self._prev_rot_info:
280 start_vec = self._prev_rot_info[0] - self._prev_rot_info[1]
281 curr_vec = pos - self._prev_rot_info[1]
282 d_angle = curr_vec.signed_angle_deg(start_vec, (0, -1, 0))
283 self._np.set_r(self._prev_rot_info[2] + d_angle)
284 self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
285 if self._start_drag_pos or self._prev_rot_info:
286 res = self._world.contact_test(self.node)
287 nres = res.get_num_contacts()
ea38777c 288 if nres <= self._exp_num_contacts:
d5932612
FC
289 self._overlapping = False
290 self._outline_model.set_color(.4, .4, .4, 1)
291 self._outline_model.set_color_scale(.4, .4, .4, 1)
ea38777c 292 if nres > self._exp_num_contacts and not self._overlapping:
d5932612
FC
293 actual_nres = 0
294 for contact in res.get_contacts():
295 for node in [contact.get_node0(), contact.get_node1()]:
296 if isinstance(node, BulletRigidBodyNode) and \
297 node != self.node:
298 actual_nres += 1
299 if actual_nres >= 1:
300 self._overlapping = True
301 loader.load_sfx('assets/audio/sfx/overlap.ogg').play()
302 self._outline_model.set_color(.9, .1, .1, 1)
303 self._outline_model.set_color_scale(.9, .1, .1, 1)
304 if not self._overlapping:
305 self._last_nonoverlapping_pos = self._np.get_pos()
306 self._last_nonoverlapping_rot = self._np.get_hpr()
ee65fee0 307 messenger.send('item-rototranslated', [self._np])
d5932612
FC
308
309 def on_aspect_ratio_changed(self):
310 if not self._instantiated:
311 self._repos()
312
313 def store_state(self):
314 self._paused = True
315 self._model.set_transparency(True)
316 self._model.set_alpha_scale(.3)
ef3c36bf 317 if hasattr(self, '_txt') and not self._txt.is_empty():
d5932612
FC
318 self._txt.set_alpha_scale(.3)
319
320 def restore_state(self):
321 self._paused = False
322 self._model.set_alpha_scale(1)
ef3c36bf 323 if hasattr(self, '_txt') and not self._txt.is_empty():
d5932612
FC
324 self._txt.set_alpha_scale(1)
325
0a0994e4 326 def fail_condition(self):
32cd89ca 327 if self._np.get_z() < -6:
0a0994e4
FC
328 return True
329 self._positions += [self._np.get_pos()]
330 self._rotations += [self._np.get_hpr()]
32cd89ca 331 if len(self._positions) > 30:
0a0994e4 332 self._positions.pop(0)
32cd89ca 333 if len(self._rotations) > 30:
0a0994e4 334 self._rotations.pop(0)
32cd89ca 335 if len(self._positions) < 28:
0a0994e4
FC
336 return
337 avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
338 avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
339 avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
340 avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
341 avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
342 avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
343 avg_pos = Point3(avg_x, avg_y, avg_z)
344 avg_rot = Point3(avg_h, avg_p, avg_r)
345 return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
346 all((rot - avg_rot).length() < 1 for rot in self._rotations)
347
ee65fee0
FC
348 @property
349 def mass(self):
350 return self._mass
351
352 @mass.setter
353 def mass(self, val):
354 self._mass = val
355 self.json['mass'] = val
356
357 @property
358 def restitution(self):
359 return self._restitution
360
361 @restitution.setter
362 def restitution(self, val):
363 self._restitution = val
364 self.json['restitution'] = val
365
366 @property
367 def friction(self):
368 return self._friction
369
370 @friction.setter
371 def friction(self, val):
372 self._friction = val
373 self.json['friction'] = val
374
375 @property
376 def position(self):
377 return self._
378
379 @position.setter
380 def position(self, val):
381 self._np.set_pos(*val)
382 self.json['position'] = val
383
384 @property
385 def roll(self):
386 return self._np.get_r()
387
388 @roll.setter
389 def roll(self, val):
390 self._np.set_r(val)
391 self.json['roll'] = val
392
393 @property
394 def scale(self):
395 return self._np.get_scale()[0]
396
397 @scale.setter
398 def scale(self, val):
399 self._np.set_scale(val)
400 self.json['model_scale'] = val
401
d5932612
FC
402 def destroy(self):
403 self._np.remove_node()
404 taskMgr.remove(self._box_tsk)
ef3c36bf
FC
405 if hasattr(self, '_txt'):
406 self._txt.destroy()
d5932612
FC
407 if not self._instantiated:
408 self._world.remove_ghost(self.node)
409 else:
410 self._world.remove_rigid_body(self.node)