@@ -73,8 +73,14 @@ def get_on_screen_2d_bounding_boxes(
7373 rv3d = context .region_data
7474 assert rv3d
7575 view_location = rv3d .view_matrix .inverted ().translation
76+ view_normal = rv3d .view_rotation @ mathutils .Vector ((0.0 , 0.0 , - 1.0 ))
7677 obj_matrix = obj .matrix_world .copy ()
7778 bbox = [obj_matrix @ Vector (v ) for v in obj .bound_box ]
79+ bbox_edges = [
80+ (0 ,1 ),(1 ,2 ),(2 ,3 ),(3 ,0 ),
81+ (4 ,5 ),(5 ,6 ),(6 ,7 ),(7 ,4 ),
82+ (0 ,4 ),(1 ,5 ),(2 ,6 ),(3 ,7 )
83+ ]
7884
7985 transposed_bbox : list [Vector ] = []
8086 bbox_2d : list [float ] = []
@@ -98,8 +104,23 @@ def get_on_screen_2d_bounding_boxes(
98104
99105 for v in bbox :
100106 coord_2d = tool .Cad .location_3d_to_region_2d_np (context .region , context .space_data .region_3d , v )
101- if coord_2d is not None :
102- transposed_bbox .append (coord_2d )
107+ transposed_bbox .append (coord_2d )
108+
109+ if not any (transposed_bbox ):
110+ transposed_bbox = []
111+ # If there are None values in transposed_bbox it means that there are vertices behind the camera
112+ # so we get the intersection of the edge with the region border
113+ # new_bbox = []
114+ if any (transposed_bbox ) and not all (transposed_bbox ):
115+ new_bbox = transposed_bbox .copy ()
116+ new_bbox = [x for x in new_bbox if x is not None ]
117+ for edge in bbox_edges :
118+ if (transposed_bbox [edge [0 ]] is None ) ^ (transposed_bbox [edge [1 ]] is None ):
119+ point , _ = cls .intersect_edge_region_border (context .region , context .space_data , rv3d , bbox [edge [0 ]], bbox [edge [1 ]])
120+ if point :
121+ new_bbox .append (point )
122+ if new_bbox :
123+ transposed_bbox = new_bbox
103124
104125 region = context .region
105126 borders = (0 , region .width , 0 , region .height )
@@ -121,6 +142,99 @@ def get_on_screen_2d_bounding_boxes(
121142 return (obj , bbox_2d )
122143 return None
123144
145+ def intersect_edge_region_border (region , space , rv3d , v1 , v2 ):
146+ def segment_intersect_near_plane (view_matrix , clip_start , p_world_a , p_world_b ):
147+ a_view = view_matrix @ p_world_a
148+ b_view = view_matrix @ p_world_b
149+ z_near = - clip_start
150+ za = a_view .z
151+ zb = b_view .z
152+ denom = (zb - za )
153+ if denom == 0.0 :
154+ return None , None
155+ t = (z_near - za ) / denom
156+ if t < 0.0 or t > 1.0 :
157+ return None , None
158+ p_view = a_view .lerp (b_view , t )
159+ cam_world = view_matrix .inverted ()
160+ p_world = cam_world @ p_view
161+ return p_world , t
162+
163+ def is_inside_region (pt2d , region ):
164+ return 0.0 <= pt2d .x <= region .width and 0.0 <= pt2d .y <= region .height
165+
166+ def clamp_to_region_border (point2d , region ):
167+ x , y = point2d
168+ x_clamped = max (0.0 , min (region .width , x ))
169+ y_clamped = max (0.0 , min (region .height , y ))
170+ return Vector ((x_clamped , y_clamped ))
171+
172+ def find_nearby_onscreen_point (region , rv3d , p1 , p2 , initial_t_on_segment , max_iters = 40 , step = 0.05 ):
173+ """
174+ Use iterative approach: move t toward 0. Returns the first point that is inside region border
175+ """
176+ t = initial_t_on_segment
177+ for i in range (max_iters ):
178+ test_3d = p1 .lerp (p2 , t )
179+ test_2d = view3d_utils .location_3d_to_region_2d (region , rv3d , test_3d )
180+ if test_2d is not None and is_inside_region (test_2d , region ):
181+ return test_3d , test_2d , t
182+ # move t toward 0 by reducing it by a fraction of its current value
183+ t -= step
184+ # if t is already very small, break
185+ if t <= 1e-6 :
186+ break
187+
188+ return None , None , None
189+
190+ # Ensures that all the calculation uses the same direction based on which point is on the screen
191+ if view3d_utils .location_3d_to_region_2d (region , rv3d , v1 ):
192+ onscreen_vert = v1
193+ offscreen_vert = v2
194+ else :
195+ onscreen_vert = v2
196+ offscreen_vert = v1
197+ # v2, v1 = v1, v2
198+
199+ clip_start = space .clip_start
200+ view_mat = rv3d .view_matrix
201+ inter_world , t_on_ab = segment_intersect_near_plane (view_mat , clip_start , onscreen_vert , offscreen_vert )
202+
203+ if inter_world is None :
204+ print ("No intersection with viewport near plane found for the segment." )
205+ return
206+
207+ init_2d = view3d_utils .location_3d_to_region_2d (region , rv3d , inter_world )
208+
209+ if init_2d is not None and is_inside_region (init_2d , region ):
210+ final_world = inter_world
211+ final_2d = init_2d
212+ final_t = initial_t
213+ else :
214+ found_world , found_2d , found_t = find_nearby_onscreen_point (
215+ region , rv3d ,
216+ onscreen_vert , offscreen_vert ,
217+ t_on_ab ,
218+ max_iters = 600 , step = 0.01
219+ )
220+ if found_world is None :
221+ if init_2d is None :
222+ print ("Initial projection invalid and iterative search failed." )
223+ return
224+ # fallback: clamp projected point to border via manual mapping
225+ final_2d = clamp_to_region_border (init_2d , region )
226+ final_world = None
227+ final_t = None
228+ # print("Iterative search failed; using clamped 2D:", final_2d)
229+ else :
230+ final_world = found_world
231+ final_2d = found_2d
232+ final_t = found_t
233+ # print(f"Found onscreen point at t={final_t:.4f}")
234+
235+ # print("Final 2D:", final_2d)
236+ return final_2d , v2
237+
124238 @classmethod
125239 def intersect_mouse_2d_bounding_box (cls , mouse_pos : tuple [int , int ], bbox : list [float , float , float , float ]):
126240 x , y = mouse_pos
@@ -241,7 +355,7 @@ def ray_cast_by_proximity_2d(
241355 cls ,
242356 context : bpy .types .Context ,
243357 event : bpy .types .Event ,
244- snap_obj ,
358+ snap_obj : SnapObj ,
245359 ):
246360
247361 def divide_vector (start , end , n ):
@@ -258,22 +372,45 @@ def divide_vector(start, end, n):
258372 ray_origin , ray_target , ray_direction = cls .get_viewport_ray_data (context , event )
259373 points = []
260374
261- verts_2d = [
262- view3d_utils .location_3d_to_region_2d (region , rv3d , v ) for v in snap_obj .verts_3d
263- ] # Numpy version is worst in performance
264- verts_2d = [
265- view3d_utils .location_3d_to_region_2d (region , rv3d , v ) for v in snap_obj .verts_3d
266- ] # Numpy version is worst in performance
267- snap_threshold = 10.0
268-
269375 try :
270376 loc = tool .Cad .region_2d_to_location_3d_np (region , rv3d , mouse_pos , ray_direction )
271377 except :
272378 loc = Vector ((0 , 0 , 0 ))
273379
380+ verts_2d = [
381+ view3d_utils .location_3d_to_region_2d (region , rv3d , v ) for v in snap_obj .verts_3d
382+ ] # Numpy version is worst in performance
383+
384+ intersected = snap_obj .raycast_boxes (
385+ context , event , snap_obj .root , intersected = [], rays = (ray_origin , ray_direction )
386+ )
387+ edges = []
388+ for it in intersected :
389+ edges .extend (it .edges )
390+ edges = set (edges )
391+
392+ edge_verts = {}
393+ for e in edges :
394+ verts_idx = tuple (snap_obj .obj .data .edges [e ].vertices )
395+ verts = snap_obj .obj .data .vertices
396+ v1 = snap_obj .obj .matrix_world @ verts [verts_idx [0 ]].co
397+ v1_2d = verts_2d [verts_idx [0 ]]
398+ v2 = snap_obj .obj .matrix_world @ verts [verts_idx [1 ]].co
399+ v2_2d = verts_2d [verts_idx [1 ]]
400+ if (v1_2d is None ) ^ (v2_2d is None ):
401+ point , _ = cls .intersect_edge_region_border (region , context .space_data , rv3d , v1 , v2 )
402+ if v1_2d is None :
403+ edge_verts [e ] = (point , v2_2d )
404+ else :
405+ edge_verts [e ] = (v1_2d , point )
406+ else :
407+ edge_verts [e ] = (v1_2d , v2_2d )
408+
409+ snap_threshold = 10.0
410+
274411 for i , point in enumerate (verts_2d ):
275412 if not point :
276- break
413+ continue
277414 distance = (Vector (mouse_pos ) - point ).length
278415 if distance <= snap_threshold :
279416 snap_point = {
@@ -284,20 +421,12 @@ def divide_vector(start, end, n):
284421 }
285422 points .append (snap_point )
286423
287- intersected = snap_obj .raycast_boxes (
288- context , event , snap_obj .root , intersected = [], rays = (ray_origin , ray_direction )
289- )
290- edges = []
291- for it in intersected :
292- edges .extend (it .edges )
293- edges = set (edges )
294424 count = 0
295425 selected_edges = {}
296426 for e in edges :
297- idx = snap_obj .obj .data .edges [e ].vertices
298-
299- p0x , p0y = verts_2d [idx [0 ]][0 ], verts_2d [idx [0 ]][1 ]
300- p1x , p1y = verts_2d [idx [1 ]][0 ], verts_2d [idx [1 ]][1 ]
427+ p0 , p1 = edge_verts [e ]
428+ p0x , p0y = p0
429+ p1x , p1y = p1
301430 px , py = mouse_pos
302431
303432 # segment vector = p1 - p0
0 commit comments