From 6ce49b71309121b384897e70c008bcfeeed322ed Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Apr 2023 23:02:20 +1000 Subject: [PATCH] See #2516. References to titleblocks, layouts, and published sheets are now explicitly stored. Titleblock paths are now configurable. Generated filenames are sanitised. --- .../blenderbim/bim/module/drawing/operator.py | 53 ++++++++++++++----- .../blenderbim/bim/module/drawing/prop.py | 15 +++--- .../blenderbim/bim/module/drawing/sheeter.py | 7 ++- .../bim/module/drawing/svgwriter.py | 1 + .../blenderbim/bim/module/drawing/ui.py | 7 ++- src/blenderbim/blenderbim/bim/ui.py | 2 + src/blenderbim/blenderbim/core/drawing.py | 21 +++++--- src/blenderbim/blenderbim/tool/drawing.py | 37 ++++++++----- src/blenderbim/blenderbim/tool/ifc.py | 11 ++++ src/blenderbim/test/tool/test_drawing.py | 6 +-- 10 files changed, 114 insertions(+), 46 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 8718ce35e8..09b027ba5d 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -732,7 +732,7 @@ class AddDrawingToSheet(bpy.types.Operator, Operator): active_drawing = props.drawings[props.active_drawing_index] active_sheet = props.sheets[props.active_sheet_index] drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id) - drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing)) + drawing_reference = tool.Drawing.get_drawing_document(drawing) sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id) if not sheet.is_a("IfcDocumentInformation"): @@ -745,7 +745,7 @@ class AddDrawingToSheet(bpy.types.Operator, Operator): has_drawing = False for reference in references: - if reference.Location == drawing_uri: + if reference.Location == drawing_reference.Location: has_drawing = True break @@ -754,9 +754,16 @@ class AddDrawingToSheet(bpy.types.Operator, Operator): reference = tool.Ifc.run("document.add_reference", information=sheet) if tool.Ifc.get_schema() == "IFC2X3": - attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))} + references = sheet.DocumentReferences + id_attr = "ItemReference" else: - attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))} + references = sheet.HasDocumentReferences + id_attr = "Identification" + attributes = { + id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1), + "Location": drawing_reference.Location, + "Description": "DRAWING", + } tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes) sheet_builder = sheeter.SheetBuilder() sheet_builder.data_dir = context.scene.BIMProperties.data_dir @@ -820,6 +827,15 @@ class CreateSheets(bpy.types.Operator): eps = os.path.splitext(svg)[0] + ".eps" dxf = os.path.splitext(svg)[0] + ".dxf" + references = getattr(sheet, "HasDocumentReferences", getattr(sheet, "DocumentReferences", [])) + if not [r for r in references if r.Description == "SHEET"]: + reference = tool.Ifc.run("document.add_reference", information=sheet) + tool.Ifc.run( + "document.edit_reference", + reference=reference, + attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"}, + ) + svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command svg2dxf_command = context.preferences.addons["blenderbim"].preferences.svg2dxf_command @@ -1186,25 +1202,29 @@ class BuildSchedule(bpy.types.Operator, Operator): core.build_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule)) -class AddScheduleToSheet(bpy.types.Operator): +class AddScheduleToSheet(bpy.types.Operator, Operator): bl_idname = "bim.add_schedule_to_sheet" bl_label = "Add Schedule To Sheet" bl_options = {"REGISTER", "UNDO"} - # TODO: check undo redo @classmethod def poll(cls, context): props = context.scene.DocProperties return props.schedules and props.sheets and context.scene.BIMProperties.data_dir - def execute(self, context): + def _execute(self, context): props = context.scene.DocProperties active_schedule = props.schedules[props.active_schedule_index] active_sheet = props.sheets[props.active_sheet_index] schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id) + if tool.Ifc.get_schema() == "IFC2X3": + schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg") + else: + schedule_location = tool.Drawing.get_path_with_ext(schedule.HasDocumentReferences[0].Location, "svg") + sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id) if not sheet.is_a("IfcDocumentInformation"): - return {"FINISHED"} + return if tool.Ifc.get_schema() == "IFC2X3": references = sheet.DocumentReferences or [] @@ -1213,19 +1233,25 @@ class AddScheduleToSheet(bpy.types.Operator): has_schedule = False for reference in references: - if reference.Location == tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg"): + if reference.Location == schedule_location: has_schedule = True break if has_schedule: - return {"FINISHED"} + return reference = tool.Ifc.run("document.add_reference", information=sheet) if tool.Ifc.get_schema() == "IFC2X3": - attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))} + references = sheet.DocumentReferences + id_attr = "ItemReference" else: - attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))} - attributes["Location"] = tool.Drawing.get_schedule_location(schedule) + references = sheet.HasDocumentReferences + id_attr = "Identification" + attributes = { + id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1), + "Location": schedule_location, + "Description": "SCHEDULE", + } tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes) sheet_builder = sheeter.SheetBuilder() @@ -1233,7 +1259,6 @@ class AddScheduleToSheet(bpy.types.Operator): sheet_builder.add_schedule(reference, schedule, sheet) tool.Drawing.import_sheets() - return {"FINISHED"} class AddDrawingStyleAttribute(bpy.types.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index f8083c7605..b8e68e4b7d 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -354,13 +354,14 @@ class DocProperties(PropertyGroup): decorations_colour: FloatVectorProperty( name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4 ) - docs_dir: StringProperty(default=os.path.join(".", "docs") + os.path.sep, name="Default Docs Directory") - sheets_dir: StringProperty(default=os.path.join(".", "sheets") + os.path.sep, name="Default Sheets Directory") - drawings_dir: StringProperty(default=os.path.join(".", "drawings") + os.path.sep, name="Default Drawings Directory") - stylesheet_path: StringProperty(default=os.path.join(".", "drawings", "assets", "default.css"), name="Default Stylesheet") - markers_path: StringProperty(default=os.path.join(".", "drawings", "assets", "markers.svg"), name="Default Markers") - symbols_path: StringProperty(default=os.path.join(".", "drawings", "assets", "symbols.svg"), name="Default Symbols") - patterns_path: StringProperty(default=os.path.join(".", "drawings", "assets", "patterns.svg"), name="Default Patterns") + docs_dir: StringProperty(default=os.path.join("docs") + os.path.sep, name="Default Docs Directory") + sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory") + titleblocks_dir: StringProperty(default=os.path.join("sheets", "titleblocks") + os.path.sep, name="Default Titleblocks Directory") + drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory") + stylesheet_path: StringProperty(default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet") + markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers") + symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols") + patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns") class BIMCameraProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py index 83895b01af..fdffe1f40f 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py @@ -35,6 +35,8 @@ class SheetBuilder: self.scale = "NTS" def create(self, sheet_path, titleblock_name): + sheet_dir = os.path.dirname(sheet_path) + root = ET.Element("svg") root.attrib["xmlns"] = "http://www.w3.org/2000/svg" root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink" @@ -48,7 +50,9 @@ class SheetBuilder: view = ET.SubElement(root, "g") view.attrib["data-type"] = "titleblock" titleblock = ET.SubElement(view, "image") - titleblock.attrib["xlink:href"] = f"./titleblocks/{titleblock_name}.svg" + titleblock.attrib["xlink:href"] = os.path.relpath( + tool.Drawing.get_default_titleblock_path(titleblock_name), sheet_dir + ) titleblock.attrib["x"] = "0" titleblock.attrib["y"] = "0" titleblock.attrib["width"] = str(view_width) @@ -58,7 +62,6 @@ class SheetBuilder: root.attrib["height"] = "{}mm".format(view_height) root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height) - sheet_dir = os.path.dirname(sheet_path) os.makedirs(sheet_dir, exist_ok=True) os.makedirs(os.path.join(sheet_dir, "titleblocks"), exist_ok=True) sheet_titleblock_path = os.path.join(sheet_dir, "titleblocks", titleblock_name + ".svg") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index f977ecee5a..972aa415ab 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -99,6 +99,7 @@ class SvgWriter: for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"): resource_path = pset.get(resource) if not resource_path: + self.resource_paths[resource] = None continue os.makedirs(os.path.dirname(resource_path), exist_ok=True) if not os.path.exists(resource_path): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index b0dc9b92f0..0991580711 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -528,5 +528,10 @@ class BIM_UL_sheets(bpy.types.UIList): row.label(text="", icon="IMAGE_DATA") elif item.reference_type == "SCHEDULE": row.label(text="", icon="LONGDISPLAY") - name = "{} - {}".format(item.identification or "X", item.name or "Unnamed") + elif item.reference_type == "TITLEBLOCK": + row.label(text="", icon="MENU_PANEL") + if item.identification: + name = f"{item.identification} - {item.name or 'Unnamed'}" + else: + name = item.name or "Unnamed" row.label(text=name) diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 6090712622..ad1c9665cb 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -187,6 +187,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row = self.layout.row(align=True) row.prop(context.scene.DocProperties, "sheets_dir") row = self.layout.row(align=True) + row.prop(context.scene.DocProperties, "titleblocks_dir") + row = self.layout.row(align=True) row.prop(context.scene.DocProperties, "drawings_dir") row = self.layout.row(align=True) row.prop(context.scene.DocProperties, "stylesheet_path") diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index fcc5302369..d792d92e11 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -66,22 +66,31 @@ def disable_editing_sheets(drawing): def add_sheet(ifc, drawing, titleblock=None): sheet = ifc.run("document.add_information") + layout = ifc.run("document.add_reference", information=sheet) + titleblock_reference = ifc.run("document.add_reference", information=sheet) identification = drawing.generate_sheet_identification() identification = drawing.ensure_unique_identification(identification) - attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "DOCUMENTATION"} if ifc.get_schema() == "IFC2X3": - attributes["DocumentId"] = attributes["Identification"] - del attributes["Identification"] - # TODO: How does IFC2X3 store the location? + attributes = {"DocumentId": identification, "Name": "UNTITLED", "Scope": "SHEET"} else: - attributes["Location"] = drawing.get_default_sheet_path(identification, "UNTITLED") + attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"} ifc.run("document.edit_information", information=sheet, attributes=attributes) + ifc.run( + "document.edit_reference", + reference=layout, + attributes={"Location": drawing.get_default_sheet_path(identification, "UNTITLED"), "Description": "LAYOUT"}, + ) + ifc.run( + "document.edit_reference", + reference=titleblock_reference, + attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"}, + ) drawing.create_svg_sheet(sheet, titleblock) drawing.import_sheets() def open_sheet(drawing, sheet=None): - drawing.open_svg(drawing.get_document_uri(sheet)) + drawing.open_svg(drawing.get_document_uri(sheet, "LAYOUT")) def remove_sheet(ifc, drawing, sheet=None): diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 5d12aab8f7..5682904ab2 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -255,8 +255,10 @@ class Drawing(blenderbim.core.tool.Drawing): return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") @classmethod - def get_document_uri(cls, document): + def get_document_uri(cls, document, description=None): if getattr(document, "Location", None): + if os.path.isabs(document.Location): + return document.Location ifc_path = tool.Ifc.get_path() if os.path.isfile(ifc_path): ifc_path = os.path.dirname(ifc_path) @@ -267,6 +269,8 @@ class Drawing(blenderbim.core.tool.Drawing): else: references = document.HasDocumentReferences for reference in references: + if description and reference.Description != description: + continue location = cls.get_document_uri(reference) if location: return location @@ -563,7 +567,7 @@ class Drawing(blenderbim.core.tool.Drawing): props = bpy.context.scene.DocProperties expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded} props.sheets.clear() - sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"] + sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"] for sheet in sheets: new = props.sheets.add() new.ifc_definition_id = sheet.id() @@ -579,22 +583,19 @@ class Drawing(blenderbim.core.tool.Drawing): continue for reference in cls.get_document_references(sheet): + if reference.Description == "LAYOUT": + continue # The layout itself is an internal detail and should not be visible to users new = props.sheets.add() new.ifc_definition_id = reference.id() new.is_sheet = False if tool.Ifc.get_schema() == "IFC2X3": - new.identification = reference.ItemReference or "X" + new.identification = reference.ItemReference or "" else: - new.identification = reference.Identification or "X" + new.identification = reference.Identification or "" - element = cls.get_reference_element(reference) - if element: - new.name = element.Name - new.reference_type = "DRAWING" - else: - new.name = cls.get_reference_document(reference).Name or "Unnamed" - new.reference_type = "SCHEDULE" + new.name = os.path.basename(reference.Location) + new.reference_type = reference.Description @classmethod def import_text_attributes(cls, obj): @@ -738,11 +739,21 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_default_sheet_path(cls, identification, name): - return os.path.join(bpy.context.scene.DocProperties.sheets_dir, f"{identification} - {name}.svg") + return os.path.join( + bpy.context.scene.DocProperties.sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg") + ) + + @classmethod + def get_default_titleblock_path(cls, name): + return os.path.join(bpy.context.scene.DocProperties.titleblocks_dir, cls.sanitise_filename(f"{name}.svg")) @classmethod def get_default_drawing_path(cls, name): - return os.path.join(bpy.context.scene.DocProperties.drawings_dir, f"{name}.svg") + return os.path.join(bpy.context.scene.DocProperties.drawings_dir, cls.sanitise_filename(f"{name}.svg")) + + @classmethod + def sanitise_filename(cls, name): + return "".join(x for x in name if (x.isalnum() or x in "_- ")) @classmethod def get_default_drawing_resource_path(cls, resource): diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 3051053d30..4f73034b6f 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -106,11 +106,22 @@ class Ifc(blenderbim.core.tool.Ifc): @classmethod def resolve_uri(cls, uri): + if os.path.isabs(uri): + return uri ifc_path = cls.get_path() if os.path.isfile(ifc_path): ifc_path = os.path.dirname(ifc_path) return uri if not uri or os.path.isabs(uri) else os.path.join(ifc_path, uri) + @classmethod + def get_relative_uri(cls, uri): + if not os.path.isabs(uri): + return uri + ifc_path = cls.get_path() + if os.path.isfile(ifc_path): + ifc_path = os.path.dirname(ifc_path) + return os.path.relpath(uri, ifc_path) + @classmethod def unlink(cls, element=None, obj=None): IfcStore.unlink_element(element, obj) diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py index 3f4c9ea4d2..4e664fb9b6 100644 --- a/src/blenderbim/test/tool/test_drawing.py +++ b/src/blenderbim/test/tool/test_drawing.py @@ -247,13 +247,13 @@ class TestGetDocumentUri(NewFile): def test_run(self): ifc = ifcopenshell.file() document = ifc.createIfcDocumentInformation( - Identification="X", Name="FOOBAR", Scope="DOCUMENTATION", Location="Location" + Identification="X", Name="FOOBAR", Scope="SHEET", Location="Location" ) assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location")) def test_get_indirect_locations(self): ifc = ifcopenshell.file() - document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION") + document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET") reference = ifc.createIfcDocumentReference(Location="Location", ReferencedDocument=document) assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location")) assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location")) @@ -263,7 +263,7 @@ class TestGetDocumentUri(NewFile): tool.Ifc.set(ifc) reference = ifc.createIfcDocumentReference(Location="Location") document = ifc.createIfcDocumentInformation( - DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION", DocumentReferences=[reference] + DocumentId="X", Name="FOOBAR", Scope="SHEET", DocumentReferences=[reference] ) assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location")) assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))