OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge commits / 5d90c3a

fix: prevent feedback loop and improve clear scene on import
- Add import cooldown on Godot side (500ms after export, 200ms after transform push) to prevent Blender's depsgraph echo from corrupting the Godot scene objects - Add frame skip (5 callbacks) in Blender change_detector after unsuppress to absorb depsgraph echo from imports - Improve _clear_bridge_objects to also remove orphaned data blocks (meshes, cameras, lights) and reset importer ID state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5d90c3a2ddc98d27f22c8f09eba864b09db22cd2
by plankatron <fleeky@gmail.com> · 2026-03-03T01:14:51.000Z

Changed files (5) +49 -1

blender_addon/__init__.py +14 -0
……@@ -55,6 +55,8 @@ def _poll_timer():
5555 scene = bpy.context.scene
5656 if hasattr(scene, "bridge_clear_on_import") and scene.bridge_clear_on_import:
5757 _clear_bridge_objects()
58 # Reset importer state so IDs don't conflict
59 _importer._id_to_name.clear()
5860 _importer.full_import()
5961 if _detector is not None:
6062 _detector.set_id_map(_importer._id_to_name)
……@@ -192,8 +194,20 @@ class BRIDGE_OT_import_scene(bpy.types.Operator):
192194def _clear_bridge_objects():
193195 """Remove all objects previously imported by the bridge."""
194196 to_remove = [obj for obj in bpy.data.objects if obj.name.startswith("bridge_")]
197 if to_remove:
198 print(f"BlenderBridge: Clearing {len(to_remove)} bridge objects")
195199 for obj in to_remove:
200 # Store data reference before removing object
201 data = obj.data
196202 bpy.data.objects.remove(obj, do_unlink=True)
203 # Clean up orphaned data blocks (meshes, cameras, lights)
204 if data is not None and data.users == 0:
205 if isinstance(data, bpy.types.Mesh):
206 bpy.data.meshes.remove(data)
207 elif isinstance(data, bpy.types.Camera):
208 bpy.data.cameras.remove(data)
209 elif isinstance(data, bpy.types.Light):
210 bpy.data.lights.remove(data)
197211
198212
199213# ── UI Panel ─────────────────────────────────────────────────────────────────
blender_addon/change_detector.py +14 -1
……@@ -54,6 +54,8 @@ class ChangeDetector:
5454 self._registered = False
5555 # Suppress change detection during imports (prevent feedback loop)
5656 self._suppressed = False
57 # Skip N depsgraph callbacks after unsuppress to absorb echo
58 self._skip_frames = 0
5759
5860 def register(self):
5961 """Register the depsgraph handler."""
……@@ -92,14 +94,25 @@ class ChangeDetector:
9294 self._suppressed = True
9395
9496 def unsuppress(self):
95 """Re-enable change detection after import."""
97 """Re-enable change detection after import.
98
99 Skips the next 5 depsgraph callbacks to absorb the echo from
100 the import — Blender's depsgraph fires asynchronously after
101 objects are modified, and those updates would loop back.
102 """
96103 self._suppressed = False
104 self._skip_frames = 5
97105
98106 def _on_depsgraph_update(self, scene, depsgraph):
99107 """Called after every depsgraph evaluation."""
100108 if self._suppressed:
101109 return
102110
111 # Skip N callbacks after unsuppress to absorb depsgraph echo
112 if self._skip_frames > 0:
113 self._skip_frames -= 1
114 return
115
103116 # Skip while any object is in edit mode — accessing mesh data crashes
104117 if bpy.context.mode != "OBJECT":
105118 return
blender_bridge_addon.zip +0 -0
(no content changes — binary or rename)
src/blender_bridge.cpp +16 -0
……@@ -377,6 +377,9 @@ Error BlenderBridge::export_scene(Node *root) {
377377 hdr.frame_number++;
378378 write_header(_shm.ptr(), hdr);
379379
380 // Cooldown: ignore Blender's depsgraph echo for 500ms
381 _import_cooldown = 0.5;
382
380383 // Signal Blender via UDS
381384 if (_uds.client_count() > 0) {
382385 const char *msg = "scene_exported";
……@@ -419,6 +422,11 @@ void BlenderBridge::import_changes(Node *root) {
419422 if (!_shm.valid())
420423 return;
421424
425 // Skip during cooldown — prevents feedback loop from Blender's
426 // depsgraph echo after we just exported
427 if (_import_cooldown > 0.0)
428 return;
429
422430 BridgeHeader hdr = read_header(_shm.ptr());
423431
424432 // Only process blender→godot updates
……@@ -586,6 +594,11 @@ void BlenderBridge::_process(double delta) {
586594 _handle_uds_message(msg);
587595 }
588596
597 // Tick down import cooldown
598 if (_import_cooldown > 0.0) {
599 _import_cooldown -= delta;
600 }
601
589602 // Auto-sync: check for incoming changes and push outgoing changes
590603 if (_auto_sync && _shm.valid()) {
591604 Node *root = get_tree() ? get_tree()->get_root() : nullptr;
……@@ -731,6 +744,9 @@ void BlenderBridge::_detect_and_push_changes() {
731744 hdr.frame_number++;
732745 write_header(_shm.ptr(), hdr);
733746
747 // Cooldown: ignore Blender's depsgraph echo for 200ms
748 _import_cooldown = 0.2;
749
734750 // Signal Blender
735751 if (_uds.client_count() > 0) {
736752 const char *msg = "transform_update";
src/blender_bridge.h +5 -0
……@@ -86,6 +86,11 @@ private:
8686 uint64_t _export_sequence = 0;
8787 uint64_t _last_import_sequence = 0;
8888
89 // ── Feedback loop prevention ────────────────────────────────────────
90 // After export, ignore incoming changes for a brief period to prevent
91 // Blender's depsgraph echo from corrupting the Godot scene.
92 double _import_cooldown = 0.0;
93
8994 // ── Object ID mapping ───────────────────────────────────────────────
9095 struct TrackedObject {
9196 uint32_t object_id;