@@ -631,6 +631,17 @@ def fn(v):
631631 _add_overlay (v , 'aspect' , data .data )
632632 self ._submit (fn )
633633
634+ # ------------------------------------------------------------------
635+ # Picking
636+ # ------------------------------------------------------------------
637+
638+ def pick (self , screen_x , screen_y ):
639+ """Pick geometry at screen coordinates. Returns hit info dict."""
640+ def fn (v ):
641+ origin , direction = v ._screen_to_ray (screen_x , screen_y )
642+ return v .rtx .pick (origin , direction )
643+ return self ._submit (fn )
644+
634645 # ------------------------------------------------------------------
635646 # Layer management
636647 # ------------------------------------------------------------------
@@ -1487,6 +1498,32 @@ def _get_right(self):
14871498 right = np .cross (world_up , front )
14881499 return right / (np .linalg .norm (right ) + 1e-8 )
14891500
1501+ def _screen_to_ray (self , screen_x , screen_y ):
1502+ """Convert screen pixel coordinates to a world-space ray.
1503+
1504+ Returns (origin, direction) as numpy float32 arrays of shape (3,).
1505+ """
1506+ front = self ._get_front ()
1507+ world_up = np .array ([0 , 0 , 1 ], dtype = np .float32 )
1508+ right = np .cross (world_up , front )
1509+ rn = np .linalg .norm (right )
1510+ if rn > 1e-8 :
1511+ right /= rn
1512+ else :
1513+ right = np .array ([1 , 0 , 0 ], dtype = np .float32 )
1514+ cam_up = np .cross (front , right )
1515+
1516+ fov_scale = np .tan (np .radians (self .fov ) / 2.0 )
1517+ aspect = self .render_width / max (1 , self .render_height )
1518+
1519+ # Window coords → NDC (-1..1)
1520+ nx = 2.0 * screen_x / max (1 , self .width ) - 1.0
1521+ ny = 1.0 - 2.0 * screen_y / max (1 , self .height )
1522+
1523+ direction = front + nx * fov_scale * aspect * right + ny * fov_scale * cam_up
1524+ direction = direction / (np .linalg .norm (direction ) + 1e-30 )
1525+ return self .position .copy (), direction .astype (np .float32 )
1526+
14901527 def _get_look_at (self ):
14911528 """Get the current look-at point."""
14921529 return self .position + self ._get_front () * 1000.0
@@ -5029,6 +5066,18 @@ def _handle_mouse_press(self, button, xpos, ypos):
50295066 self ._mouse_last_x = xpos
50305067 self ._mouse_last_y = ypos
50315068
5069+ elif button == 1 : # right click — object picking
5070+ origin , direction = self ._screen_to_ray (xpos , ypos )
5071+ result = self .rtx .pick (origin , direction )
5072+ if result ['hit' ]:
5073+ gid = result ['geometry_id' ] or '?'
5074+ px , py , pz = result ['position' ]
5075+ print (f"Pick: geometry='{ gid } ' pos=({ px :.1f} , { py :.1f} , { pz :.1f} ) "
5076+ f"t={ result ['t' ]:.1f} prim={ result ['primitive_id' ]} "
5077+ f"instance={ result ['instance_id' ]} " )
5078+ else :
5079+ print ("Pick: no geometry hit" )
5080+
50325081 def _handle_mouse_release (self , button ):
50335082 """End drag on button release."""
50345083 self ._mouse_dragging = False
@@ -5327,6 +5376,7 @@ def _render_help_text(self):
53275376 ("GEOMETRY" , [
53285377 ("N" , "Cycle geometry layer" ),
53295378 ("P" , "Prev geometry in group" ),
5379+ ("Right-Click" , "Pick geometry" ),
53305380 ]),
53315381 ("OBSERVERS" , [
53325382 ("1-8" , "Select / create observer" ),
0 commit comments