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 Cursor(DirectObject): """Cursor object for the UI.""" parent: NodePath mouse_np: NodePath actor: Actor last_position: Point moved: bool pointed_at: Optional[NodePath] def __init__(self, parent: NodePath): super().__init__() self.parent = parent self.mouse_np = p3d.camera.attach_new_node(PandaNode('mouse')) self.mouse_np.set_y(p3d.lens.get_near()) picker_node = CollisionNode('mouse_ray') picker_np = p3d.camera.attach_new_node(picker_node) self._picker_ray = CollisionRay() picker_node.add_solid(self._picker_ray) self._collision_handler = CollisionHandlerQueue() self._traverser = CollisionTraverser('mouse_traverser') self._traverser.add_collider(picker_np, self._collision_handler) self.actor = Actor( resource_filename('tsim', 'data/models/cursor'), {'spin': resource_filename('tsim', 'data/models/cursor-spin')}) self.actor.loop('spin') self.actor.reparent_to(parent) self.actor.set_pos(0.0, 0.0, 0.0) self.actor.set_shader_off() self._position = Point(0.0, 0.0) self.last_position = self._position self.moved = False self.pointed_at = None self._tool: Optional[Tool] = None self._register_events() @property def position(self) -> Point: """Get the cursor position.""" return self._position @position.setter def position(self, value: Union[Point, Iterable]): if not isinstance(value, Point): value = Point(*islice(value, 2)) self.actor.set_x(value.x) self.actor.set_y(value.y) self._position = value @property def tool(self) -> Optional[Tool]: """Get current tool.""" return self._tool @tool.setter def tool(self, value: Tool): if self._tool is not None: self._tool.cleanup() self.ignore_all() self._register_events() self._tool = value if value is not None: for key in INPUT.keys_for('tool_1'): self.accept(key, self._tool.on_button1_press) self.accept(f'{key}-up', self._tool.on_button1_release) for key in INPUT.keys_for('tool_2'): self.accept(key, self._tool.on_button2_press) self.accept(f'{key}-up', self._tool.on_button2_release) for key in INPUT.keys_for('tool_3'): self.accept(key, self._tool.on_button3_press) self.accept(f'{key}-up', self._tool.on_button3_release) self.accept('cursor_move', self._tool.on_cursor_move) def update(self): """Update callback.""" self.actor.set_scale(p3d.camera.get_z()**0.6 / 10) self.moved = False if p3d.mouse_watcher.has_mouse(): mouse_x, mouse_y = p3d.mouse_watcher.get_mouse() self._picker_ray.set_from_lens(p3d.cam_node, mouse_x, mouse_y) self._traverser.traverse(p3d.render) if self._collision_handler.get_num_entries(): self._collision_handler.sort_entries() node_path = ( self._collision_handler.get_entry(0).get_into_node_path()) self.position = node_path.get_pos(p3d.render) self.actor.set_z(2.0) self.pointed_at = node_path else: self.pointed_at = None film = p3d.lens.get_film_size() * 0.5 self.mouse_np.set_x(mouse_x * film.x) self.mouse_np.set_y(p3d.lens.get_focal_length()) self.mouse_np.set_z(mouse_y * film.y) self.last_position = self._position mouse_pos = self.mouse_np.get_pos(self.parent) cam_pos = p3d.camera.get_pos(self.parent) mouse_vec = mouse_pos - cam_pos if mouse_vec.z < 0.0: scale = -mouse_pos.z / mouse_vec.z self.actor.set_pos(mouse_pos + mouse_vec * scale) self.position = self.actor.get_pos() if self._position != self.last_position: self.moved = True p3d.messenger.send('cursor_move') if self._tool is not None: self._tool.on_update() def _on_simulation_step(self, dt: Duration): if self._tool is not None: self._tool.on_simulation_step(dt) def _register_events(self): def set_tool(tool: Type[Tool]): self.tool = tool(self) log.info('[%s] Changing tool to %s', __name__, tool.__name__) for tool in TOOLS: try: self.accept(tool.KEY, partial(set_tool, tool)) except AttributeError: log.warning('[%s] No KEY set for tool %s', __name__, tool.__name__) self.accept('simulation_step', self._on_simulation_step)
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.velocity = Vec3(0, 0, 0) self.prev_click_time = time.time() self.double_click_distance = .5 self.hovered_entity = None self.left = False self.right = False self.middle = False self.delta_drag = Vec3(0, 0, 0) self.i = 0 self.update_rate = 10 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.raycast = True self.collision = None 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 @property def y(self): if not self._mouse_watcher.has_mouse(): return 0 return self._mouse_watcher.getMouseY() / 2 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) 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.position = Vec3(self.x, self.y, 0) self.moving = self.x + self.y != self.prev_x + self.prev_y if self.moving: if self.locked: self.velocity = self.position application.base.win.move_pointer(0, int(window.size[0] / 2), int(window.size[1] / 2)) 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_rate: 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) self._picker.traverse(base.render) if self._pq.get_num_entries() > 0: # print('collided with world', self._pq.getNumEntries()) self.find_collision() return # 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 if not self.collision.has_surface_normal(): print('no surface normal') return None n = self.collision.get_surface_normal( self.collision.get_into_node_path().parent) return (n[0], n[2], n[1]) @property def world_normal(self): if not self.collision: return None if not self.collision.has_surface_normal(): print('no surface normal') return None n = self.collision.get_surface_normal(render) return (n[0], n[2], n[1]) @property def point(self): if self.hovered_entity: p = self.collision.getSurfacePoint(self.hovered_entity) return Point3(p[0], p[2], p[1]) else: return None @property def world_point(self): if self.hovered_entity: p = self.collision.getSurfacePoint(render) return Point3(p[0], p[2], p[1]) else: return None def find_collision(self): if not self.raycast: return self._pq.sortEntries() if len(self._pq.get_entries()) == 0: self.collision = None return self.collisions = list() for entry in self._pq.getEntries(): # print(entry.getIntoNodePath().parent) for entity in scene.entities: if entry.getIntoNodePath().parent == entity: if entity.collision: self.collisions.append( Hit( hit=entry.collided(), entity=entity, distance=0, point=entry.getSurfacePoint(entity), world_point=entry.getSurfacePoint(scene), normal=entry.getSurfaceNormal(entity), world_normal=entry.getSurfaceNormal(scene), )) break self.collision = self._pq.getEntry(0) nP = self.collision.getIntoNodePath().parent for entity in scene.entities: if not hasattr(entity, 'collision' ) or not entity.collision or not entity.collider: continue # if hit entity is not hovered, call on_mouse_enter() if entity == nP: if not entity.hovered: entity.hovered = True self.hovered_entity = entity # print(entity.name) if hasattr(entity, 'on_mouse_enter'): entity.on_mouse_enter() for s in entity.scripts: if hasattr(s, 'on_mouse_enter'): s.on_mouse_enter() # unhover the rest else: if entity.hovered: entity.hovered = False 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()
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 MapPicker(): __name: Final[str] __base: Final[ShowBase] __data: Final[NDArray[(Any, Any, Any), np.uint8]] # collision data __ctrav: Final[CollisionTraverser] __cqueue: Final[CollisionHandlerQueue] __cn: Final[CollisionNode] __cnp: Final[NodePath] # picker data __pn: Final[CollisionNode] __pnp: Final[NodePath] __pray: Final[CollisionRay] # constants COLLIDE_MASK: Final[BitMask32] = BitMask32.bit(1) def __init__(self, services: Services, base: ShowBase, map_data: MapData, name: Optional[str] = None): self.__services = services self.__services.ev_manager.register_listener(self) self.__base = base self.__name = name if name is not None else (map_data.name + "_picker") self.__map = map_data self.__data = map_data.data # collision traverser & queue self.__ctrav = CollisionTraverser(self.name + '_ctrav') self.__cqueue = CollisionHandlerQueue() # collision boxes self.__cn = CollisionNode(self.name + '_cn') self.__cn.set_collide_mask(MapPicker.COLLIDE_MASK) self.__cnp = self.__map.root.attach_new_node(self.__cn) self.__ctrav.add_collider(self.__cnp, self.__cqueue) self.__points = [] z_offset = 1 if self.__map.dim == 3 else self.__map.depth for idx in np.ndindex(self.__data.shape): if bool(self.__data[idx] & MapData.TRAVERSABLE_MASK): p = Point(*idx) self.__points.append(p) idx = self.__cn.add_solid(CollisionBox(idx, Point3(p.x+1, p.y+1, p.z-z_offset))) assert idx == (len(self.__points) - 1) # mouse picker self.__pn = CollisionNode(self.name + '_pray') self.__pnp = self.__base.cam.attach_new_node(self.__pn) self.__pn.set_from_collide_mask(MapPicker.COLLIDE_MASK) self.__pray = CollisionRay() self.__pn.add_solid(self.__pray) self.__ctrav.add_collider(self.__pnp, self.__cqueue) # debug -> shows collision ray / impact point # self.__ctrav.show_collisions(self.__map.root) @property def name(self) -> str: return self.__name @property def pos(self): # check if we have access to the mouse if not self.__base.mouseWatcherNode.hasMouse(): return None # get the mouse position mpos = self.__base.mouseWatcherNode.get_mouse() # set the position of the ray based on the mouse position self.__pray.set_from_lens(self.__base.camNode, mpos.getX(), mpos.getY()) # find collisions self.__ctrav.traverse(self.__map.root) # if we have hit something sort the hits so that the closest is first if self.__cqueue.get_num_entries() == 0: return None self.__cqueue.sort_entries() # compute & return logical cube position x, y, z = self.__cqueue.get_entry(0).getSurfacePoint(self.__map.root) x, y, z = [max(math.floor(x), 0), max(math.floor(y), 0), max(math.ceil(z), 0)] if x == len(self.__data): x -= 1 if y == len(self.__data[x]): y -= 1 if z == len(self.__data[x][y]): z -= 1 return Point(x, y, z) def notify(self, event: Event) -> None: if isinstance(event, MapUpdateEvent): z_offset = 1 if self.__map.dim == 3 else self.__map.depth for p in event.updated_cells: if p.n_dim == 2: p = Point(*p, 0) if bool(self.__data[p.values] & MapData.TRAVERSABLE_MASK): self.__points.append(p) idx = self.__cn.add_solid(CollisionBox(p.values, Point3(p.x+1, p.y+1, p.z-z_offset))) assert idx == (len(self.__points) - 1) else: try: i = self.__points.index(p) except ValueError: continue self.__cn.remove_solid(i) self.__points.pop(i) def destroy(self) -> None: self.__cqueue.clearEntries() self.__ctrav.clear_colliders() self.__cnp.remove_node() self.__pnp.remove_node()