From b58314f5e1ebe3e7fcf1fa67733bf14dd4987baf Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 2 Sep 2026 16:52:46 +0200 Subject: [PATCH] Fix Workspace.remove_object leaking Inbox-side registrations remove_object() only forgot the Outbox's persisted material/visibility slots for a removed guid, never the Inbox's own geometry_registry, metadata_registry, object_actions_registry, buttons callback map, or (for a Brep) its brep_viewmesh_registry entry. Any workflow that repeatedly removes and re-adds objects under the same guid built up an unbounded set of stale entries for the life of the process. Adds Inbox.forget_geometry(), called from remove_object() alongside the existing Outbox cleanup. --- CHANGELOG.md | 1 + src/compas_threejs/viewer/inbox.py | 19 +++++++++++++++++++ src/compas_threejs/viewer/workspace.py | 9 ++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c1230..09be058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bumped `compas-pb` to 1.2.0 (within the existing `>=1,<2` constraint), matching the version the bundled frontend's `compas-pb-ts` was upgraded to. +- Fixed `Workspace.remove_object()` never clearing an object's `Inbox`-side registrations (`geometry_registry`, `metadata_registry`, `object_actions_registry`, the guid-keyed `buttons` callback map, and a removed `Brep`'s `brep_viewmesh_registry` entry) - only the `Outbox`'s persisted material/visibility slots were forgotten, so any workflow that repeatedly removes and re-adds objects under the same guid (e.g. a viewer that rebuilds part of its own scene, or an object action whose geometry gets refreshed to pick up a new default value) leaked a growing set of stale entries, unbounded, for the life of the process. ### Removed diff --git a/src/compas_threejs/viewer/inbox.py b/src/compas_threejs/viewer/inbox.py index c6abaa6..eea65d3 100644 --- a/src/compas_threejs/viewer/inbox.py +++ b/src/compas_threejs/viewer/inbox.py @@ -39,6 +39,25 @@ def register_geometry(self, obj_id, geometry, metadata=None, actions=None): def register_button(self, guid, action): self.buttons[guid] = action + def forget_geometry(self, obj_id: str) -> None: + """ + Drops every registry entry register_geometry created for `obj_id` - called by + Workspace.remove_object, whose own cleanup only forgets the *outbox*'s persisted + material/visibility state (see there) and never touched these inbox-side registries. + Without this, a removed object's geometry/metadata/action-callback registrations stay + forever, and re-adding the same guid later (a Brep re-registered after + to_viewmesh()) would otherwise still work, but at the cost of a permanently growing + `buttons`/`object_actions_registry` for any workflow that repeatedly removes and + re-adds objects (e.g. a live-editable object action, or a viewer that clears and + rebuilds part of its own scene). + """ + self.geometry_registry.pop(obj_id, None) + self.metadata_registry.pop(obj_id, None) + actions = self.object_actions_registry.pop(obj_id, None) + if actions: + for action in actions: + self.buttons.pop(str(action.guid), None) + def register_action(self, name, callable_function): """Manually registers a callable against a fixed action name. diff --git a/src/compas_threejs/viewer/workspace.py b/src/compas_threejs/viewer/workspace.py index b296a53..f0054c9 100644 --- a/src/compas_threejs/viewer/workspace.py +++ b/src/compas_threejs/viewer/workspace.py @@ -357,8 +357,10 @@ def update_geometry(self, geometry): def remove_object(self, geometry): """Removes a geometry object from this workspace.""" # If the geometry is a Brep, remove its viewmesh from the registry + brep_id = None if isinstance(geometry, Brep): - viewmesh = self.app.inbox.brep_viewmesh_registry.get((self.workspace_id, geometry.guid)) + brep_id = geometry.guid + viewmesh = self.app.inbox.brep_viewmesh_registry.get((self.workspace_id, brep_id)) if viewmesh: geometry = viewmesh @@ -372,6 +374,11 @@ def remove_object(self, geometry): # for whichever it never had. self.app.outbox.forget(("material", obj_id), workspace_id=self.workspace_id) self.app.outbox.forget(("visibility", obj_id), workspace_id=self.workspace_id) + # Drops the inbox-side registrations too (geometry/metadata/action-callbacks) - see + # Inbox.forget_geometry for why that matters on its own, not just as an outbox mirror. + self.app.inbox.forget_geometry(str(obj_id)) + if brep_id is not None: + self.app.inbox.brep_viewmesh_registry.pop((self.workspace_id, brep_id), None) def hide_geometry(self, geometry): """Hides a geometry object in this workspace without removing it.