| … | … | @@ -294,8 +294,10 @@ def _attempt_reconnect(): |
| 294 | 294 | _detector._shm = _shm |
| 295 | 295 | _detector._pending.clear() |
| 296 | 296 | _detector._last_depsgraph = None |
| 297 | _detector._transform_hashes.clear() # Force re-detect all transforms | |
| 298 | _detector._geometry_hashes.clear() # Force re-detect all geometry | |
| 297 | # Keep _transform_hashes and _geometry_hashes intact — they reflect | |
| 298 | # the current Blender scene state and are still valid. Clearing them | |
| 299 | # causes ALL objects to be falsely re-detected as changed, triggering | |
| 300 | # an echo loop that duplicates objects in Godot. | |
| 299 | 301 | _detector.register() |
| 300 | 302 | _detector.set_resize_callback(_on_shm_resized) |
| 301 | 303 | else: |
| … | … | @@ -128,11 +128,24 @@ class ChangeDetector: |
| 128 | 128 | Only the named objects are suppressed. Other objects (e.g. those |
| 129 | 129 | with modifiers that depend on the moved object) still get their |
| 130 | 130 | updates captured and flushed to Godot. |
| 131 | ||
| 132 | Pre-caches transform and geometry hashes so that when suppression | |
| 133 | expires, the objects won't be falsely detected as "changed". | |
| 131 | 134 | """ |
| 132 | 135 | for name in names: |
| 133 | 136 | self._suppressed_objects[name] = max( |
| 134 | 137 | self._suppressed_objects.get(name, 0), frames |
| 135 | 138 | ) |
| 139 | # Pre-cache hashes to prevent false detection after unsuppress | |
| 140 | obj = bpy.data.objects.get(name) if bpy else None | |
| 141 | if obj is not None: | |
| 142 | self._transform_hashes[name] = self._hash_transform(obj.matrix_world) | |
| 143 | if obj.type == "MESH" and obj.data is not None: | |
| 144 | verts = obj.data.vertices | |
| 145 | if len(verts) > 0: | |
| 146 | coords = np.empty(len(verts) * 3, dtype=np.float32) | |
| 147 | verts.foreach_get("co", coords) | |
| 148 | self._geometry_hashes[name] = hash(coords.tobytes()) | |
| 136 | 149 | |
| 137 | 150 | def unsuppress(self): |
| 138 | 151 | """Decrement suppression counters (called each timer tick).""" |
| … | … | @@ -222,8 +235,12 @@ class ChangeDetector: |
| 222 | 235 | |
| 223 | 236 | name = obj.name |
| 224 | 237 | |
| 225 | # Skip echoes for objects that Godot just told us about | |
| 238 | # Skip echoes for objects that Godot just told us about, | |
| 239 | # but still cache the transform hash so unsuppress doesn't | |
| 240 | # trigger false change detection | |
| 226 | 241 | if name in self._suppressed_objects: |
| 242 | if update.is_updated_transform: | |
| 243 | self._transform_hashes[name] = self._hash_transform(obj.matrix_world) | |
| 227 | 244 | continue |
| 228 | 245 | |
| 229 | 246 | existing = self._pending.get(name, -1) |
| … | … | @@ -173,8 +173,8 @@ private: |
| 173 | 173 | c.fd = -1; |
| 174 | 174 | break; |
| 175 | 175 | } else { |
| 176 | // EAGAIN/EWOULDBLOCK = no more data | |
| 177 | if (errno == EAGAIN || errno == EWOULDBLOCK) | |
| 176 | // EAGAIN/EWOULDBLOCK = no more data; EINTR = signal interrupted | |
| 177 | if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) | |
| 178 | 178 | break; |
| 179 | 179 | // Other error = disconnect |
| 180 | 180 | close(c.fd); |
| … | … | @@ -223,14 +223,29 @@ private: |
| 223 | 223 | _clients.end()); |
| 224 | 224 | } |
| 225 | 225 | |
| 226 | static bool _write_all(int fd, const void *buf, size_t len) { | |
| 227 | const uint8_t *p = static_cast<const uint8_t *>(buf); | |
| 228 | size_t remaining = len; | |
| 229 | while (remaining > 0) { | |
| 230 | ssize_t n = write(fd, p, remaining); | |
| 231 | if (n > 0) { | |
| 232 | p += n; | |
| 233 | remaining -= (size_t)n; | |
| 234 | } else if (n < 0) { | |
| 235 | if (errno == EINTR) | |
| 236 | continue; | |
| 237 | if (errno == EAGAIN || errno == EWOULDBLOCK) | |
| 238 | continue; // Spin briefly for non-blocking (messages are small) | |
| 239 | return false; // Real error | |
| 240 | } | |
| 241 | } | |
| 242 | return true; | |
| 243 | } | |
| 244 | ||
| 226 | 245 | static void _send_to(int fd, const void *data, uint32_t size) { |
| 227 | 246 | uint32_t le_size = size; |
| 228 | // Best-effort write (non-blocking may partial write, but for signaling | |
| 229 | // messages this is fine — they're small) | |
| 230 | ssize_t n1 = write(fd, &le_size, 4); | |
| 231 | if (n1 == 4) { | |
| 232 | ssize_t n2 = write(fd, data, size); | |
| 233 | (void)n2; | |
| 247 | if (_write_all(fd, &le_size, 4)) { | |
| 248 | _write_all(fd, data, size); | |
| 234 | 249 | } |
| 235 | 250 | } |
| 236 | 251 | }; |