1616from ..pyshics_materials .material_functions import assign_physics_material , create_default_material , \
1717 set_active_physics_material , set_material
1818
19+ # How long the viewport-navigation HUD dimming stays on after the view was
20+ # last seen changing (see draw_viewport_overlay). Short enough to feel
21+ # responsive once navigation actually stops, long enough to bridge the gap
22+ # between individual redraw callbacks.
23+ NAVIGATION_HOLD_SECONDS = 0.2
24+
1925
2026def alignObjects (new , old ):
2127 """Align two objects"""
@@ -110,11 +116,14 @@ def set_origin_to_center_of_mass(obj, depsgraph=None):
110116 print (f"Object '{ obj .name } ' has no vertices. Cannot calculate center of mass." )
111117 return
112118
113- # Use numpy for faster vertex operations
114- import numpy as np
115- verts_local = np .array ([v .co for v in mesh .vertices ])
116- verts_world = verts_local @ obj .matrix_world
117- com = np .mean (verts_world , axis = 0 )
119+ # Use numpy for faster vertex operations. obj.matrix_world is a 4x4
120+ # mathutils.Matrix; numpy's `@` can't multiply it directly against an
121+ # (N, 3) array (shape mismatch), so the rotation/scale submatrix and
122+ # translation are applied explicitly instead.
123+ verts_local = numpy .array ([v .co for v in mesh .vertices ])
124+ mat_world = numpy .array (obj .matrix_world )
125+ verts_world = verts_local @ mat_world [:3 , :3 ].T + mat_world [:3 , 3 ]
126+ com = numpy .mean (verts_world , axis = 0 )
118127
119128 # Calculate the offset
120129 offset = obj .matrix_world .inverted () @ mathutils .Vector (com )
@@ -247,6 +256,25 @@ def draw_viewport_overlay(self, context):
247256 """Draw 3D viewport overlay for the modal operator"""
248257 items = []
249258
259+ # Detecting "is the user currently navigating" from event types seen in
260+ # modal() doesn't work reliably: an MMB orbit drag hands its MOUSEMOVE
261+ # events - and usually even the terminating release - to Blender's own
262+ # view3d.rotate modal operator before they ever reach this operator's
263+ # modal(), so nothing observed there can tell whether a drag is still
264+ # in progress. This draw callback runs on every actual repaint though,
265+ # and Blender keeps repainting continuously for as long as the view is
266+ # visibly changing - so comparing the region's view matrix frame to
267+ # frame is a direct, reliable "is navigation actually happening" signal
268+ # instead of an event-based guess.
269+ region_3d = getattr (context .space_data , 'region_3d' , None )
270+ if region_3d is not None :
271+ view_snapshot = (region_3d .view_matrix .copy (), region_3d .view_distance )
272+ if self .navigation_view_snapshot is not None and view_snapshot != self .navigation_view_snapshot :
273+ self .navigation_hold_until = time .time () + NAVIGATION_HOLD_SECONDS
274+ self .arm_navigation_timer ()
275+ self .navigation_view_snapshot = view_snapshot
276+ self .navigation = time .time () < self .navigation_hold_until
277+
250278 self .valid_input_selection = True if len (self .new_colliders_list ) > 0 else False
251279 if self .use_space :
252280 label = "Global/Local"
@@ -849,6 +877,43 @@ def force_redraw():
849877 bpy .context .space_data .overlay .show_text = not bpy .context .space_data .overlay .show_text
850878 pass
851879
880+ def arm_navigation_timer (self ):
881+ """Make sure poke_navigation_redraw() is scheduled. Only one timer
882+ is ever in flight - it re-arms itself via its own return value for
883+ as long as navigation_hold_until keeps getting pushed out, so this
884+ just needs to kick it off once."""
885+ if not self .navigation_timer_scheduled :
886+ self .navigation_timer_scheduled = True
887+ bpy .app .timers .register (self .poke_navigation_redraw , first_interval = NAVIGATION_HOLD_SECONDS )
888+
889+ def poke_navigation_redraw (self ):
890+ """bpy.app.timers callback: force a repaint once the navigation hold
891+ window elapses, even if no further event ever reaches modal() or
892+ draw_viewport_overlay() to notice on its own (e.g. the user stops
893+ touching mouse/keyboard right after navigating, and Blender doesn't
894+ repaint an idle viewport by itself).
895+
896+ This deliberately does NOT set self.navigation directly - it only
897+ prompts a fresh repaint, and draw_viewport_overlay() re-derives the
898+ real answer from the live view_matrix each time it draws. That
899+ means this can never falsely clear the dimming while navigation is
900+ still genuinely in progress: if the view is still changing, Blender
901+ is already generating real repaints on its own, each of which
902+ pushes the hold window out again before this timer's turn comes.
903+ Re-arms itself via its return value, so only one timer is ever in
904+ flight.
905+ """
906+ try :
907+ remaining = self .navigation_hold_until - time .time ()
908+ if remaining > 0 :
909+ return remaining
910+ self .navigation_timer_scheduled = False
911+ self .navigation_area .tag_redraw ()
912+ except ReferenceError :
913+ # operator has already finished/cancelled and its RNA was freed
914+ pass
915+ return None
916+
852917 def set_collisions_wire_preview (self , mode ):
853918 """Show wireframe for colliders"""
854919 if mode in ['PREVIEW' , 'ALWAYS' ]:
@@ -1628,6 +1693,12 @@ def set_modal_state(self, cylinder_segments_active=False, displace_active=False,
16281693 self .height_active = height_active
16291694 self .width_active = width_active
16301695
1696+ # A keypress alone doesn't make Blender repaint the viewport (only
1697+ # mouse motion over the region does), so the new highlight color set
1698+ # above wouldn't show up until the next MOUSEMOVE. Force a repaint
1699+ # now so activating a parameter highlights it immediately.
1700+ self .force_redraw ()
1701+
16311702 def invoke (self , context , event ):
16321703 colSettings = context .scene .simple_collider
16331704
@@ -1649,6 +1720,10 @@ def invoke(self, context, event):
16491720
16501721 # INITIAL STATE
16511722 self .navigation = False
1723+ self .navigation_hold_until = 0.0 # grace window keeping navigation coloring on after the view last changed
1724+ self .navigation_timer_scheduled = False
1725+ self .navigation_area = context .area
1726+ self .navigation_view_snapshot = None # (view_matrix, view_distance) as of the last draw call
16521727 self .selected_objects = context .selected_objects .copy ()
16531728 self .active_obj = context .view_layer .objects .active
16541729 self .obj_mode = context .object .mode
@@ -1787,28 +1862,21 @@ def invoke(self, context, event):
17871862 def modal (self , context , event ):
17881863 colSettings = context .scene .simple_collider
17891864
1790- self .navigation = False
1791-
17921865 # Ignore if Alt is pressed
17931866 if event .alt :
17941867 self .ignore_input = True
17951868 self .force_redraw ()
17961869 return {'RUNNING_MODAL' }
17971870
17981871 if event .type in {'MIDDLEMOUSE' , 'WHEELUPMOUSE' , 'WHEELDOWNMOUSE' }:
1799- # allow navigation
1800- self .navigation = True
1801-
1802- self .opacity_active = False
1803- self .displace_active = False
1804- self .decimate_active = False
1805- self .cylinder_segments_active = False
1806- self .remesh_active = False
1807- self .height_active = False
1808- self .width_active = False
1809- self .sphere_segments_active = False
1810- self .capsule_segments_active = False
1811-
1872+ # Whether the view is actually navigating is now detected in
1873+ # draw_viewport_overlay() by watching the region's view_matrix
1874+ # (see there for why: this handler doesn't reliably see events
1875+ # for the duration of an MMB orbit drag at all, since Blender's
1876+ # own view3d.rotate modal operator consumes them). This branch
1877+ # only needs to cancel any in-progress parameter drag so
1878+ # navigating doesn't fight with an active (S)/(D)/(A)/etc. edit.
1879+ self .set_modal_state ()
18121880 return {'PASS_THROUGH' }
18131881
18141882 # User Input
0 commit comments