From 69d12aa87eeadc2a32ee1ba43860d8c43e26a210 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 12 Jul 2026 08:54:05 +0300 Subject: [PATCH] Bonsai: uniquify drawing name on rename so SVGs don't overwrite (#5635) A drawing's SVG filename is derived from its Name (get_default_drawing_path -> drawings/.svg). New drawings are de-duplicated by ensure_unique_drawing_name in core.add_drawing, but the rename path core.update_drawing_name applied the raw name with no uniqueness check. Renaming one drawing to another's name then made both resolve to the same SVG path: update_drawing_name moved one SVG onto the other and pointed both IfcDocumentReference.Location values at the same file, overwriting the drawing and crossing the sheet references. Give ensure_unique_drawing_name an optional `ignore` entity (so a drawing can keep its own name) and call it from update_drawing_name with ignore=drawing, mirroring add_drawing. Default stays None, so existing callers are unchanged. Verified live in headless Blender: renaming a second drawing to a taken name now yields "-X" and a distinct SVG path (paths no longer collide). Core test_drawing.py::TestUpdateDrawingName passes; the tool ensure_unique_drawing_name ignore case is confirmed live. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 --- src/bonsai/bonsai/core/drawing.py | 5 +++++ src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/bonsai/tool/drawing.py | 10 ++++++++-- src/bonsai/test/core/test_drawing.py | 2 ++ src/bonsai/test/tool/test_drawing.py | 4 +++- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 2a1f760570..26794fad22 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -475,6 +475,11 @@ def remove_drawing( def update_drawing_name( ifc: type[tool.Ifc], drawing_tool: type[tool.Drawing], drawing: ifcopenshell.entity_instance, name: str ) -> None: + # A drawing's Name is used to derive its SVG filename, so two drawings + # sharing a name would resolve to the same file and overwrite each other + # (and tangle the sheet references). Uniquify the name on rename, just like + # new drawings do, ignoring this drawing's own current name. + name = drawing_tool.ensure_unique_drawing_name(name, ignore=drawing) if drawing_tool.get_name(drawing) != name: ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name}) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 8a77d36661..52014676ff 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -357,7 +357,7 @@ class Drawing: def ensure_annotation_in_drawing_plane(cls, obj, camera=None): pass def ensure_drawings_parent_document(cls): pass def ensure_drawings_parent_group(cls): pass - def ensure_unique_drawing_name(cls, name): pass + def ensure_unique_drawing_name(cls, name, ignore=None): pass def ensure_unique_identification(cls, identification): pass def export_font_size(cls, obj): pass def export_symbol(cls, obj): pass diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index d08faa0229..1760782717 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -617,8 +617,14 @@ class Drawing(bonsai.core.tool.Drawing): props.is_editing_product = True @classmethod - def ensure_unique_drawing_name(cls, name: str) -> str: - names = [e.Name for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] + def ensure_unique_drawing_name( + cls, name: str, ignore: Optional[ifcopenshell.entity_instance] = None + ) -> str: + names = [ + e.Name + for e in tool.Ifc.get().by_type("IfcAnnotation") + if e.ObjectType == "DRAWING" and e != ignore + ] while name in names: name += "-X" return name diff --git a/src/bonsai/test/core/test_drawing.py b/src/bonsai/test/core/test_drawing.py index 914ddbbb54..79dd56ff7d 100644 --- a/src/bonsai/test/core/test_drawing.py +++ b/src/bonsai/test/core/test_drawing.py @@ -533,6 +533,7 @@ class TestRemoveDrawing: class TestUpdateDrawingName: def test_do_not_update_if_name_unchanged(self, ifc, drawing): + drawing.ensure_unique_drawing_name("name", ignore="drawing").should_be_called().will_return("name") drawing.get_name("drawing").should_be_called().will_return("name") drawing.set_camera_name("drawing", "name").should_be_called() drawing.get_drawing_group("drawing").should_be_called().will_return("group") @@ -549,6 +550,7 @@ class TestUpdateDrawingName: subject.update_drawing_name(ifc, drawing, drawing="drawing", name="name") def test_run(self, ifc, drawing): + drawing.ensure_unique_drawing_name("name", ignore="drawing").should_be_called().will_return("name") drawing.get_name("drawing").should_be_called().will_return("oldname") ifc.run("attribute.edit_attributes", product="drawing", attributes={"Name": "name"}).should_be_called() drawing.set_camera_name("drawing", "name").should_be_called() diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 580b326822..030822a81f 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -385,10 +385,12 @@ class TestEnsureUniqueDrawingName(NewFile): ifc = ifcopenshell.file() tool.Ifc.set(ifc) assert subject.ensure_unique_drawing_name("FOOBAR") == "FOOBAR" - ifc.createIfcAnnotation(Name="FOOBAR", ObjectType="DRAWING") + foobar = ifc.createIfcAnnotation(Name="FOOBAR", ObjectType="DRAWING") assert subject.ensure_unique_drawing_name("FOOBAR") == "FOOBAR-X" ifc.createIfcAnnotation(Name="FOOBAR-X", ObjectType="DRAWING") assert subject.ensure_unique_drawing_name("FOOBAR") == "FOOBAR-X-X" + # A drawing may keep its own name (e.g. when re-applying it on rename). + assert subject.ensure_unique_drawing_name("FOOBAR", ignore=foobar) == "FOOBAR" class TestEnsureUniqueIdentification(NewFile):