See #2516. References to titleblocks, layouts, and published sheets are now explicitly stored. Titleblock paths are now configurable. Generated filenames are sanitised.

This commit is contained in:
Dion Moult
2023-04-09 23:02:20 +10:00
parent 804ad0cece
commit 6ce49b7130
10 changed files with 114 additions and 46 deletions
@@ -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):
@@ -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):
@@ -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")
@@ -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):
@@ -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)
+2
View File
@@ -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")
+15 -6
View File
@@ -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):
+24 -13
View File
@@ -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):
+11
View File
@@ -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)
+3 -3
View File
@@ -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"))