def setup_ray(node, traverser, bitmask, point_a=(0, 0, 1), point_b=(0, 0, 0)): ray = CollisionSegment(point_a, point_b) col = CollisionNode(node.getName() + "-ray") col.add_solid(ray) col.set_from_collide_mask(bitmask) col.set_into_collide_mask(CollideMask.all_off()) col_node = node.attach_new_node(col) handler = CollisionHandlerQueue() traverser.add_collider(col_node, handler) return {"collider": col, "ray": ray, "handler": handler, "node": col_node}
def enter_filter_mouseoverable(self, entity): model_proxy = self.proxies['model'] model_node = model_proxy.field(entity) mouseoverable = entity[MouseOverable] into_node = CollisionNode('wecs_mouseoverable') into_node.add_solid(mouseoverable.solid) into_node.set_from_collide_mask(0x0) into_node.set_into_collide_mask(mouseoverable.mask) into_node_path = model_node.attach_new_node(into_node) into_node_path.set_python_tag('wecs_mouseoverable', entity._uid)
def ray(self, name, bitmask, point_a=(0, 0, 1), point_b=(0, 0, 0)): shape = CollisionSegment(point_a, point_b) col = CollisionNode(self.node.getName() + "-ray-" + name) col.add_solid(shape) col.set_from_collide_mask(bitmask) col.set_into_collide_mask(CollideMask.all_off()) col_node = self.node.attach_new_node(col) handler = CollisionHandlerQueue() self.traverser.add_collider(col_node, handler) return { "collider": col, "shape": shape, "handler": handler, "node": col_node }
def sphere(self, name, bitmask, pos=(0, 0, 1), radius=0.2): col = CollisionNode(self.node.getName() + "-sphere-" + name) shape = CollisionSphere(pos, radius) col.add_solid(shape) col.set_from_collide_mask(bitmask) col.set_into_collide_mask(CollideMask.allOff()) col_node = self.node.attachNewNode(col) handler = CollisionHandlerPusher() handler.add_collider(col_node, self.node) self.traverser.add_collider(col_node, handler) return { "collider": col, "shape": shape, "handler": handler, "node": col_node }
def __init__(self, app, render, camera, mouseWatcher, pickKeyOn, pickKeyOff, collideMask, pickableTag="pickable"): self.render = render self.mouseWatcher = mouseWatcher.node() self.camera = camera self.camLens = camera.node().get_lens() self.collideMask = collideMask self.pickableTag = pickableTag self.taskMgr = app.task_mgr # setup event callback for picking body self.pickKeyOn = pickKeyOn self.pickKeyOff = pickKeyOff app.accept(self.pickKeyOn, self._pickBody, [self.pickKeyOn]) app.accept(self.pickKeyOff, self._pickBody, [self.pickKeyOff]) # collision data self.collideMask = collideMask self.cTrav = CollisionTraverser() self.collisionHandler = CollisionHandlerQueue() self.pickerRay = CollisionRay() pickerNode = CollisionNode("Utilities.pickerNode") node = NodePath("PhysicsNode") node.reparentTo(render) anp = node.attachNewNode(pickerNode) base.physicsMgr.attachPhysicalNode(pickerNode) pickerNode.add_solid(self.pickerRay) pickerNode.set_from_collide_mask(self.collideMask) pickerNode.set_into_collide_mask(BitMask32.all_off()) #pickerNode.node().getPhysicsObject().setMass(10) self.cTrav.add_collider(self.render.attach_new_node(pickerNode), self.collisionHandler) # service data self.pickedBody = None self.oldPickingDist = 0.0 self.deltaDist = 0.0 self.dragging = False self.updateTask = None
def __init__(self, world): self.world = world self.root = self.world.root.attach_new_node("player") self.root_target = self.world.root.attach_new_node("player_target") self.pivot = self.root.attach_new_node("player_pivot") base.camera.reparent_to(self.pivot) base.camera.set_z(1.7) base.cam.node().get_lens().set_fov(90) self.traverser = CollisionTraverser() self.ray = setup_ray( self.pivot, self.traverser, self.world.mask, # ray ends well below feet to register downward slopes (0, 0, 1), (0, 0, -1)) self.xyh_inertia = Vec3(0, 0, 0) h_acc = ConfigVariableDouble('mouse-accelleration', 0.1).get_value() self.xyh_acceleration = Vec3(0.8, 0.8, h_acc) self.friction = 0.15 self.torque = 0.5 self.last_up = Vec3(0, 0, 1) # Collider for portals csphere = CollisionSphere(0, 0, 1.25, 1.5) cnode = CollisionNode('player') cnode.add_solid(csphere) cnode.set_from_collide_mask(0x2) cnode.set_into_collide_mask(CollideMask.all_off()) self.collider = self.root.attach_new_node(cnode) self.event_handler = CollisionHandlerEvent() self.event_handler.add_in_pattern('into-%in') self.traverser.add_collider(self.collider, self.event_handler) self.collider.show() self.teleported = False base.input.set_mouse_relativity(True)
class Mouse(): def __init__(self): self.enabled = False self.locked = False self.position = Vec3(0, 0, 0) self.delta = Vec3(0, 0, 0) self.prev_x = 0 self.prev_y = 0 self.start_x = 0 self.start_y = 0 self.velocity = Vec3(0, 0, 0) self.prev_click_time = time.time() self.double_click_distance = .5 self.hovered_entity = None # returns the closest hovered entity with a collider. self.left = False self.right = False self.middle = False self.delta_drag = Vec3(0, 0, 0) self.update_step = 1 self.traverse_target = scene self._i = 0 self._mouse_watcher = None self._picker = CollisionTraverser() # Make a traverser self._pq = CollisionHandlerQueue() # Make a handler self._pickerNode = CollisionNode('mouseRay') self._pickerNP = camera.attach_new_node(self._pickerNode) self._pickerRay = CollisionRay() # Make our ray self._pickerNode.addSolid(self._pickerRay) self._picker.addCollider(self._pickerNP, self._pq) self._pickerNode.set_into_collide_mask(0) self.raycast = True self.collision = None self.collisions = list() self.enabled = True @property def x(self): if not self._mouse_watcher.has_mouse(): return 0 return self._mouse_watcher.getMouseX( ) / 2 * window.aspect_ratio # same space as ui stuff @x.setter def x(self, value): self.position = (value, self.y) @property def y(self): if not self._mouse_watcher.has_mouse(): return 0 return self._mouse_watcher.getMouseY() / 2 @y.setter def y(self, value): self.position = (self.x, value) @property def position(self): return Vec3(self.x, self.y, 0) @position.setter def position(self, value): base.win.move_pointer( 0, round(value[0] + (window.size[0] / 2) + (value[0] / 2 * window.size[0]) * 1.124), # no idea why I have * with 1.124 round(value[1] + (window.size[1] / 2) - (value[1] * window.size[1])), ) def __setattr__(self, name, value): if name == 'visible': window.set_cursor_hidden(not value) application.base.win.requestProperties(window) if name == 'locked': try: object.__setattr__(self, name, value) window.set_cursor_hidden(value) if value: window.set_mouse_mode(window.M_relative) else: window.set_mouse_mode(window.M_absolute) application.base.win.requestProperties(window) except: pass try: super().__setattr__(name, value) # return except: pass def input(self, key): if not self.enabled: return if key.endswith('mouse down'): self.start_x = self.x self.start_y = self.y elif key.endswith('mouse up'): self.delta_drag = Vec3(self.x - self.start_x, self.y - self.start_y, 0) if key == 'left mouse down': self.left = True if self.hovered_entity: if hasattr(self.hovered_entity, 'on_click'): self.hovered_entity.on_click() for s in self.hovered_entity.scripts: if hasattr(s, 'on_click'): s.on_click() # double click if time.time( ) - self.prev_click_time <= self.double_click_distance: base.input('double click') if self.hovered_entity: if hasattr(self.hovered_entity, 'on_double_click'): self.hovered_entity.on_double_click() for s in self.hovered_entity.scripts: if hasattr(s, 'on_double_click'): s.on_double_click() self.prev_click_time = time.time() if key == 'left mouse up': self.left = False if key == 'right mouse down': self.right = True if key == 'right mouse up': self.right = False if key == 'middle mouse down': self.middle = True if key == 'middle mouse up': self.middle = False def update(self): if not self.enabled or not self._mouse_watcher.has_mouse(): self.velocity = Vec3(0, 0, 0) return self.moving = self.x + self.y != self.prev_x + self.prev_y if self.moving: if self.locked: self.velocity = self.position self.position = (0, 0) else: self.velocity = Vec3(self.x - self.prev_x, (self.y - self.prev_y) / window.aspect_ratio, 0) else: self.velocity = Vec3(0, 0, 0) if self.left or self.right or self.middle: self.delta = Vec3(self.x - self.start_x, self.y - self.start_y, 0) self.prev_x = self.x self.prev_y = self.y self._i += 1 if self._i < self.update_step: return # collide with ui self._pickerNP.reparent_to(scene.ui_camera) self._pickerRay.set_from_lens(camera._ui_lens_node, self.x * 2 / window.aspect_ratio, self.y * 2) self._picker.traverse(camera.ui) if self._pq.get_num_entries() > 0: # print('collided with ui', self._pq.getNumEntries()) self.find_collision() return # collide with world self._pickerNP.reparent_to(camera) self._pickerRay.set_from_lens(scene.camera.lens_node, self.x * 2 / window.aspect_ratio, self.y * 2) try: self._picker.traverse(self.traverse_target) except: # print('error: mouse._picker could not traverse', self.traverse_target) return if self._pq.get_num_entries() > 0: self.find_collision() else: # print('mouse miss', base.render) # unhover all if it didn't hit anything for entity in scene.entities: if hasattr(entity, 'hovered') and entity.hovered: entity.hovered = False self.hovered_entity = None if hasattr(entity, 'on_mouse_exit'): entity.on_mouse_exit() for s in entity.scripts: if hasattr(s, 'on_mouse_exit'): s.on_mouse_exit() @property def normal(self): if not self.collision: return None return self.collision.normal @property def world_normal(self): if not self.collision: return None return self.collision.world_normal @property def point(self): # returns the point hit in local space if self.collision: return self.collision.point return None @property def world_point(self): if self.collision: return self.collision.world_point return None def find_collision(self): self.collisions = list() self.collision = None if not self.raycast or self._pq.get_num_entries() == 0: self.unhover_everything_not_hit() return False self._pq.sortEntries() for entry in self._pq.getEntries(): for entity in scene.entities: if entry.getIntoNodePath( ).parent == entity and entity.collision: if entity.collision: hit = HitInfo( hit=entry.collided(), entity=entity, distance=distance(entry.getSurfacePoint(scene), camera.getPos()), point=entry.getSurfacePoint(entity), world_point=entry.getSurfacePoint(scene), normal=entry.getSurfaceNormal(entity), world_normal=entry.getSurfaceNormal(scene), ) self.collisions.append(hit) break if self.collisions: self.collision = self.collisions[0] self.hovered_entity = self.collision.entity if not self.hovered_entity.hovered: self.hovered_entity.hovered = True if hasattr(self.hovered_entity, 'on_mouse_enter'): self.hovered_entity.on_mouse_enter() for s in self.hovered_entity.scripts: if hasattr(s, 'on_mouse_enter'): s.on_mouse_enter() self.unhover_everything_not_hit() def unhover_everything_not_hit(self): for e in scene.entities: if e == self.hovered_entity: continue if e.hovered: e.hovered = False if hasattr(e, 'on_mouse_exit'): e.on_mouse_exit() for s in e.scripts: if hasattr(s, 'on_mouse_exit'): s.on_mouse_exit()
class Raycaster(Entity): line_model = Mesh(vertices=[Vec3(0, 0, 0), Vec3(0, 0, 1)], mode='line') _boxcast_box = Entity(model='cube', origin_z=-.5, collider='box', color=color.white33, enabled=False) def __init__(self): super().__init__(name='raycaster', eternal=True) self._picker = CollisionTraverser() # Make a traverser self._pq = CollisionHandlerQueue() # Make a handler self._pickerNode = CollisionNode('raycaster') self._pickerNode.set_into_collide_mask(0) self._pickerNP = self.attach_new_node(self._pickerNode) self._picker.addCollider(self._pickerNP, self._pq) def distance(self, a, b): return sqrt(sum((a - b)**2 for a, b in zip(a, b))) def raycast(self, origin, direction=(0, 0, 1), distance=inf, traverse_target=scene, ignore=list(), debug=False): self.position = origin self.look_at(self.position + direction) self._pickerNode.clearSolids() ray = CollisionRay() ray.setOrigin(Vec3(0, 0, 0)) ray.setDirection(Vec3(0, 0, 1)) self._pickerNode.addSolid(ray) if debug: temp = Entity(position=origin, model=Raycaster.line_model, scale=Vec3(1, 1, min(distance, 9999)), add_to_scene_entities=False) temp.look_at(self.position + direction) destroy(temp, 1 / 30) self._picker.traverse(traverse_target) if self._pq.get_num_entries() == 0: self.hit = HitInfo(hit=False, distance=distance) return self.hit ignore += tuple([e for e in scene.entities if not e.collision]) self._pq.sort_entries() self.entries = [ # filter out ignored entities e for e in self._pq.getEntries() if e.get_into_node_path().parent not in ignore and self.distance( self.world_position, Vec3( *e.get_surface_point(render))) <= distance ] if len(self.entries) == 0: self.hit = HitInfo(hit=False, distance=distance) return self.hit self.collision = self.entries[0] nP = self.collision.get_into_node_path().parent point = Vec3(*self.collision.get_surface_point(nP)) world_point = Vec3(*self.collision.get_surface_point(render)) hit_dist = self.distance(self.world_position, world_point) self.hit = HitInfo(hit=True, distance=distance) for e in scene.entities: if e == nP: self.hit.entity = e nPs = [e.get_into_node_path().parent for e in self.entries] self.hit.entities = [e for e in scene.entities if e in nPs] self.hit.point = point self.hit.world_point = world_point self.hit.distance = hit_dist self.hit.normal = Vec3(*self.collision.get_surface_normal( self.collision.get_into_node_path().parent).normalized()) self.hit.world_normal = Vec3( *self.collision.get_surface_normal(render).normalized()) return self.hit self.hit = HitInfo(hit=False, distance=distance) return self.hit def boxcast(self, origin, direction=(0, 0, 1), distance=9999, thickness=(1, 1), traverse_target=scene, ignore=list(), debug=False): # similar to raycast, but with width and height if isinstance(thickness, (int, float, complex)): thickness = (thickness, thickness) Raycaster._boxcast_box.enabled = True Raycaster._boxcast_box.collision = True Raycaster._boxcast_box.position = origin Raycaster._boxcast_box.scale = Vec3(abs(thickness[0]), abs(thickness[1]), abs(distance)) Raycaster._boxcast_box.always_on_top = debug Raycaster._boxcast_box.visible = debug Raycaster._boxcast_box.look_at(origin + direction) hit_info = Raycaster._boxcast_box.intersects( traverse_target=traverse_target, ignore=ignore) if hit_info.world_point: hit_info.distance = ursinamath.distance(origin, hit_info.world_point) else: hit_info.distance = distance if debug: Raycaster._boxcast_box.collision = False Raycaster._boxcast_box.scale_z = hit_info.distance invoke(setattr, Raycaster._boxcast_box, 'enabled', False, delay=.2) else: Raycaster._boxcast_box.enabled = False return hit_info
class Entity(NodePath): rotation_directions = (-1,-1,1) default_shader = None def __init__(self, add_to_scene_entities=True, **kwargs): super().__init__(self.__class__.__name__) self.name = camel_to_snake(self.type) self.enabled = True # disabled entities wil not be visible nor run code self.visible = True self.ignore = False # if True, will not try to run code self.eternal = False # eternal entities does not get destroyed on scene.clear() self.ignore_paused = False self.ignore_input = False self.parent = scene self.add_to_scene_entities = add_to_scene_entities # set to False to be ignored by the engine, but still get rendered. if add_to_scene_entities: scene.entities.append(self) self.model = None # set model with model='model_name' (without file type extention) self.color = color.white self.texture = None # set model with texture='texture_name'. requires a model to be set beforehand. self.reflection_map = scene.reflection_map self.reflectivity = 0 self.render_queue = 0 self.double_sided = False self.shader = Entity.default_shader # self.always_on_top = False self.collision = False # toggle collision without changing collider. self.collider = None # set to 'box'/'sphere'/'mesh' for auto fitted collider. self.scripts = list() # add with add_script(class_instance). will assign an 'entity' variable to the script. self.animations = list() self.hovered = False # will return True if mouse hovers entity. self.origin = Vec3(0,0,0) self.position = Vec3(0,0,0) # right, up, forward. can also set self.x, self.y, self.z self.rotation = Vec3(0,0,0) # can also set self.rotation_x, self.rotation_y, self.rotation_z self.scale = Vec3(1,1,1) # can also set self.scale_x, self.scale_y, self.scale_z self.line_definition = None # returns a Traceback(filename, lineno, function, code_context, index). if application.trace_entity_definition and add_to_scene_entities: from inspect import getframeinfo, stack _stack = stack() caller = getframeinfo(_stack[1][0]) if len(_stack) > 2 and _stack[1].code_context and 'super().__init__()' in _stack[1].code_context[0]: caller = getframeinfo(_stack[2][0]) self.line_definition = caller if caller.code_context: self.code_context = caller.code_context[0] if (self.code_context.count('(') == self.code_context.count(')') and ' = ' in self.code_context and not 'name=' in self.code_context and not 'Ursina()' in self.code_context): self.name = self.code_context.split(' = ')[0].strip().replace('self.', '') # print('set name to:', self.code_context.split(' = ')[0].strip().replace('self.', '')) if application.print_entity_definition: print(f'{Path(caller.filename).name} -> {caller.lineno} -> {caller.code_context}') for key, value in kwargs.items(): setattr(self, key, value) def _list_to_vec(self, value): if isinstance(value, (int, float, complex)): return Vec3(value, value, value) if len(value) % 2 == 0: new_value = Vec2() for i in range(0, len(value), 2): new_value.add_x(value[i]) new_value.add_y(value[i+1]) if len(value) % 3 == 0: new_value = Vec3() for i in range(0, len(value), 3): new_value.add_x(value[i]) new_value.add_y(value[i+1]) new_value.add_z(value[i+2]) return new_value def enable(self): self.enabled = True def disable(self): self.enabled = False def __setattr__(self, name, value): if name == 'enabled': try: # try calling on_enable() on classes inheriting from Entity if value == True: self.on_enable() else: self.on_disable() except: pass if value == True: if hasattr(self, 'is_singleton') and not self.is_singleton(): self.unstash() else: if hasattr(self, 'is_singleton') and not self.is_singleton(): self.stash() if name == 'eternal': for c in self.children: c.eternal = value if name == 'world_parent': self.reparent_to(value) if name == 'model': if value is None: if hasattr(self, 'model') and self.model: self.model.removeNode() # print('removed model') object.__setattr__(self, name, value) return None if isinstance(value, NodePath): # pass procedural model if self.model is not None and value != self.model: self.model.removeNode() object.__setattr__(self, name, value) elif isinstance(value, str): # pass model asset name m = load_model(value, application.asset_folder) if not m: m = load_model(value, application.internal_models_compressed_folder) if m: if self.model is not None: self.model.removeNode() object.__setattr__(self, name, m) # if isinstance(m, Mesh): # m.recipe = value # print('loaded model successively') else: # if '.' in value: # print(f'''trying to load model with specific filename extention. please omit it. '{value}' -> '{value.split('.')[0]}' ''') print('missing model:', value) return if self.model: self.model.reparentTo(self) self.model.setTransparency(TransparencyAttrib.M_dual) self.color = self.color # reapply color after changing model self.texture = self.texture # reapply texture after changing model self._vert_cache = None if isinstance(value, Mesh): if hasattr(value, 'on_assign'): value.on_assign(assigned_to=self) return if name == 'color' and value is not None: if isinstance(value, str): value = color.hex(value) if not isinstance(value, Vec4): value = Vec4(value[0], value[1], value[2], value[3]) if self.model: self.model.setColorScaleOff() # prevent inheriting color from parent self.model.setColorScale(value) object.__setattr__(self, name, value) if name == 'collision' and hasattr(self, 'collider') and self.collider: if value: self.collider.node_path.unstash() else: self.collider.node_path.stash() object.__setattr__(self, name, value) return if name == 'render_queue': if self.model: self.model.setBin('fixed', value) if name == 'double_sided': self.setTwoSided(value) try: super().__setattr__(name, value) except: pass # print('failed to set attribiute:', name) @property def parent(self): try: return self._parent except: return None @parent.setter def parent(self, value): self._parent = value if value is None: destroy(self) else: try: self.reparentTo(value) except: print('invalid parent:', value) @property def type(self): # get class name. return self.__class__.__name__ @property def types(self): # get all class names including those this inhertits from. from inspect import getmro return [c.__name__ for c in getmro(self.__class__)] @property def visible(self): return self._visible @visible.setter def visible(self, value): self._visible = value if value: self.show() else: self.hide() @property def visible_self(self): # set visibility of self, without affecting children. if not hasattr(self, '_visible_self'): return True return self._visible_self @visible_self.setter def visible_self(self, value): self._visible_self = value if not self.model: return if value: self.model.show() else: self.model.hide() @property def collider(self): return self._collider @collider.setter def collider(self, value): # destroy existing collider if value and hasattr(self, 'collider') and self._collider: self._collider.remove() self._collider = value if value == 'box': if self.model: self._collider = BoxCollider(entity=self, center=-self.origin, size=self.model_bounds) else: self._collider = BoxCollider(entity=self) self._collider.name = value elif value == 'sphere': self._collider = SphereCollider(entity=self, center=-self.origin) self._collider.name = value elif value == 'mesh' and self.model: self._collider = MeshCollider(entity=self, mesh=None, center=-self.origin) self._collider.name = value elif isinstance(value, Mesh): self._collider = MeshCollider(entity=self, mesh=value, center=-self.origin) elif isinstance(value, str): m = load_model(value) if not m: return self._collider = MeshCollider(entity=self, mesh=m, center=-self.origin) self._collider.name = value self.collision = bool(self.collider) return @property def origin(self): return self._origin @origin.setter def origin(self, value): if not self.model: self._origin = Vec3(0,0,0) return if not isinstance(value, (Vec2, Vec3)): value = self._list_to_vec(value) if isinstance(value, Vec2): value = Vec3(*value, self.origin_z) self._origin = value self.model.setPos(-value[0], -value[1], -value[2]) @property def origin_x(self): return self.origin[0] @origin_x.setter def origin_x(self, value): self.origin = (value, self.origin_y, self.origin_z) @property def origin_y(self): return self.origin[1] @origin_y.setter def origin_y(self, value): self.origin = (self.origin_x, value, self.origin_z) @property def origin_z(self): return self.origin[2] @origin_z.setter def origin_z(self, value): self.origin = (self.origin_x, self.origin_y, value) @property def world_position(self): return Vec3(self.get_position(render)) @world_position.setter def world_position(self, value): if not isinstance(value, (Vec2, Vec3)): value = self._list_to_vec(value) if isinstance(value, Vec2): value = Vec3(*value, self.z) self.setPos(render, Vec3(value[0], value[1], value[2])) @property def world_x(self): return self.getX(render) @property def world_y(self): return self.getY(render) @property def world_z(self): return self.getZ(render) @world_x.setter def world_x(self, value): self.setX(render, value) @world_y.setter def world_y(self, value): self.setY(render, value) @world_z.setter def world_z(self, value): self.setZ(render, value) @property def position(self): return Vec3(*self.getPos()) @position.setter def position(self, value): if not isinstance(value, (Vec2, Vec3)): value = self._list_to_vec(value) if isinstance(value, Vec2): value = Vec3(*value, self.z) self.setPos(value[0], value[1], value[2]) @property def x(self): return self.getX() @x.setter def x(self, value): self.setX(value) @property def y(self): return self.getY() @y.setter def y(self, value): self.setY(value) @property def z(self): return self.getZ() @z.setter def z(self, value): self.setZ(value) @property def world_rotation(self): rotation = self.getHpr(base.render) return Vec3(rotation[1], rotation[0], rotation[2]) * Entity.rotation_directions @world_rotation.setter def world_rotation(self, value): rotation = self.setHpr(Vec3(value[1], value[0], value[2]) * Entity.rotation_directions, base.render) @property def world_rotation_x(self): return self.world_rotation[0] @world_rotation_x.setter def world_rotation_x(self, value): self.world_rotation = Vec3(value, self.world_rotation[1], self.world_rotation[2]) @property def world_rotation_y(self): return self.world_rotation[1] @world_rotation_y.setter def world_rotation_y(self, value): self.world_rotation = Vec3(self.world_rotation[0], value, self.world_rotation[2]) @property def world_rotation_z(self): return self.world_rotation[2] @world_rotation_z.setter def world_rotation_z(self, value): self.world_rotation = Vec3(self.world_rotation[0], self.world_rotation[1], value) @property def rotation(self): rotation = self.getHpr() return Vec3(rotation[1], rotation[0], rotation[2]) * Entity.rotation_directions @rotation.setter def rotation(self, value): if not isinstance(value, (Vec2, Vec3)): value = self._list_to_vec(value) if isinstance(value, Vec2): value = Vec3(*value, self.rotation_z) self.setHpr(Vec3(value[1], value[0], value[2]) * Entity.rotation_directions) @property def rotation_x(self): return self.rotation.x @rotation_x.setter def rotation_x(self, value): self.rotation = Vec3(value, self.rotation[1], self.rotation[2]) @property def rotation_y(self): return self.rotation.y @rotation_y.setter def rotation_y(self, value): self.rotation = Vec3(self.rotation[0], value, self.rotation[2]) @property def rotation_z(self): return self.rotation.z @rotation_z.setter def rotation_z(self, value): self.rotation = Vec3(self.rotation[0], self.rotation[1], value) @property def world_scale(self): return Vec3(*self.getScale(base.render)) @world_scale.setter def world_scale(self, value): if isinstance(value, (int, float, complex)): value = Vec3(value, value, value) self.setScale(base.render, value) @property def world_scale_x(self): return self.getScale(base.render)[0] @world_scale_x.setter def world_scale_x(self, value): self.setScale(base.render, Vec3(value, self.world_scale_y, self.world_scale_z)) @property def world_scale_y(self): return self.getScale(base.render)[1] @world_scale_y.setter def world_scale_y(self, value): self.setScale(base.render, Vec3(self.world_scale_x, value, self.world_scale_z)) @property def world_scale_z(self): return self.getScale(base.render)[2] @world_scale_z.setter def world_scale_z(self, value): self.setScale(base.render, Vec3(self.world_scale_x, value, self.world_scale_z)) @property def scale(self): scale = self.getScale() return Vec3(scale[0], scale[1], scale[2]) @scale.setter def scale(self, value): if not isinstance(value, (Vec2, Vec3)): value = self._list_to_vec(value) if isinstance(value, Vec2): value = Vec3(*value, self.scale_z) value = [e if e!=0 else .001 for e in value] self.setScale(value[0], value[1], value[2]) @property def scale_x(self): return self.scale[0] @scale_x.setter def scale_x(self, value): self.setScale(value, self.scale_y, self.scale_z) @property def scale_y(self): return self.scale[1] @scale_y.setter def scale_y(self, value): self.setScale(self.scale_x, value, self.scale_z) @property def scale_z(self): return self.scale[2] @scale_z.setter def scale_z(self, value): self.setScale(self.scale_x, self.scale_y, value) @property def forward(self): # get forward direction. return render.getRelativeVector(self, (0, 0, 1)) @property def back(self): # get backwards direction. return -self.forward @property def right(self): # get right direction. return render.getRelativeVector(self, (1, 0, 0)) @property def left(self): # get left direction. return -self.right @property def up(self): # get up direction. return render.getRelativeVector(self, (0, 1, 0)) @property def down(self): # get down direction. return -self.up @property def screen_position(self): # get screen position(ui space) from world space. from ursina import camera p3 = camera.getRelativePoint(self, Vec3.zero()) full = camera.lens.getProjectionMat().xform(Vec4(*p3, 1)) recip_full3 = 1 / full[3] p2 = Vec3(full[0], full[1], full[2]) * recip_full3 screen_pos = Vec3(p2[0]*camera.aspect_ratio/2, p2[1]/2, 0) return screen_pos @property def shader(self): return self._shader @shader.setter def shader(self, value): self._shader = value if value is None: self.setShaderAuto() return if isinstance(value, Panda3dShader): #panda3d shader self.setShader(value) return if isinstance(value, Shader): if not value.compiled: value.compile() self.setShader(value._shader) value.entity = self for key, value in value.default_input.items(): self.set_shader_input(key, value) def set_shader_input(self, name, value): if isinstance(value, Texture): value = value._texture # make sure to send the panda3d texture to the shader super().set_shader_input(name, value) @property def texture(self): if not hasattr(self, '_texture'): return None return self._texture @texture.setter def texture(self, value): if value is None and self._texture: # print('remove texture') self._texture = None self.setTextureOff(True) return if value.__class__ is Texture: texture = value elif isinstance(value, str): texture = load_texture(value) # print('loaded texture:', texture) if texture is None: print('no texture:', value) return if texture.__class__ is MovieTexture: self._texture = texture self.model.setTexture(texture, 1) return self._texture = texture if self.model: self.model.setTexture(texture._texture, 1) @property def texture_scale(self): if not hasattr(self, '_texture_scale'): return Vec2(1,1) return self._texture_scale @texture_scale.setter def texture_scale(self, value): self._texture_scale = value if self.model and self.texture: self.model.setTexScale(TextureStage.getDefault(), value[0], value[1]) @property def texture_offset(self): return self._texture_offset @texture_offset.setter def texture_offset(self, value): if self.model and self.texture: self.model.setTexOffset(TextureStage.getDefault(), value[0], value[1]) self.texture = self.texture self._texture_offset = value @property def alpha(self): return self.color[3] @alpha.setter def alpha(self, value): if value > 1: value = value / 255 self.color = color.color(self.color.h, self.color.s, self.color.v, value) @property def always_on_top(self): return self._always_on_top @always_on_top.setter def always_on_top(self, value): self._always_on_top = value self.set_bin("fixed", 0) self.set_depth_write(not value) self.set_depth_test(not value) @property def billboard(self): # set to True to make this Entity always face the camera. return self._billboard @billboard.setter def billboard(self, value): self._billboard = value if value: self.setBillboardPointEye(value) @property def reflection_map(self): return self._reflection_map @reflection_map.setter def reflection_map(self, value): if value.__class__ is Texture: texture = value elif isinstance(value, str): texture = load_texture(value) self._reflection_map = texture @property def reflectivity(self): return self._reflectivity @reflectivity.setter def reflectivity(self, value): self._reflectivity = value if value == 0: self.texture = None if value > 0: # if self.reflection_map == None: # self.reflection_map = scene.reflection_map # # if not self.reflection_map: # print('error setting reflectivity. no reflection map') # return if not self.normals: self.model.generate_normals() # ts = TextureStage('env') # ts.setMode(TextureStage.MAdd) # self.model.setTexGen(ts, TexGenAttrib.MEyeSphereMap) # print('---------------set reflectivity', self.reflection_map) # self.model.setTexture(ts, self.reflection_map) self.texture = self._reflection_map # print('set reflectivity') def generate_sphere_map(self, size=512, name=f'sphere_map_{len(scene.entities)}'): from ursina import camera _name = 'textures/' + name + '.jpg' org_pos = camera.position camera.position = self.position base.saveSphereMap(_name, size=size) camera.position = org_pos print('saved sphere map:', name) self.model.setTexGen(TextureStage.getDefault(), TexGenAttrib.MEyeSphereMap) self.reflection_map = name def generate_cube_map(self, size=512, name=f'cube_map_{len(scene.entities)}'): from ursina import camera _name = 'textures/' + name org_pos = camera.position camera.position = self.position base.saveCubeMap(_name+'.jpg', size=size) camera.position = org_pos print('saved cube map:', name + '.jpg') self.model.setTexGen(TextureStage.getDefault(), TexGenAttrib.MWorldCubeMap) self.reflection_map = _name + '#.jpg' self.model.setTexture(loader.loadCubeMap(_name + '#.jpg'), 1) @property def model_bounds(self): if self.model: bounds = self.model.getTightBounds() bounds = Vec3( Vec3(bounds[1][0], bounds[1][1], bounds[1][2]) # max point - Vec3(bounds[0][0], bounds[0][1], bounds[0][2]) # min point ) return bounds return (0,0,0) @property def bounds(self): return Vec3( self.model_bounds[0] * self.scale_x, self.model_bounds[1] * self.scale_y, self.model_bounds[2] * self.scale_z ) def reparent_to(self, entity): if entity is not None: self.wrtReparentTo(entity) self._parent = entity def get_position(self, relative_to=scene): return self.getPos(relative_to) def set_position(self, value, relative_to=scene): self.setPos(relative_to, Vec3(value[0], value[1], value[2])) def add_script(self, class_instance): if isinstance(class_instance, object) and type(class_instance) is not str: class_instance.entity = self class_instance.enabled = True setattr(self, camel_to_snake(class_instance.__class__.__name__), class_instance) self.scripts.append(class_instance) # print('added script:', camel_to_snake(name.__class__.__name__)) return class_instance def combine(self, analyze=False, auto_destroy=True, ignore=[]): from ursina.scripts.combine import combine self.model = combine(self, analyze, auto_destroy, ignore) return self.model def flip_faces(self): if not hasattr(self, '_vertex_order'): self._vertex_order = True self._vertex_order = not self._vertex_order if self._vertex_order: self.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullClockwise)) else: self.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullCounterClockwise)) def look_at(self, target, axis='forward'): from panda3d.core import Quat if not isinstance(target, Entity): target = Vec3(*target) self.lookAt(target) if axis == 'forward': return rotation_offset = { 'back' : Quat(0,0,1,0), 'down' : Quat(-.707,.707,0,0), 'up' : Quat(-.707,-.707,0,0), 'right' : Quat(-.707,0,.707,0), 'left' : Quat(-.707,0,-.707,0), }[axis] self.setQuat(rotation_offset * self.getQuat()) def look_at_2d(self, target, axis='z'): from math import degrees, atan2 if isinstance(target, Entity): target = Vec3(target.world_position) pos = target - self.world_position if axis == 'z': self.rotation_z = degrees(atan2(pos[0], pos[1])) def has_ancestor(self, possible_ancestor): p = self if isinstance(possible_ancestor, Entity): # print('ENTITY') for i in range(100): if p.parent: if p.parent == possible_ancestor: return True p = p.parent if isinstance(possible_ancestor, list) or isinstance(possible_ancestor, tuple): # print('LIST OR TUPLE') for e in possible_ancestor: for i in range(100): if p.parent: if p.parent == e: return True break p = p.parent elif isinstance(possible_ancestor, str): print('CLASS NAME', possible_ancestor) for i in range(100): if p.parent: if p.parent.__class__.__name__ == possible_ancestor: return True break p = p.parent return False @property def children(self): return [e for e in scene.entities if e.parent == self] @property def attributes(self): # attribute names. used by duplicate() for instance. return ('name', 'enabled', 'eternal', 'visible', 'parent', 'origin', 'position', 'rotation', 'scale', 'model', 'color', 'texture', 'texture_scale', 'texture_offset', # 'world_position', 'world_x', 'world_y', 'world_z', # 'world_rotation', 'world_rotation_x', 'world_rotation_y', 'world_rotation_z', # 'world_scale', 'world_scale_x', 'world_scale_y', 'world_scale_z', # 'x', 'y', 'z', # 'origin_x', 'origin_y', 'origin_z', # 'rotation_x', 'rotation_y', 'rotation_z', # 'scale_x', 'scale_y', 'scale_z', 'render_queue', 'always_on_top', 'collision', 'collider', 'scripts') #------------ # ANIMATIONS #------------ def animate(self, name, value, duration=.1, delay=0, curve=curve.in_expo, loop=False, resolution=None, interrupt='kill', time_step=None, auto_destroy=True): animator_name = name + '_animator' # print('start animating value:', name, animator_name ) if interrupt and hasattr(self, animator_name): getattr(getattr(self, animator_name), interrupt)() # call kill() or finish() depending on what the interrupt value is. # print('interrupt', interrupt, animator_name) sequence = Sequence(loop=loop, time_step=time_step, auto_destroy=auto_destroy) setattr(self, animator_name, sequence) self.animations.append(sequence) sequence.append(Wait(delay)) if not resolution: resolution = max(int(duration * 60), 1) for i in range(resolution+1): t = i / resolution t = curve(t) sequence.append(Wait(duration / resolution)) sequence.append(Func(setattr, self, name, lerp(getattr(self, name), value, t))) sequence.start() return sequence def animate_position(self, value, duration=.1, **kwargs): x = self.animate('x', value[0], duration, **kwargs) y = self.animate('y', value[1], duration, **kwargs) z = None if len(value) > 2: z = self.animate('z', value[2], duration, **kwargs) return x, y, z def animate_rotation(self, value, duration=.1, **kwargs): x = self.animate('rotation_x', value[0], duration, **kwargs) y = self.animate('rotation_y', value[1], duration, **kwargs) z = self.animate('rotation_z', value[2], duration, **kwargs) return x, y, z def animate_scale(self, value, duration=.1, **kwargs): if isinstance(value, (int, float, complex)): value = Vec3(value, value, value) return self.animate('scale', value, duration, **kwargs) # generate animation functions for e in ('x', 'y', 'z', 'rotation_x', 'rotation_y', 'rotation_z', 'scale_x', 'scale_y', 'scale_z'): exec(dedent(f''' def animate_{e}(self, value, duration=.1, delay=0, **kwargs): return self.animate('{e}', value, duration=duration, delay=delay, **kwargs) ''')) def shake(self, duration=.2, magnitude=1, speed=.05, direction=(1,1)): import random s = Sequence() original_position = self.position for i in range(int(duration / speed)): s.append(Func(self.set_position, Vec3( original_position[0] + (random.uniform(-.1, .1) * magnitude * direction[0]), original_position[1] + (random.uniform(-.1, .1) * magnitude * direction[1]), original_position[2], ))) s.append(Wait(speed)) s.append(Func(self.set_position, original_position)) s.start() return s def animate_color(self, value, duration=.1, interrupt='finish', **kwargs): return self.animate('color', value, duration, interrupt=interrupt, **kwargs) def fade_out(self, value=0, duration=.5, **kwargs): return self.animate('color', Vec4(self.color[0], self.color[1], self.color[2], value), duration, **kwargs) def fade_in(self, value=1, duration=.5, **kwargs): return self.animate('color', Vec4(self.color[0], self.color[1], self.color[2], value), duration, **kwargs) def blink(self, value=color.clear, duration=.1, delay=0, curve=curve.in_expo_boomerang, interrupt='finish', **kwargs): return self.animate_color(value, duration=duration, delay=delay, curve=curve, interrupt=interrupt, **kwargs) def intersects(self, traverse_target=scene, ignore=(), debug=False): from ursina.hit_info import HitInfo if not self.collision or not self.collider: self.hit = HitInfo(hit=False) return self.hit from ursina import distance if not hasattr(self, '_picker'): from panda3d.core import CollisionTraverser, CollisionNode, CollisionHandlerQueue from panda3d.core import CollisionRay, CollisionSegment, CollisionBox self._picker = CollisionTraverser() # Make a traverser self._pq = CollisionHandlerQueue() # Make a handler self._pickerNode = CollisionNode('raycaster') self._pickerNode.set_into_collide_mask(0) self._pickerNP = self.attach_new_node(self._pickerNode) self._picker.addCollider(self._pickerNP, self._pq) self._pickerNP.show() self._pickerNode.addSolid(self._collider.shape) if debug: self._pickerNP.show() else: self._pickerNP.hide() self._picker.traverse(traverse_target) if self._pq.get_num_entries() == 0: self.hit = HitInfo(hit=False) return self.hit ignore += (self, ) ignore += tuple([e for e in scene.entities if not e.collision]) self._pq.sort_entries() self.entries = [ # filter out ignored entities e for e in self._pq.getEntries() if e.get_into_node_path().parent not in ignore ] if len(self.entries) == 0: self.hit = HitInfo(hit=False, distance=0) return self.hit collision = self.entries[0] nP = collision.get_into_node_path().parent point = collision.get_surface_point(nP) point = Vec3(*point) world_point = collision.get_surface_point(render) world_point = Vec3(*world_point) hit_dist = distance(self.world_position, world_point) self.hit = HitInfo(hit=True) self.hit.entity = next(e for e in scene.entities if e == nP) self.hit.point = point self.hit.world_point = world_point self.hit.distance = hit_dist normal = collision.get_surface_normal(collision.get_into_node_path().parent).normalized() self.hit.normal = Vec3(*normal) normal = collision.get_surface_normal(render).normalized() self.hit.world_normal = Vec3(*normal) self.hit.entities = [] for collision in self.entries: self.hit.entities.append(next(e for e in scene.entities if e == collision.get_into_node_path().parent)) return self.hit
class Raycaster(Entity): def __init__(self): super().__init__(name='raycaster', eternal=True) self._picker = CollisionTraverser() # Make a traverser self._pq = CollisionHandlerQueue() # Make a handler self._pickerNode = CollisionNode('raycaster') self._pickerNode.set_into_collide_mask(0) self._pickerNP = self.attach_new_node(self._pickerNode) self._picker.addCollider(self._pickerNP, self._pq) self._pickerNP.show() def distance(self, a, b): return math.sqrt(sum((a - b)**2 for a, b in zip(a, b))) def raycast(self, origin, direction=(0, 0, 1), distance=math.inf, traverse_target=scene, ignore=list(), debug=False): self.position = origin self.look_at(self.position + direction) self._pickerNode.clearSolids() # if thickness == (0,0): if distance == math.inf: ray = CollisionRay() ray.setOrigin(Vec3(0, 0, 0)) ray.setDirection(Vec3(0, 1, 0)) else: ray = CollisionSegment(Vec3(0, 0, 0), Vec3(0, distance, 0)) self._pickerNode.addSolid(ray) if debug: self._pickerNP.show() else: self._pickerNP.hide() self._picker.traverse(traverse_target) if self._pq.get_num_entries() == 0: self.hit = Hit(hit=False) return self.hit ignore += tuple([e for e in scene.entities if not e.collision]) self._pq.sort_entries() self.entries = [ # filter out ignored entities e for e in self._pq.getEntries() if e.get_into_node_path().parent not in ignore ] if len(self.entries) == 0: self.hit = Hit(hit=False) return self.hit self.collision = self.entries[0] nP = self.collision.get_into_node_path().parent point = self.collision.get_surface_point(nP) point = Vec3(point[0], point[2], point[1]) world_point = self.collision.get_surface_point(render) world_point = Vec3(world_point[0], world_point[2], world_point[1]) hit_dist = self.distance(self.world_position, world_point) if nP.name.endswith('.egg'): nP = nP.parent self.hit = Hit(hit=True) for e in scene.entities: if e == nP: # print('cast nP to Entity') self.hit.entity = e self.hit.point = point self.hit.world_point = world_point self.hit.distance = hit_dist normal = self.collision.get_surface_normal( self.collision.get_into_node_path().parent) self.hit.normal = (normal[0], normal[2], normal[1]) normal = self.collision.get_surface_normal(render) self.hit.world_normal = (normal[0], normal[2], normal[1]) return self.hit self.hit = Hit(hit=False) return self.hit def boxcast(self, origin, direction=(0, 0, 1), distance=math.inf, thickness=(1, 1), traverse_target=scene, ignore=list(), debug=False): if isinstance(thickness, (int, float, complex)): thickness = (thickness, thickness) resolution = 3 rays = list() debugs = list() for y in range(3): for x in range(3): pos = origin + Vec3( lerp(-(thickness[0] / 2), thickness[0] / 2, x / (3 - 1)), lerp(-(thickness[1] / 2), thickness[1] / 2, y / (3 - 1)), 0) ray = self.raycast(pos, direction, distance, traverse_target, ignore, False) rays.append(ray) if debug and ray.hit: d = Entity(model='cube', origin_z=-.5, position=pos, scale=(.02, .02, distance), ignore=True) d.look_at(pos + Vec3(direction)) debugs.append(d) # print(pos, hit.point) if ray.hit and ray.distance > 0: d.scale_z = ray.distance d.color = color.green from ursina import destroy # [destroy(e, 1/60) for e in debugs] rays.sort(key=lambda x: x.distance) closest = rays[0] return Hit( hit=sum([int(e.hit) for e in rays]) > 0, entity=closest.entity, point=closest.point, world_point=closest.world_point, distance=closest.distance, normal=closest.normal, world_normal=closest.world_normal, hits=[e.hit for e in rays], entities=list(set([e.entity for e in rays])), # get unique entities hit )
class mode_head(): def __init__(self,base,Golog, folder_path = None, parent = None): # Set up basic attributes self.base = base self.golog = Golog self.bools = {'textboxes':True} self.buttons = dict() self.window_tasks = dict() self.bt = None self.mw = None self.listener = DirectObject() self.folder_path = folder_path #absolute path of golog folder '/path/to/golog/folder' if self.folder_path: self.file_path = os.path.abspath(self.folder_path + '/' + self.golog.label+ '.golog') autosave = True self.has_window = False self.parent = parent #for autosaving up to original golog self.reset = self.basic_reset self.garbage = [] #list of deleted math_data/graphics_data etc. #create a 2d rende self.render2d = NodePath('2d render') self.camera2D = self.render2d.attachNewNode(Camera('2d Camera')) self.camera2D.setDepthTest(False) self.camera2D.setDepthWrite(False) lens = OrthographicLens() lens.setFilmSize(2, 2) lens.setNearFar(-1000, 1000) self.camera2D.node().setLens(lens) # make a dictionary of mode_heads in the underlying golog if hasattr(self.golog,'mode_heads'): m = 0 while m in self.golog.mode_heads.keys(): m+=1 #get smallest unused mode_head index self.index = m self.label = self.golog.label+"_mode_head_"+str(self.index) self.golog.mode_heads[self.index] = self else: self.golog.mode_heads = dict() self.index = 0 self.label = self.golog.label+"_mode_head_"+ str(self.index) self.golog.mode_heads[self.index] = self ########## ### set up collision handling ### self.queue = CollisionHandlerQueue() ### set up selection tools self.create_list = [[],[]] #select nodes and add to create_list in order to create higher simplecies self.bools = {'selecter':False,'textboxes':True,'shift_clicked':False} #some bools that will be usefull self.dict = {'shift_pt':[None,None]} # set up mouse picker self.pickerNode = CollisionNode('mouseRay') self.pickerNP = self.golog.camera.attachNewNode(self.pickerNode) #attach collision node to camera self.pickerRay = CollisionRay() self.pickerNode.addSolid(self.pickerRay) self.pickerNode.set_into_collide_mask(0) #so that collision rays don't collide into each other if there are two mode_heads self.golog.cTrav.addCollider(self.pickerNP,self.queue) #send collisions to self.queue # set up plane for picking self.planeNode = self.golog.render.attachNewNode("plane") self.planeNode.setTag("mode_head",self.label) # tag to say it belongs to this mode_head self.planeNode.setTag("mode_node", 'plane') self.planeFromObject = self.planeNode.attachNewNode(CollisionNode("planeColNode")) self.planeFromObject.node().addSolid(CollisionPlane(Plane(Vec3(0,-1,0),Point3(0,0,0)))) self.plane = self.planeFromObject.node().getSolid(0) ### # set up preview text self.textNP = self.render2d.attachNewNode(TextNode('text node')) self.textNP.setScale(.2) self.textNP.setPos(-1,0,0) self.textNP.show() #set up dragging info self.grabbed_dict = dict() self.drag_dict = dict() #a mapping from selected nodes to their original positions for dragging self.mouse_down_loc = (0,0,0) self.lowest_level = 3 def open_math_data(self,math_data): if math_data.type == 'golog': golog_dict = math_data() #calls the mathdata (which is a golog_dict) subgolog = golog_dict['golog'] subfolder_path = golog_dict['folder_path'] #### just to be safe, but probably not needed subgolog_folder_path = os.path.join(self.folder_path,'subgologs') if not os.path.exists(subgolog_folder_path): os.mkdir(subgolog_folder_path) #### folder_path = os.path.join(self.folder_path, *golog_dict['folder_path']) print(folder_path) controllable_golog = mode_head(self.base, subgolog, folder_path = folder_path, parent = self) window_manager.modeHeadToWindow(self.base, controllable_golog) if math_data.type == 'file': file_name, file_extension = os.path.splitext(math_data()[-1]) # if file_extension == '.txt': # tk_funcs.edit_txt(os.path.join(self.folder_path,*math_data())) # else: #prompt user to select a program tk_funcs.run_program('',os.path.join(self.folder_path,*math_data())) if math_data.type == 'latex': file_dict = math_data() tex_folder = os.path.join(os.path.abspath(self.folder_path),*file_dict['folder']) tex_file = os.path.join(os.path.abspath(self.folder_path),*file_dict['tex']) if 'pdf' in file_dict.keys(): pdf_file = os.path.join(self.folder_path, *file_dict['pdf']) elif os.path.exists(tex_file.split('.tex')[0]+'.pdf'): #if there is a pdf in the folder with the same name file_dict['pdf'] = file_dict['tex'] file_dict['pdf'][-1] = file_dict['pdf'][-1].split('.tex')[0]+'.pdf' #change extension to .pdf pdf_file = os.path.join(self.folder_path, *file_dict['pdf']) else: pdf_file = None tk_funcs.pdf_or_tex(pdf_file, tex_file) if math_data.type == 'weblink': open_new_tab(math_data()) def update_math_data(self,simplex, math_data_type, **kwargs): if autosave == True: self.save() if 'label' in kwargs: simplex.label = kwargs['label'] self.golog.Simplex_to_Graphics[simplex].textNP.node().setText(simplex.label) self.golog.text_preview_set(self.bools['textboxes']) if simplex.math_data.type != 'None': answer = tk_funcs.are_you_sure('are you sure you want to delete the current math_data?') if answer == 'yes': self.delete_math_data(simplex) else: return if math_data_type == 'None': simplex.math_data = hcat.Math_Data(type = 'None') if math_data_type == 'golog': #create a path for all subgologs, if it doesn't already exist subgolog_folder_path = os.path.join(self.folder_path,'subgologs') if not os.path.exists(subgolog_folder_path): os.mkdir(subgolog_folder_path) new_golog = golog.golog(self.base, label = kwargs['label']) #create a new golog #create a unique folder path list in subgolog_folder_path unique_path = tk_funcs.unique_path(subgolog_folder_path,[kwargs['label']]) new_folder_path = ['subgologs', *unique_path] os.mkdir(os.path.join(self.folder_path , *new_folder_path)) #make the directory as above #create a new golog save at new_folder_path/label.golog new_save_location = os.path.join(self.folder_path, *new_folder_path, kwargs['label']+'.golog') #new_save_location should be a subfolder of subgologs gexport(new_golog, new_save_location) #math data is a dictionary of the physical golog and it's relative save path list golog_dict = {'golog':new_golog, 'folder_path':new_folder_path} simplex.math_data = hcat.Math_Data(math_data = golog_dict, type = 'golog') if math_data_type == 'file': if not os.path.exists(os.path.join(self.folder_path,'files')): os.mkdir(os.path.join(self.folder_path,'files')) file_folder_path = ['files'] file_location = tk_funcs.ask_file_location() if not file_location: return #if user cancels file_name = os.path.split(file_location)[1] #file name with extension file_path = tk_funcs.unique_path(os.path.join(self.folder_path),[*file_folder_path, file_name]) #get a unique file path starting from the file_folder copyfile(file_location, os.path.join(self.folder_path,*file_path)) simplex.math_data = hcat.Math_Data(math_data = file_path, type = 'file') #? add handler for if user exits text editor #? make asynchronous if math_data_type == 'latex': #ensure latex folder exists if not os.path.exists(os.path.join(self.folder_path,'latex')): os.mkdir(os.path.join(self.folder_path,'latex')) # create a uniquely named folder in self.folder_path/latex/ based on simplex.label tex_folder_path = tk_funcs.unique_path(root = self.folder_path, path = ['latex',simplex.label]) os.mkdir(os.path.join(self.folder_path, *tex_folder_path)) #create a tex file in tex folder tex_file_path = [*tex_folder_path, simplex.label+'.tex'] # ask if want new or to load one location = tk_funcs.load_tex(self.folder_path) # if new, returns True and copies template tex file # if load, returns a path and copies the path into tex_file_path true_path = os.path.join(self.folder_path,*tex_file_path) if location == True: copyfile(os.path.abspath('./misc_data/config_files/template.tex'), true_path) # # open( true_path , 'w').close() if isinstance(location, str): copyfile(location, true_path) # make a file dictionary with just tex file in it file_dict = {'tex':tex_file_path, 'folder':tex_folder_path} simplex.math_data = hcat.Math_Data(math_data = file_dict, type = 'latex') if math_data_type == 'weblink': weblink = tk_funcs.ask_weblink() simplex.math_data = hcat.Math_Data(math_data = weblink, type = 'weblink') if math_data_type == 'text': #create a text file if not os.path.exists(os.path.join(self.folder_path,'files')): os.mkdir(os.path.join(self.folder_path,'files')) file_folder_path = ['files'] file_path = tk_funcs.unique_path(root = self.folder_path, path = ['files',simplex.label+'.txt' ]) with open(os.path.join(self.folder_path, *file_path),'w') as file: pass #create a math_data for it simplex.math_data = hcat.Math_Data(math_data = file_path, type = 'file') #save golog if autosave == True: self.save() return simplex.math_data def delete_math_data(self,simplex,**kwargs): simplex.math_data.delete(self.folder_path) simplex.math_data = hcat.Math_Data(type = 'None') def setup_window(self, windict): self.windict = windict for button in self.buttons.keys(): self.listener.accept(self.windict['bt'].prefix+button, self.buttons[button], extraArgs = [self.windict['mw']]) for window_task in self.window_tasks.keys(): base.taskMgr.add(self.window_tasks[window_task], window_task, extraArgs = [self.windict['mw']], appendTask = True) self.has_window = True def save(self, *ask): #if has a parent, save the parent. If not, save itself if not self.parent: #if doesn't have a parent mode_head, save parent #if we pass something to ask it will ask (this includes mousewatchers) if ask: save_location = tk_funcs.ask_file_location(initial_dir = self.folder_path) if not save_location: return #if user cancels print('saving to:\n'+save_location) gexport(self.golog, self.folder_path) else: gexport(self.golog, self.file_path) else: self.parent.save()# if parent has no mode_head, save itself #basic reset function which shuts off the listener and removes button bindings. Should only be called if no "reset" function exists def basic_reset(self,*args): self.buttons = dict() self.window_tasks = dict() self.listener.ignoreAll() #function to call before deleting the mode_head, for example when closing a window def clean(self): self.reset() #close window if self.has_window == True: if hasattr(mode_head,'windict'): if 'win' in mode_head.windict.keys(): self.base.closeWindow(mode_head.windict['win'], keepCamera = True, removeWindow = True) self.has_window = False del self.golog.mode_heads[self.index] del self.reset # function to return the only the relevant collision data from the mouseRay def get_relevant_entries(self, mw): # get list of entries by distance if not mw.node().hasMouse(): return mpos = mw.node().getMouse() self.pickerRay.setFromLens(self.golog.camNode,mpos.getX(),mpos.getY()) #mouse ray goes from camera through the 'lens plane' at position of mouse self.golog.cTrav.traverse(self.golog.render) self.queue.sortEntries() # get the first relevant node traversed by mouseRay #### ignore everything with a mode_head tag that is not defined by this mode_head for e in self.queue.getEntries(): if e.getIntoNodePath().getParent().hasTag("mode_head"): if e.getIntoNodePath().getParent().getTag("mode_head") == self.label: return (e.getIntoNodePath().getParent(), e.getIntoNodePath().getParent().getTag("mode_node"),e.getSurfacePoint(e.getIntoNodePath())) break else: entry = e break #return node create_list in the golog entryNP = entry.getIntoNodePath().getParent() if entryNP.hasTag('level'): return (entryNP, entryNP.getTag('level'),entryNP.getPos()) #function to 'pick up' a node by adding it to the dragged dictionary def pickup(self, mw): if not mw.node().hasMouse(): return (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) ### get position on plane for mouseloc mpos = mw.node().getMouse() self.pickerRay.setFromLens(self.golog.camNode,mpos.getX(),mpos.getY()) #mouse ray goes from camera through the 'lens plane' at position of mouse self.golog.cTrav.traverse(self.golog.render) self.queue.sortEntries() for e in self.queue.getEntries(): if e.getIntoNodePath().getParent().hasTag("mode_head"): if e.getIntoNodePath().getParent().getTag("mode_head") == self.label: if e.getIntoNodePath().getParent().getTag("mode_node") == 'plane': self.mouse_down_loc = e.getSurfacePoint(e.getIntoNodePath()) break #if selected node is in the drag_dict, use it to set a mouse location # if entryNP in self.drag_dict.keys(): self.mouse_down_loc = entryNP.getPos() if node_type in ['0','1']: self.grabbed_dict = {'graphics': self.golog.NP_to_Graphics[entryNP],'dragged':False, 'orig_pos': entryNP.getPos()} if node_type == 'plane': for node in self.create_list[0]: node.setColorScale(1,1,1,1) #turn white self.create_list = [[],[]] for node in self.drag_dict.keys(): node.setColorScale(1,1,1,1) self.drag_dict = dict() #drag dict is used for multi-dragging a = self.plane.distToPlane(self.golog.camera.getPos())/(2*self.golog.camera.node().getLens().getNear())# ratio for camera movement self.grabbed_dict = {'graphics':self.golog.camera, 'dragged':False, 'orig_pos': self.golog.camera.getPos(), 'mpos': LPoint3f(a*mpos.getX(),a*0,a*mpos.getY())} self.lowest_level = 3 #function to 'put down' a node, returns true if it's dragged something def putdown(self, mw): for node in self.drag_dict.keys(): self.drag_dict[node] = self.golog.NP_to_Graphics[node].graphics_kwargs['pos'] self.lowest_level = min(self.lowest_level, int(node.getTag('level'))) if 'dragged' in self.grabbed_dict.keys(): if self.grabbed_dict['dragged'] == True: self.grabbed_dict = dict() return True self.grabbed_dict = dict() #function to select a node and add a 1-simplex between 2 create_list 0-simplecies def select_for_creation(self, mw): if not mw.node().hasMouse(): return #remove selected for node in self.drag_dict.keys(): node.setColorScale(1,1,1,1) self.drag_dict = dict() self.lowest_level = 3 ### selection ### (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) if node_type == '0':# and set(create_list[1:]) = {[]}: if entryNP not in self.create_list[0]: #? don't just append, re-sort self.create_list[0].append(entryNP) entryNP.setColorScale(1,0,0,0) #turn red if len(self.create_list[0]) == 2: # NP -> graphics -> simplex faces = tuple([self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[faceNP]] for faceNP in self.create_list[0][-1:-3:-1]]) asked_list = tk_funcs.ask_math_data('1-Simplex') if not asked_list: #[label, math_data_type,] return simplex = self.golog.add(faces, label = asked_list[0]) #reversed create_list objects and creates a 1 - simplex from them self.update_math_data(simplex, asked_list[1], label = asked_list[0]) for node in self.create_list[0]: node.setColorScale(1,1,1,1) self.create_list[0] = [] #reset create_list #open math_data of simplex under mouse def mouse_open(self, mw): (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) # if spaced on a 0 simplex, open it's math data, or create it if node_type in ['0','1']: simplex = self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[entryNP]] if not simplex.math_data(): asked_list = tk_funcs.ask_math_data(simplex.label) if not asked_list: return self.update_math_data(simplex, math_data_type = asked_list[1], label = asked_list[0]) #? make asynchronous else: self.open_math_data(simplex.math_data) # delete a simplex, prompt to delete the math_data and (recursively) it's supported simplecies def delete(self,mw): (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) if node_type in ['0','1']: #warning about deleting supported simplecies graphics = self.golog.NP_to_Graphics[entryNP] simplex = self.golog.Graphics_to_Simplex[graphics] if simplex.supports: answer = tk_funcs.are_you_sure(simplex.label+' still supports other simplecies. Are you sure you wish to delete?') if answer != 'yes': return self.delete_math_data(simplex) graphics._remove() #update the math_data of a simplex under the mouse def mouse_update(self, mw): (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) if node_type in ['0','1']: simplex = self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[entryNP]] asked_list = tk_funcs.ask_math_data(simplex.label) if not asked_list: return self.update_math_data(simplex, math_data_type = asked_list[1], label = asked_list[0]) #create a simplex given a mouse position def create(self, mw): (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) if node_type == 'plane': for node in self.create_list[0]: node.setColorScale(1,1,1,1) #turn white self.create_list = [[],[]] asked_list = tk_funcs.ask_math_data('0-Simplex') if not asked_list: return #if canceled, do not create a simplex simplex = self.golog.add(0, pos = entry_pos, label = asked_list[0]) #create a simplex self.update_math_data(simplex, math_data_type = asked_list[1], label = asked_list[0]) #function which checks the drag dictionary and drags stuff def drag(self, mw): if self.grabbed_dict: #get mouse_loc mpos = mw.node().getMouse() self.pickerRay.setFromLens(self.golog.camNode,mpos.getX(),mpos.getY()) #mouse ray goes from camera through the 'lens plane' at position of mouse self.golog.cTrav.traverse(self.golog.render) self.queue.sortEntries() mouseloc = None for e in self.queue.getEntries(): if e.getIntoNodePath().getParent().getTag("mode_node") == 'plane': mouseloc = e.getSurfacePoint(e.getIntoNodePath()) break if not mouseloc:return self.bools['dragging'] = True # if there is something in the drag dict (for multiselect) if self.drag_dict: self.grabbed_dict['dragged'] = True #only drag lowest dim simplecies for node in self.drag_dict.keys(): if int(node.getTag('level')) == self.lowest_level: self.golog.NP_to_Graphics[node].update({'pos':self.drag_dict[node]+mouseloc-self.mouse_down_loc}) #if nothing is selected, drag the thing below you elif self.grabbed_dict['graphics'] != self.golog.camera: #radius of drag selection offset = mouseloc - self.grabbed_dict['graphics'].parent_pos_convolution() delta = offset - self.grabbed_dict['orig_pos'] norm = delta.getX()**2 +delta.getY()**2 +delta.getZ()**2 if self.grabbed_dict['dragged'] == True or norm > 1: self.grabbed_dict['dragged'] = True #if offset magnitude is greater than 1 or dragged == true, actually drag it if self.grabbed_dict['dragged'] == True: self.grabbed_dict['graphics'].update({'pos':offset}) elif self.grabbed_dict['graphics'] == self.golog.camera: a = self.plane.distToPlane(self.golog.camera.getPos())/(2*self.golog.camera.node().getLens().getNear())# ratio for camera movement self.golog.camera.setPos(self.grabbed_dict['orig_pos']-(a*mpos.getX(),0,a*mpos.getY())+self.grabbed_dict['mpos']) #send preview of math_data of simplex under the mouse def preview(self, mw): (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) if node_type == '0': simplex = self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[entryNP]] elif node_type == '1': #? again consider what needs to be shown with 1-simplecies simplex = self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[entryNP]] else: return # set preview up if self.has_window: if simplex.math_data.type == 'golog': self.windict['preview_dr'].setCamera(simplex.math_data()['golog'].camera) else: self.windict['preview_dr'].setCamera(self.camera2D) self.textNP.node().setText("label:\n" +simplex.label+"\n\n math data type:\n" + simplex.math_data.type) return def pprint(self,mw): (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) if node_type == '0': simplex = self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[entryNP]] elif node_type == '1': #? again consider what needs to be shown with 1-simplecies simplex = self.golog.Graphics_to_Simplex[self.golog.NP_to_Graphics[entryNP]] else: return simplex.pprint() #tool for selecting multiple simplecies def multi_select(self,mw): if isinstance(mw, NodePath): entryNP = mw node_type = entryNP.getTag('level') else: return #reset select and create if node_type in ['0','1']: if entryNP in self.drag_dict.keys(): del self.drag_dict[entryNP] entryNP.setColorScale(1,1,1,1) self.lowest_level = min([*[int(node.getTag('level')) for node in self.drag_dict.keys()],3]) else: self.drag_dict[entryNP] = self.golog.NP_to_Graphics[entryNP].graphics_kwargs['pos'] entryNP.setColorScale(.5,.5,0,1) self.lowest_level = min(self.lowest_level, int(entryNP.getTag('level'))) def shift_box(self,mw): if None in self.dict['shift_pt']: self.dict['shift_pt'] = [None,None] return pt_1 = self.dict['shift_pt'][0] pt_2 = self.dict['shift_pt'][1] if sum([x**2 for x in list(pt_2-pt_1)]) <= 1: (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) self.multi_select(entryNP) #create vectors N = self.planeFromObject.node().getSolid(0).getNormal() x = [(pt_2-pt_1).getX(),0,0] z = [0,0,(pt_2-pt_1).getZ()] campos = list(self.golog.camera.getPos()) for simplex in self.golog.sSet.rawSimps: node = self.golog.Simplex_to_Graphics[simplex].node_list[0] P = list(node.getPos()) if t_int(pt_1,x,z,campos,P): self.multi_select(node) self.dict['shift_pt'] = [None,None] def consolidate(self, selected = False,sel_simp = None): #ask for label, or cancel G = self.golog # if a collection isn't provided, use the (multi-select) drag dictionary if not selected: selected = [G.Graphics_to_Simplex[G.NP_to_Graphics[node]] for node in self.drag_dict.keys()] if not selected: return #return if there was nothing passed, and nothing in the drag_dict for simp in selected: for face in simp.faces: if face not in selected: selected.append(face) # #? select a simplex to consolidate into # sel_simp = None # for simp in selected: # if simp.math_data.type == 'None': # sel_simp = simp #make a golog from selected new_golog = golog.golog(self.base, label ='test') def add(simplex): for face in simplex.faces: add(face) new_golog.add(simplex, pos = G.Simplex_to_Graphics[simplex].graphics_kwargs['pos']) for simplex in selected: add(simplex) #consolidate into 1 simplex if sel_simp: #consolidate into sel_simp subgolog_folder_path = os.path.join(self.folder_path,'subgologs') unique_path = tk_funcs.unique_path(subgolog_folder_path,[sel_simp.label]) new_folder_path = ['subgologs', *unique_path] sel_simp.math_data = hcat.Math_Data(math_data = {'golog':new_golog, 'folder_path':new_folder_path}, type = 'golog') #? remove simplexes and place at selected simplex location return sel_simp #create an entirely new simplex to put the golog into else: #? ask for label / cancel label = "test" subgolog_folder_path = os.path.join(self.folder_path,'subgologs') unique_path = tk_funcs.unique_path(subgolog_folder_path,[label]) new_folder_path = ['subgologs', *unique_path] #create a simplex with average position of things in golog avg = LPoint3f(*[sum([G.Simplex_to_Graphics[simplex].graphics_kwargs['pos'][i]/len(new_golog.sSet.simplecies[()]) for simplex in new_golog.sSet.simplecies[()]]) for i in range(3)]) s = self.golog.add(0, label ='test', math_data = hcat.Math_Data(math_data = {'golog':new_golog, 'folder_path':new_folder_path}, type = 'golog'), pos = LPoint3f(avg)) return s ########## BEGIN DEFINING MODES ########## #selection and creation mode def selection_and_creation(self, windict): def mouse1(mw): if not mw: return self.bools['shift_clicked'] = False self.pickup(mw) def shift_mouse1(mw): if not mw: return #on click, begin a rectagle dragging function (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) self.dict['shift_pt'][0] = entry_pos self.bools['shift_clicked'] = True def mouse1_up(mw): dropped_bool = self.putdown(mw) if self.bools['shift_clicked']: (entryNP, node_type, entry_pos) = self.get_relevant_entries(mw) self.dict['shift_pt'][1] = entry_pos self.shift_box(mw) self.bools['shift_clicked'] = False elif dropped_bool: pass else: self.select_for_creation(mw) def space(mw): if not mw: return self.mouse_open(mw) def u(mw): if not mw: return #? do a change not just update self.mouse_update(mw) def c(mw): self.consolidate() def p(mw): if not mw: return self.pprint(mw) def mouse3(mw): if not mw: return self.create(mw) def backspace(mw): if not mw: return self.delete(mw) def mouse_watch_task(mw,task): if not mw: return task.cont if not mw.node().hasMouse(): return task.cont self.drag(mw) self.preview(mw) return task.cont def wheel_up(mw): if not mw:return a = self.plane.distToPlane(self.golog.camera.getPos())/(2*self.golog.camera.node().getLens().getNear())# ratio for camera movement self.golog.camera.setPos( self.golog.camera.getPos() + (0,10,0) ) #fix for offset by storing a global camera - plane ratio def wheel_down(mw): if not mw:return self.golog.camera.setPos( self.golog.camera.getPos() - (0,10,0) ) def reset(*args): self.buttons = dict() self.window_task = dict() self.reset = self.basic_reset self.reset = reset self.buttons = {'mouse1':mouse1, 'mouse1-up':mouse1_up, 'mouse3':mouse3,'c':c, 'space':space, 'escape':self.reset, 's':self.save, 'u':u,'backspace':backspace, 'shift-mouse1':shift_mouse1,'p':p,'wheel_up':wheel_up, "wheel_down":wheel_down} self.window_tasks = {'mouse_watch_task':mouse_watch_task} self.setup_window(windict)
def __init__( self, name: str, category: str, collision_settings: CollisionSettings, scale: int = None, animated_parts: list = None, static_parts: list = None, ): self.name = name log.debug(f"Initializing {self.name} object") self.category = category # creating empty node to attach everything to. This way it will be easier # to attach other objects (like floating text and such), coz offset trickery # from animation wont affect other items attached to node (since its not # a parent anymore, but just another child) entity_node = PandaNode(name) # for now, self.node will be empty nodepath - we will reparent it to render # on spawn, to dont overflood it with reference entity instances self.node = NodePath(entity_node) # separate visuals node, to which parts from below will be attached vn = PandaNode(f"{name}_visuals") self.visuals = self.node.attach_new_node(vn) # this allows for rotating node around its h without making it invisible self.visuals.set_two_sided(True) # storage for entity parts (visuals) that will be attached to entity node. # static parts is used for stuff that is SpritesheetObject and doesnt have # switcher in it. Animated_parts reffers to thing that need to be changed # in case some related even occurs self.static_parts = static_parts or [] self.animated_parts = animated_parts or [] # setting character's collisions entity_collider = CollisionNode(self.category) # if no collision mask has been received - using defaults if collision_settings.mask is not None: mask = BitMask32(collision_settings.mask) entity_collider.set_from_collide_mask(mask) entity_collider.set_into_collide_mask(mask) entity_collider.add_solid( collision_settings.shape(*collision_settings.size)) self.collision = self.node.attach_new_node(entity_collider) if collision_settings.position: self.collision.set_pos(*collision_settings.position) self.direction = None # initializing visual parts added with self.add_part() if self.static_parts or self.animated_parts: for sp in self.static_parts: sp.instance.wrt_reparent_to(self.visuals) if sp.position: sp.instance.set_pos(sp.position) if sp.scale: sp.instance.set_scale(sp.scale) for ap in self.animated_parts: ap.instance.node.wrt_reparent_to(self.visuals) if ap.position: ap.instance.node.set_pos(ap.position) if ap.scale: ap.instance.node.set_scale(ap.scale) # this will rescale whole node with all parts attached to it # in case some part as already been rescaled above - it will be rescaled # again, which may cause issues if scale: self.node.set_scale(scale) self.change_direction("right") # death status, that may be usefull during cleanup self.dead = False # attaching python tags to node, so these will be accessible during # collision events and similar stuff self.node.set_python_tag("name", self.name) self.node.set_python_tag("category", self.category) # I thought to put ctrav there, but for whatever reason it glitched proj # to fly into left wall. So I moved it to Creature subclass # debug function to show collisions all time if shared.settings.show_collisions: self.collision.show()
class MouseOverOnEntity(System): entity_filters = { 'mouseoverable': [Proxy('model'), MouseOverable], 'mouseoverable_geometry': [Proxy('geometry'), MouseOverableGeometry], 'camera': [Camera, Input, MouseOveringCamera], } proxies = { 'model': ProxyType(Model, 'node'), 'geometry': ProxyType(Geometry, 'node'), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.traverser = CollisionTraverser() self.queue = CollisionHandlerQueue() self.picker_ray = CollisionRay() self.picker_node = CollisionNode('mouse ray') self.picker_node.add_solid(self.picker_ray) self.picker_node.set_from_collide_mask(MOUSEOVER_MASK) self.picker_node.set_into_collide_mask(0x0) self.picker_node_path = NodePath(self.picker_node) self.traverser.add_collider(self.picker_node_path, self.queue) def enter_filter_mouseoverable(self, entity): model_proxy = self.proxies['model'] model_node = model_proxy.field(entity) mouseoverable = entity[MouseOverable] into_node = CollisionNode('wecs_mouseoverable') into_node.add_solid(mouseoverable.solid) into_node.set_from_collide_mask(0x0) into_node.set_into_collide_mask(mouseoverable.mask) into_node_path = model_node.attach_new_node(into_node) into_node_path.set_python_tag('wecs_mouseoverable', entity._uid) def exit_filter_mouseoverable(self, entity): # FIXME: Undo all the other stuff that accumulated! entity[MouseOverable].solid.detach_node() def enter_filter_mouseoverable_geometry(self, entity): into_node = self.proxies['geometry'].field(entity) old_mask = into_node.get_collide_mask() new_mask = old_mask | entity[MouseOverableGeometry].mask into_node.set_collide_mask(new_mask) into_node.find('**/+GeomNode').set_python_tag('wecs_mouseoverable', entity._uid) def update(self, entities_by_filter): for entity in entities_by_filter['camera']: mouse_overing = entity[MouseOveringCamera] camera = entity[Camera] input = entity[Input] # Reset overed entity to None mouse_overing.entity = None mouse_overing.collision_entry = None requested = 'mouse_over' in input.contexts has_mouse = base.mouseWatcherNode.has_mouse() if requested and has_mouse: # Attach and align testing ray, and run collisions self.picker_node_path.reparent_to(camera.camera) mpos = base.mouseWatcherNode.get_mouse() self.picker_ray.set_from_lens( base.camNode, mpos.getX(), mpos.getY(), ) self.traverser.traverse(camera.camera.get_top()) # Remember reference to mouseovered entity, if any if self.queue.get_num_entries() > 0: self.queue.sort_entries() entry = self.queue.get_entry(0) picked_node = entry.get_into_node_path() picked_uid = picked_node.get_python_tag( 'wecs_mouseoverable') mouse_overing.entity = picked_uid mouse_overing.collision_entry = entry
class Raycaster(Entity): def __init__(self): super().__init__(name='raycaster', eternal=True) self._picker = CollisionTraverser() # Make a traverser self._pq = CollisionHandlerQueue() # Make a handler self._pickerNode = CollisionNode('raycaster') self._pickerNode.set_into_collide_mask(0) self._pickerNP = self.attach_new_node(self._pickerNode) self._picker.addCollider(self._pickerNP, self._pq) self._pickerNP.show() def distance(self, a, b): return sqrt(sum((a - b)**2 for a, b in zip(a, b))) def raycast(self, origin, direction=(0, 0, 1), distance=inf, traverse_target=scene, ignore=list(), debug=False): self.position = origin self.look_at(self.position + direction) self._pickerNode.clearSolids() # if thickness == (0,0): if distance == inf: ray = CollisionRay() ray.setOrigin(Vec3(0, 0, 0)) # ray.setDirection(Vec3(0,1,0)) ray.setDirection(Vec3(0, 0, 1)) else: # ray = CollisionSegment(Vec3(0,0,0), Vec3(0,distance,0)) ray = CollisionSegment(Vec3(0, 0, 0), Vec3(0, 0, distance)) self._pickerNode.addSolid(ray) if debug: self._pickerNP.show() else: self._pickerNP.hide() self._picker.traverse(traverse_target) if self._pq.get_num_entries() == 0: self.hit = HitInfo(hit=False) return self.hit ignore += tuple([e for e in scene.entities if not e.collision]) self._pq.sort_entries() self.entries = [ # filter out ignored entities e for e in self._pq.getEntries() if e.get_into_node_path().parent not in ignore ] if len(self.entries) == 0: self.hit = HitInfo(hit=False) return self.hit self.collision = self.entries[0] nP = self.collision.get_into_node_path().parent point = self.collision.get_surface_point(nP) # point = Vec3(point[0], point[2], point[1]) point = Vec3(point[0], point[1], point[2]) world_point = self.collision.get_surface_point(render) # world_point = Vec3(world_point[0], world_point[2], world_point[1]) world_point = Vec3(world_point[0], world_point[1], world_point[2]) hit_dist = self.distance(self.world_position, world_point) if nP.name.endswith('.egg'): nP = nP.parent self.hit = HitInfo(hit=True) for e in scene.entities: if e == nP: # print('cast nP to Entity') self.hit.entity = e self.hit.point = point self.hit.world_point = world_point self.hit.distance = hit_dist self.hit.normal = Vec3(*self.collision.get_surface_normal( self.collision.get_into_node_path().parent).normalized()) self.hit.world_normal = Vec3( *self.collision.get_surface_normal(render).normalized()) return self.hit self.hit = HitInfo(hit=False) return self.hit def boxcast(self, origin, direction=(0, 0, 1), distance=9999, thickness=(1, 1), traverse_target=scene, ignore=list(), debug=False): if isinstance(thickness, (int, float, complex)): thickness = (thickness, thickness) temp = Entity(position=origin, model='cube', origin_z=-.5, scale=Vec3(abs(thickness[0]), abs(thickness[1]), abs(distance)), collider='box', color=color.white33, always_on_top=debug, visible=debug) temp.look_at(origin + direction) hit_info = temp.intersects(traverse_target=traverse_target, ignore=ignore) if debug: temp.collision = False destroy(temp, delay=.1) else: destroy(temp) return hit_info
def __init__(self, name, spritesheet=None, sprite_size=None, hitbox_size=None, collision_mask=None, position=None, animations_speed=None): log.debug(f"Initializing {name} object") if not animations_speed: animations_speed = DEFAULT_ANIMATIONS_SPEED self.animations_timer = animations_speed self.animations_speed = animations_speed if not sprite_size: sprite_size = DEFAULT_SPRITE_SIZE if not spritesheet: #I cant link assets above, coz their default value is None texture = base.assets.sprite[name] else: texture = base.assets.sprite[spritesheet] size_x, size_y = sprite_size log.debug(f"{name}'s size has been set to {size_x}x{size_y}") #the magic that allows textures to be mirrored. With that thing being #there, its possible to use values in range 1-2 to get versions of sprites #that will face the opposite direction, removing the requirement to draw #them with hands. Without thing thing being there, 0 and 1 will be threated #as same coordinates, coz "out of box" texture wrap mode is "repeat" texture.set_wrap_u(Texture.WM_mirror) texture.set_wrap_v(Texture.WM_mirror) sprite_data = spritesheet_cutter.cut_spritesheet(texture, sprite_size) horizontal_scale, vertical_scale = sprite_data['offset_steps'] offsets = sprite_data['offsets'] entity_frame = CardMaker(name) #setting frame's size. Say, for 32x32 sprite all of these need to be 16 entity_frame.set_frame(-(size_x / 2), (size_x / 2), -(size_y / 2), (size_y / 2)) entity_object = render.attach_new_node(entity_frame.generate()) entity_object.set_texture(texture) #okay, this does the magic #basically, to show the very first sprite of 2 in row, we set tex scale #to half (coz half is our normal char's size). If we will need to use it #with sprites other than first - then we also should adjust offset accordingly #entity_object.set_tex_offset(TextureStage.getDefault(), 0.5, 0) #entity_object.set_tex_scale(TextureStage.getDefault(), 0.5, 1) entity_object.set_tex_scale(TextureStage.getDefault(), horizontal_scale, vertical_scale) #now, to use the stuff from cut_spritesheet function. #lets say, we need to use second sprite from sheet. Just do: #entity_object.set_tex_offset(TextureStage.getDefault(), *offsets[1]) #but, by default, offset should be always set to 0. In case our object #has just one sprite. Or something like that default_sprite = 0 entity_object.set_tex_offset(TextureStage.getDefault(), *offsets[default_sprite]) #enable support for alpha channel. This is a float, e.g making it non-100% #will require values between 0 and 1 entity_object.set_transparency(1) #if no position has been received - wont set it up if position: entity_object.set_pos(*position) #setting character's collisions entity_collider = CollisionNode(name) #if no collision mask has been received - using defaults if collision_mask: entity_collider.set_from_collide_mask(BitMask32(collision_mask)) entity_collider.set_into_collide_mask(BitMask32(collision_mask)) #TODO: move this to be under character's legs #right now its centered on character's center if hitbox_size: self.hitbox_size = hitbox_size else: #coz its sphere and not oval - it doesnt matter if we use x or y #but, for sake of convenience - we are going for size_y self.hitbox_size = (size_y / 2) entity_collider.add_solid(CollisionSphere(0, 0, 0, self.hitbox_size)) entity_collision = entity_object.attach_new_node(entity_collider) #this will explode if its not, but I dont have a default right now if name in ANIMS: entity_anims = ANIMS[name] self.name = name self.object = entity_object self.collision = entity_collision self.sprites = offsets #setting this to None may cause crashes on few rare cases, but going #for "idle_right" wont work for projectiles... So I technically add it #there for anims updater, but its meant to be overwritten at 100% cases self.current_animation = None #this will always be 0, so regardless of consistency I will live it be self.current_frame = default_sprite self.animations = entity_anims #death status, that may be usefull during cleanup self.dead = False #attaching python tags to object node, so these will be accessible during #collision events and similar stuff self.object.set_python_tag("name", self.name) #I thought to put ctrav there, but for whatever reason it glitched projectile #to fly into left wall. So I moved it to Creature subclass #debug function to show collisions all time #self.collision.show() #do_method_later wont work there, coz its indeed based on frames base.task_mgr.add(self.update_anims, "update entity's animations")