diff --git a/src/blenderbim/blenderbim/bim/data/styles/default.css b/src/blenderbim/blenderbim/bim/data/assets/default.css similarity index 100% rename from src/blenderbim/blenderbim/bim/data/styles/default.css rename to src/blenderbim/blenderbim/bim/data/assets/default.css diff --git a/src/blenderbim/blenderbim/bim/data/templates/markers.svg b/src/blenderbim/blenderbim/bim/data/assets/markers.svg similarity index 100% rename from src/blenderbim/blenderbim/bim/data/templates/markers.svg rename to src/blenderbim/blenderbim/bim/data/assets/markers.svg diff --git a/src/blenderbim/blenderbim/bim/data/templates/patterns.svg b/src/blenderbim/blenderbim/bim/data/assets/patterns.svg similarity index 100% rename from src/blenderbim/blenderbim/bim/data/templates/patterns.svg rename to src/blenderbim/blenderbim/bim/data/assets/patterns.svg diff --git a/src/blenderbim/blenderbim/bim/data/styles/sample.css b/src/blenderbim/blenderbim/bim/data/assets/sample.css similarity index 100% rename from src/blenderbim/blenderbim/bim/data/styles/sample.css rename to src/blenderbim/blenderbim/bim/data/assets/sample.css diff --git a/src/blenderbim/blenderbim/bim/data/templates/symbols.svg b/src/blenderbim/blenderbim/bim/data/assets/symbols.svg similarity index 100% rename from src/blenderbim/blenderbim/bim/data/templates/symbols.svg rename to src/blenderbim/blenderbim/bim/data/assets/symbols.svg diff --git a/src/blenderbim/blenderbim/bim/data/templates/view-title.svg b/src/blenderbim/blenderbim/bim/data/assets/view-title.svg similarity index 100% rename from src/blenderbim/blenderbim/bim/data/templates/view-title.svg rename to src/blenderbim/blenderbim/bim/data/assets/view-title.svg diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index df53ddb710..cba91c9793 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -24,6 +24,7 @@ import zipfile import tempfile import ifcopenshell import blenderbim.bim.handler +import blenderbim.tool as tool from pathlib import Path @@ -371,7 +372,7 @@ class IfcStore: def unlink_element(element=None, obj=None): if element is None: try: - element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + element = tool.Ifc.get_entity(obj) except: pass diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 2a2d60d95c..8718ce35e8 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -179,6 +179,7 @@ class CreateDrawing(bpy.types.Operator): self.camera = context.scene.camera self.camera_element = tool.Ifc.get_entity(self.camera) + self.camera_document = tool.Drawing.get_drawing_document(self.camera_element) self.file = IfcStore.get_file() with profile("Drawing generation process"): @@ -198,14 +199,8 @@ class CreateDrawing(bpy.types.Operator): self.svg_writer.camera_projection = tuple( self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)) ) - pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"] - related_paths = { - "Stylesheet": pset.get("Stylesheet"), - "Markers": pset.get("Markers"), - "Symbols": pset.get("Symbols"), - "Patterns": pset.get("Patterns"), - } - self.svg_writer.define_related_paths(**related_paths) + + self.svg_writer.setup_drawing_resource_paths(self.camera_element) with profile("Generate underlay"): underlay_svg = self.generate_underlay(context) @@ -237,7 +232,7 @@ class CreateDrawing(bpy.types.Operator): def combine_svgs(self, context, underlay, linework, annotation): # Hacky :) - svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg") + svg_path = self.get_svg_path() with open(svg_path, "w") as outfile: self.svg_writer.create_blank_svg(svg_path).define_boilerplate() boilerplate = self.svg_writer.svg.tostring() @@ -282,7 +277,7 @@ class CreateDrawing(bpy.types.Operator): def generate_underlay(self, context): if not self.cprops.has_underlay: return - svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-underlay.svg") + svg_path = self.get_svg_path(cache_type="underlay") context.scene.render.filepath = svg_path[0:-4] + ".png" drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index] @@ -322,7 +317,7 @@ class CreateDrawing(bpy.types.Operator): def generate_linework(self, context): if not self.cprops.has_linework: return - svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-linework.svg") + svg_path = self.get_svg_path(cache_type="linework") if os.path.isfile(svg_path) and self.props.should_use_linework_cache: return svg_path @@ -605,7 +600,7 @@ class CreateDrawing(bpy.types.Operator): def generate_annotation(self, context): if not self.cprops.has_annotation: return - svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-annotation.svg") + svg_path = self.get_svg_path(cache_type="annotation") if os.path.isfile(svg_path) and self.props.should_use_annotation_cache: return svg_path @@ -662,6 +657,17 @@ class CreateDrawing(bpy.types.Operator): return element.LayerSetName return "mat-" + str(element.id()) + def get_svg_path(self, cache_type=None): + drawing_path = tool.Drawing.get_document_uri(self.camera_document) + drawings_dir = os.path.dirname(drawing_path) + + if cache_type: + drawings_dir = os.path.join(drawings_dir, "cache") + os.makedirs(drawings_dir, exist_ok=True) + return os.path.join(drawings_dir, f"{self.drawing_name}-{cache_type}.svg") + os.makedirs(drawings_dir, exist_ok=True) + return drawing_path + class AddAnnotation(bpy.types.Operator, Operator): bl_idname = "bim.add_annotation" @@ -711,7 +717,7 @@ class OpenSheet(bpy.types.Operator, Operator): core.open_sheet(tool.Drawing, sheet=sheet) -class AddDrawingToSheet(bpy.types.Operator): +class AddDrawingToSheet(bpy.types.Operator, Operator): bl_idname = "bim.add_drawing_to_sheet" bl_label = "Add Drawing To Sheet" bl_options = {"REGISTER", "UNDO"} @@ -721,11 +727,13 @@ class AddDrawingToSheet(bpy.types.Operator): props = context.scene.DocProperties return props.drawings and props.sheets and context.scene.BIMProperties.data_dir - def execute(self, context): + def _execute(self, context): props = context.scene.DocProperties 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)) + sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id) if not sheet.is_a("IfcDocumentInformation"): return {"FINISHED"} @@ -737,13 +745,7 @@ class AddDrawingToSheet(bpy.types.Operator): has_drawing = False for reference in references: - if tool.Ifc.get_schema() == "IFC2X3": - element = [r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference][ - 0 - ].RelatedObjects[0] - else: - element = reference.DocumentRefForObjects[0].RelatedObjects[0] - if element == drawing: + if reference.Location == drawing_uri: has_drawing = True break @@ -756,7 +758,6 @@ class AddDrawingToSheet(bpy.types.Operator): else: attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))} tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes) - tool.Ifc.run("document.assign_document", product=drawing, document=reference) sheet_builder = sheeter.SheetBuilder() sheet_builder.data_dir = context.scene.BIMProperties.data_dir sheet_builder.add_drawing(reference, drawing, sheet) @@ -810,15 +811,19 @@ class CreateSheets(bpy.types.Operator): name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0] sheet_builder = sheeter.SheetBuilder() sheet_builder.data_dir = scene.BIMProperties.data_dir - sheet_builder.build(sheet) + + # These variables will be made available to the evaluated commands + svg = sheet_builder.build(sheet) + basename = os.path.basename(svg) + path = os.path.dirname(svg) + pdf = os.path.splitext(svg)[0] + ".pdf" + eps = os.path.splitext(svg)[0] + ".eps" + dxf = os.path.splitext(svg)[0] + ".dxf" svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command svg2dxf_command = context.preferences.addons["blenderbim"].preferences.svg2dxf_command if svg2pdf_command: - path = os.path.join(scene.BIMProperties.data_dir, "build", name) - svg = os.path.join(path, name + ".svg") - pdf = os.path.join(path, name + ".pdf") # With great power comes great responsibility. Example: # [['inkscape', svg, '-o', pdf]] commands = eval(svg2pdf_command) @@ -826,11 +831,6 @@ class CreateSheets(bpy.types.Operator): subprocess.run(command) if svg2dxf_command: - path = os.path.join(scene.BIMProperties.data_dir, "build", name) - svg = os.path.join(path, name + ".svg") - eps = os.path.join(path, name + ".eps") - dxf = os.path.join(path, name + ".dxf") - base = os.path.join(path, name) # With great power comes great responsibility. Example: # [['inkscape', svg, '-o', eps], ['pstoedit', '-dt', '-f', 'dxf:-polyaslines -mm', eps, dxf, '-psarg', '-dNOSAFER']] commands = eval(svg2dxf_command) @@ -840,10 +840,7 @@ class CreateSheets(bpy.types.Operator): if svg2pdf_command: open_with_user_command(context.preferences.addons["blenderbim"].preferences.pdf_command, pdf) else: - open_with_user_command( - context.preferences.addons["blenderbim"].preferences.svg_command, - os.path.join(scene.BIMProperties.data_dir, "build", name, name + ".svg"), - ) + open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg) return {"FINISHED"} @@ -894,19 +891,19 @@ class OpenView(bpy.types.Operator): return self.execute(context) def execute(self, context): - if not self.open_all: - view_list = [self.view] + if self.open_all: + drawings = [tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings] else: - view_list = [d.name for d in context.scene.DocProperties.drawings] + drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] - data_dir = Path(context.scene.BIMProperties.data_dir) - diagrams = [] + drawing_uris = [] drawings_not_found = [] - for view_name in view_list.copy(): - path = data_dir / "diagrams" / (view_name + ".svg") - if not path.is_file(): - drawings_not_found.append(view_name) - diagrams.append(path) + + for drawing in drawings: + drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing)) + drawing_uris.append(drawing_uri) + if not os.path.exists(drawing_uri): + drawings_not_found.append(drawing.Name) if drawings_not_found: msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format( @@ -915,11 +912,8 @@ class OpenView(bpy.types.Operator): self.report({"ERROR"}, msg) return {"CANCELLED"} - for diagram_path in diagrams: - open_with_user_command( - context.preferences.addons["blenderbim"].preferences.svg_command, - str(diagram_path), - ) + for drawing_uri in drawing_uris: + open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, drawing_uri) return {"FINISHED"} @@ -1143,12 +1137,15 @@ class AddSchedule(bpy.types.Operator, Operator): bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"}) - use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) + use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) def _execute(self, context): filepath = self.filepath if self.use_relative_path: - filepath = os.path.relpath(filepath, bpy.path.abspath("//")) + ifc_path = tool.Ifc.get_path() + if os.path.isfile(ifc_path): + ifc_path = os.path.dirname(ifc_path) + filepath = os.path.relpath(filepath, ifc_path) core.add_schedule( tool.Ifc, tool.Drawing, @@ -1216,7 +1213,7 @@ class AddScheduleToSheet(bpy.types.Operator): has_schedule = False for reference in references: - if reference.Location == tool.Drawing.get_schedule_location(schedule): + if reference.Location == tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg"): has_schedule = True break diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index 74d8009143..f8083c7605 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -354,6 +354,13 @@ 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") class BIMCameraProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py index 3a202bb645..83895b01af 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py @@ -17,14 +17,15 @@ # along with BlenderBIM Add-on. If not, see . import os +import bpy import uuid +import shutil import ntpath import pystache import urllib.parse import xml.etree.ElementTree as ET import blenderbim.tool as tool import ifcopenshell.util.geolocation -from shutil import copy from xml.dom import minidom @@ -40,15 +41,14 @@ class SheetBuilder: root.attrib["id"] = "root" root.attrib["version"] = "1.1" - view_root = ET.parse( - os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg") - ).getroot() + titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg") + view_root = ET.parse(titleblock_path).getroot() view_width = self.convert_to_mm(view_root.attrib.get("width")) view_height = self.convert_to_mm(view_root.attrib.get("height")) view = ET.SubElement(root, "g") view.attrib["data-type"] = "titleblock" titleblock = ET.SubElement(view, "image") - titleblock.attrib["xlink:href"] = f"../templates/titleblocks/{titleblock_name}.svg" + titleblock.attrib["xlink:href"] = f"./titleblocks/{titleblock_name}.svg" titleblock.attrib["x"] = "0" titleblock.attrib["y"] = "0" titleblock.attrib["width"] = str(view_width) @@ -58,17 +58,24 @@ 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") + if not os.path.exists(sheet_titleblock_path): + shutil.copy(titleblock_path, sheet_titleblock_path) with open(sheet_path, "w") as f: f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")) def add_drawing(self, reference, drawing, sheet): filename = drawing.Name - sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0] - sheet_dir = os.path.join(self.data_dir, "sheets") - drawing_dir = os.path.join(self.data_dir, "diagrams") - sheet_path = os.path.join(sheet_dir, sheet_name + ".svg") - drawing_path = os.path.join(drawing_dir, filename + ".svg") - underlay_path = os.path.join(drawing_dir, filename + "-underlay.png") + sheet_path = tool.Drawing.get_document_uri(sheet) + sheet_name = os.path.splitext(os.path.basename(sheet_path))[0] + sheet_dir = os.path.dirname(sheet_path) + + drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing)) + drawing_path = tool.Ifc.resolve_uri(drawing_path) + underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png" if not os.path.isfile(sheet_path): raise FileNotFoundError @@ -110,7 +117,7 @@ class SheetBuilder: foreground.attrib["width"] = str(view_width) foreground.attrib["height"] = str(view_height) - self.add_view_title(30, view_height + 35, view) + self.add_view_title(30, view_height + 35, view, sheet_dir) sheet_tree.write(sheet_path) def remove_drawing(self, reference, sheet): @@ -128,11 +135,12 @@ class SheetBuilder: sheet_tree.write(sheet_path) def add_schedule(self, reference, schedule, sheet): - view_path = tool.Drawing.get_document_uri(schedule) + view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg") if not os.path.exists(view_path): tool.Drawing.create_svg_schedule(schedule) schedule_name = os.path.splitext(os.path.basename(view_path))[0] sheet_path = tool.Drawing.get_document_uri(sheet) + sheet_dir = os.path.dirname(sheet_path) ET.register_namespace("", "http://www.w3.org/2000/svg") ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") @@ -152,31 +160,40 @@ class SheetBuilder: foreground = ET.SubElement(view, "image") foreground.attrib["data-type"] = "table" - foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name) + foreground.attrib["xlink:href"] = os.path.relpath(view_path, sheet_dir) foreground.attrib["x"] = "30" foreground.attrib["y"] = "30" foreground.attrib["width"] = str(view_width) foreground.attrib["height"] = str(view_height) - self.add_view_title(30, view_height + 35, view) + self.add_view_title(30, view_height + 35, view, sheet_dir) sheet_tree.write(sheet_path) - def add_view_title(self, x, y, parent): - title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg")) + def add_view_title(self, x, y, parent, sheet_dir): + title_path = os.path.join(sheet_dir, "assets", "view-title.svg") + os.makedirs(os.path.dirname(title_path), exist_ok=True) + if not os.path.exists(title_path): + ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg") + shutil.copy(ootb_title, title_path) + + title_tree = ET.parse(title_path) title_root = title_tree.getroot() title = ET.SubElement(parent, "image") title.attrib["data-type"] = "view-title" - title.attrib["xlink:href"] = "../templates/view-title.svg" + title.attrib["xlink:href"] = os.path.relpath(title_path, sheet_dir) title.attrib["x"] = str(x) title.attrib["y"] = str(y) title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width"))) title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height"))) def build(self, sheet): - sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0] - os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True) + sheet_path = tool.Drawing.get_document_uri(sheet) + sheet_name = os.path.splitext(os.path.basename(sheet_path))[0] + self.sheet_dir = os.path.dirname(sheet_path) - sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg") + docs_dir = tool.Ifc.resolve_uri(os.path.join(bpy.context.scene.DocProperties.docs_dir, sheet_name)) + + os.makedirs(docs_dir, exist_ok=True) ET.register_namespace("", "http://www.w3.org/2000/svg") ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") @@ -191,9 +208,13 @@ class SheetBuilder: self.build_drawings(root, sheet) self.build_schedules(root) - with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output: + output_filename = os.path.join(docs_dir, f"{sheet_name}.svg") + + with open(output_filename, "wb") as output: tree.write(output) + return output_filename + def build_titleblock(self, root, sheet): titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0] image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0] @@ -233,7 +254,7 @@ class SheetBuilder: if background is not None: background_path = os.path.join(self.data_dir, "sheets", self.get_href(background)) - copy(background_path, os.path.join(self.data_dir, "build", sheet_name)) + shutil.copy(background_path, os.path.join(self.data_dir, "build", sheet_name)) if view_title is not None: foreground_path = self.get_href(foreground) @@ -298,7 +319,7 @@ class SheetBuilder: self.defs.append(clip_path) svg_path = self.get_href(image) - with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template: + with open(os.path.join(self.sheet_dir, svg_path), "r") as template: embedded = ET.fromstring(pystache.render(template.read(), data)) # viewBox should not be nested embedded.attrib["viewBox"] = "" diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index bbb2a7376f..f977ecee5a 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -21,6 +21,7 @@ import re import bpy import math import bmesh +import shutil import pystache import mathutils import xml.etree.ElementTree as ET @@ -65,10 +66,9 @@ class SvgWriter: self.scale = 1 / 100 # 1:100 self.camera_width = None self.camera_height = None - self.related_paths = None + self.resource_paths = {} def create_blank_svg(self, output_path): - self.define_related_paths() # making sure all paths are defined self.calculate_scale() self.svg = svgwrite.Drawing( output_path, @@ -94,26 +94,19 @@ class SvgWriter: ) return self - def define_related_paths(self, **related_paths): - if not self.related_paths: - self.related_paths = {} - - if not related_paths: - related_paths = { - "Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"), - "Markers": os.path.join(self.data_dir, "templates", "markers.svg"), - "Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"), - "Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"), - } - for path_name in list(related_paths.keys()): - if path_name in self.related_paths: - del related_paths[path_name] - - for path_name in related_paths: - uri = related_paths[path_name] - custom_path = tool.Ifc.resolve_uri(uri) - if custom_path: - self.related_paths[path_name] = custom_path + def setup_drawing_resource_paths(self, element): + pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing") + for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"): + resource_path = pset.get(resource) + if not resource_path: + continue + os.makedirs(os.path.dirname(resource_path), exist_ok=True) + if not os.path.exists(resource_path): + resource_basename = os.path.basename(resource_path) + ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename) + if os.path.exists(ootb_resource): + shutil.copy(ootb_resource, resource_path) + self.resource_paths[resource] = tool.Ifc.resolve_uri(resource_path) def define_boilerplate(self): self.add_stylesheet() @@ -132,28 +125,36 @@ class SvgWriter: self.height = self.raw_height * self.svg_scale def add_stylesheet(self): - with open(self.related_paths["Stylesheet"], "r") as stylesheet: + if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]): + return + with open(self.resource_paths["Stylesheet"], "r") as stylesheet: self.svg.defs.add(self.svg.style(stylesheet.read())) def add_markers(self): - tree = ET.parse(self.related_paths["Markers"]) + if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]): + return + tree = ET.parse(self.resource_paths["Markers"]) root = tree.getroot() for child in root: self.svg.defs.add(External(child)) def add_symbols(self): - tree = ET.parse(self.related_paths["Symbols"]) + if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]): + return + tree = ET.parse(self.resource_paths["Symbols"]) root = tree.getroot() for child in root: self.svg.defs.add(External(child)) def find_xml_symbol_by_id(self, id): - tree = ET.parse(self.related_paths["Symbols"]) + tree = ET.parse(self.resource_paths["Symbols"]) xml_symbol = tree.find(f'.//*[@id="{id}"]') return External(xml_symbol) if xml_symbol else None def add_patterns(self): - tree = ET.parse(self.related_paths["Patterns"]) + if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]): + return + tree = ET.parse(self.resource_paths["Patterns"]) root = tree.getroot() for child in root: self.svg.defs.add(External(child)) diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index b4705aa0b8..6090712622 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -182,6 +182,21 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.prop(context.scene.BIMProperties, "data_dir") row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="") + row = self.layout.row(align=True) + row.prop(context.scene.DocProperties, "docs_dir") + row = self.layout.row(align=True) + row.prop(context.scene.DocProperties, "sheets_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") + row = self.layout.row(align=True) + row.prop(context.scene.DocProperties, "markers_path") + row = self.layout.row(align=True) + row.prop(context.scene.DocProperties, "symbols_path") + row = self.layout.row(align=True) + row.prop(context.scene.DocProperties, "patterns_path") + row = layout.row() row.operator("bim.configure_visibility") diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index a7a88be04f..fcc5302369 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -72,6 +72,9 @@ def add_sheet(ifc, drawing, titleblock=None): if ifc.get_schema() == "IFC2X3": attributes["DocumentId"] = attributes["Identification"] del attributes["Identification"] + # TODO: How does IFC2X3 store the location? + else: + attributes["Location"] = drawing.get_default_sheet_path(identification, "UNTITLED") ifc.run("document.edit_information", information=sheet, attributes=attributes) drawing.create_svg_sheet(sheet, titleblock) drawing.import_sheets() @@ -103,13 +106,13 @@ def disable_editing_schedules(drawing): def add_schedule(ifc, drawing, uri=None): schedule = ifc.run("document.add_information") reference = ifc.run("document.add_reference", information=schedule) + name = drawing.get_path_filename(uri) if ifc.get_schema() == "IFC2X3": - attributes = {"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"} - ifc.run("document.edit_information", information=schedule, attributes=attributes) - ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) + attributes = {"DocumentId": "X", "Name": name, "Scope": "SCHEDULE"} else: - attributes = {"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": uri} - ifc.run("document.edit_information", information=schedule, attributes=attributes) + attributes = {"Identification": "X", "Name": name, "Scope": "SCHEDULE"} + ifc.run("document.edit_information", information=schedule, attributes=attributes) + ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) drawing.import_schedules() @@ -119,7 +122,7 @@ def remove_schedule(ifc, drawing, schedule=None): def open_schedule(drawing, schedule=None): - drawing.open_spreadsheet(drawing.get_schedule_location(schedule)) + drawing.open_spreadsheet(drawing.get_document_uri(schedule)) def update_schedule_name(ifc, drawing, schedule=None, name=None): @@ -164,8 +167,22 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None): "HasLinework": True, "HasAnnotation": True, "GlobalReferencing": True, + "Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"), + "Markers": drawing.get_default_drawing_resource_path("Markers"), + "Symbols": drawing.get_default_drawing_resource_path("Symbols"), + "Patterns": drawing.get_default_drawing_resource_path("Patterns"), }, ) + information = ifc.run("document.add_information") + uri = drawing.get_default_drawing_path(drawing_name) + reference = ifc.run("document.add_reference", information=information) + if ifc.get_schema() == "IFC2X3": + attributes = {"DocumentId": "X", "Name": drawing_name, "Scope": "DRAWING"} + else: + attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} + ifc.run("document.edit_information", information=information, attributes=attributes) + ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) + ifc.run("document.assign_document", product=element, document=reference) drawing.import_drawings() @@ -204,6 +221,7 @@ def remove_drawing(ifc, drawing_tool, drawing=None): if reference_obj: drawing_tool.delete_object(reference_obj) ifc.run("root.remove_product", product=reference) + ifc.run("document.remove_information", information=drawing_tool.get_drawing_document(drawing)) ifc.run("root.remove_product", product=drawing) drawing_tool.import_drawings() @@ -243,7 +261,7 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None) def build_schedule(drawing, schedule=None): drawing.create_svg_schedule(schedule) - drawing.open_svg(drawing.get_document_uri(schedule)) + drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg")) def sync_references(ifc, collector, drawing_tool, drawing=None): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 1b8d2b9d93..d7b09664dd 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -255,15 +255,19 @@ class Drawing: def get_annotation_context(cls, target_view): pass def get_assigned_product(cls, element): pass def get_body_context(cls): pass + def get_default_drawing_path(cls, name): pass + def get_default_drawing_resource_path(cls, resource): pass + def get_default_sheet_path(cls, identification, name): pass def get_document_uri(cls, document): pass def get_drawing_collection(cls, drawing): pass + def get_drawing_document(cls, drawing): pass def get_drawing_group(cls, drawing): pass + def get_path_filename(cls, uri): pass def get_drawing_references(cls, drawing): pass def get_drawing_target_view(cls, drawing): pass def get_group_elements(cls, group): pass def get_ifc_representation_class(cls, object_type): pass def get_name(cls, element): pass - def get_schedule_location(cls, schedule): pass def get_text_literal(cls, obj): pass def remove_literal_from_annotation(cls, obj, literal): pass def synchronise_ifc_and_text_attributes(cls, obj): pass diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index f6b4a69a42..5d12aab8f7 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -128,13 +128,17 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def create_svg_schedule(cls, schedule): schedule_creator = scheduler.Scheduler() - schedule_creator.schedule(cls.get_schedule_location(schedule), cls.get_document_uri(schedule)) + schedule_creator.schedule( + cls.get_document_uri(schedule), cls.get_path_with_ext(cls.get_document_uri(schedule), "svg") + ) @classmethod def create_svg_sheet(cls, document, titleblock): sheet_builder = sheeter.SheetBuilder() sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir - sheet_builder.create(cls.get_document_uri(document), titleblock) + uri = cls.get_document_uri(document) + sheet_builder.create(uri, titleblock) + return uri @classmethod def delete_collection(cls, collection): @@ -252,17 +256,28 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_document_uri(cls, document): - if hasattr(document, "Identification"): - name = document.Identification or "X" - else: - name = document.DocumentId or "X" - name += " - " + (document.Name or "Unnamed") - if not hasattr(document, "Scope"): - return - if document.Scope == "DOCUMENTATION": - return os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", name + ".svg") - elif document.Scope == "SCHEDULE": - return os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", name + ".svg") + if getattr(document, "Location", None): + ifc_path = tool.Ifc.get_path() + if os.path.isfile(ifc_path): + ifc_path = os.path.dirname(ifc_path) + return os.path.abspath(os.path.join(ifc_path, document.Location)) + if document.is_a("IfcDocumentInformation"): + if tool.Ifc.get_schema() == "IFC2X3": + references = document.DocumentReferences + else: + references = document.HasDocumentReferences + for reference in references: + location = cls.get_document_uri(reference) + if location: + return location + + @classmethod + def get_path_filename(cls, path): + return os.path.splitext(os.path.basename(path))[0] + + @classmethod + def get_path_with_ext(cls, path, ext): + return os.path.splitext(path)[0] + f".{ext}" @classmethod def get_drawing_collection(cls, drawing): @@ -276,6 +291,12 @@ class Drawing(blenderbim.core.tool.Drawing): if rel.is_a("IfcRelAssignsToGroup"): return rel.RelatingGroup + @classmethod + def get_drawing_document(cls, drawing): + for rel in drawing.HasAssociations: + if rel.is_a("IfcRelAssociatesDocument"): + return rel.RelatingDocument + @classmethod def get_drawing_references(cls, drawing): results = set() @@ -305,12 +326,6 @@ class Drawing(blenderbim.core.tool.Drawing): def get_name(cls, element): return element.Name - @classmethod - def get_schedule_location(cls, schedule): - if tool.Ifc.get_schema() == "IFC2X3": - return schedule.DocumentReferences[0].Location - return schedule.Location - @classmethod def generate_drawing_matrix(cls, target_view, location_hint): x = 0 if location_hint == 0 else bpy.context.scene.cursor.matrix[0][3] @@ -721,6 +736,18 @@ class Drawing(blenderbim.core.tool.Drawing): return location_hint + " " + target_view.split("_")[0] return target_view + @classmethod + def get_default_sheet_path(cls, identification, name): + return os.path.join(bpy.context.scene.DocProperties.sheets_dir, f"{identification} - {name}.svg") + + @classmethod + def get_default_drawing_path(cls, name): + return os.path.join(bpy.context.scene.DocProperties.drawings_dir, f"{name}.svg") + + @classmethod + def get_default_drawing_resource_path(cls, resource): + return getattr(bpy.context.scene.DocProperties, f"{resource.lower()}_path") or None + @classmethod def get_potential_reference_elements(cls, drawing): elements = [] diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 9065684e4f..3051053d30 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -106,7 +106,10 @@ class Ifc(blenderbim.core.tool.Ifc): @classmethod def resolve_uri(cls, uri): - return uri if not uri or os.path.isabs(uri) else os.path.join(os.path.dirname(cls.get_path()), 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 unlink(cls, element=None, obj=None): diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index f4d0d4777b..86b6e3c020 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -87,10 +87,11 @@ class TestAddSheet: drawing.generate_sheet_identification().should_be_called().will_return("identification") drawing.ensure_unique_identification("identification").should_be_called().will_return("u_identification") ifc.get_schema().should_be_called().will_return("IFC4") + drawing.get_default_sheet_path("u_identification", "UNTITLED").should_be_called().will_return("uri") ifc.run( "document.edit_information", information="sheet", - attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "DOCUMENTATION"}, + attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "DOCUMENTATION", "Location": "uri"}, ).should_be_called() drawing.create_svg_sheet("sheet", "titleblock").should_be_called() drawing.import_sheets().should_be_called() @@ -141,18 +142,21 @@ class TestDisableEditingSchedules: class TestAddSchedule: def test_run(self, ifc, drawing): ifc.run("document.add_information").should_be_called().will_return("schedule") + drawing.get_path_filename("uri").should_be_called().will_return("UNTITLED") ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference") ifc.get_schema().should_be_called().will_return("IFC4") ifc.run( "document.edit_information", information="schedule", - attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": "uri"}, + attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"}, ).should_be_called() + ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called() drawing.import_schedules().should_be_called() subject.add_schedule(ifc, drawing, uri="uri") def test_using_a_document_id_in_ifc2x3(self, ifc, drawing): ifc.run("document.add_information").should_be_called().will_return("schedule") + drawing.get_path_filename("uri").should_be_called().will_return("UNTITLED") ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference") ifc.get_schema().should_be_called().will_return("IFC2X3") ifc.run( @@ -174,7 +178,7 @@ class TestRemoveSchedule: class TestOpenSchedule: def test_run(self, drawing): - drawing.get_schedule_location("schedule").should_be_called().will_return("uri") + drawing.get_document_uri("schedule").should_be_called().will_return("uri") drawing.open_spreadsheet("uri").should_be_called() subject.open_schedule(drawing, schedule="schedule") @@ -225,6 +229,10 @@ class TestAddDrawing: ifc.run("group.assign_group", group="group", products=["element"]).should_be_called() collector.assign("obj").should_be_called() ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset") + drawing.get_default_drawing_resource_path("Stylesheet").should_be_called().will_return("stylesheet.css") + drawing.get_default_drawing_resource_path("Markers").should_be_called().will_return("markers.svg") + drawing.get_default_drawing_resource_path("Symbols").should_be_called().will_return("symbols.svg") + drawing.get_default_drawing_resource_path("Patterns").should_be_called().will_return("patterns.svg") ifc.run( "pset.edit_pset", pset="pset", @@ -236,8 +244,19 @@ class TestAddDrawing: "HasLinework": True, "HasAnnotation": True, "GlobalReferencing": True, + "Stylesheet": "stylesheet.css", + "Markers": "markers.svg", + "Symbols": "symbols.svg", + "Patterns": "patterns.svg", }, ).should_be_called() + drawing.get_default_drawing_path("name").should_be_called().will_return("uri") + ifc.run("document.add_information").should_be_called().will_return("information") + ifc.run("document.add_reference", information="information").should_be_called().will_return("reference") + ifc.get_schema().should_be_called().will_return("IFC4") + ifc.run("document.edit_information", information="information", attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"}).should_be_called() + ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called() + ifc.run("document.assign_document", product="element", document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint") @@ -277,6 +296,8 @@ class TestRemoveDrawing: ifc.get_object("reference").should_be_called().will_return("reference_obj") drawing.delete_object("reference_obj").should_be_called() ifc.run("root.remove_product", product="reference").should_be_called() + drawing.get_drawing_document("drawing").should_be_called().will_return("information") + ifc.run("document.remove_information", information="information").should_be_called() ifc.run("root.remove_product", product="drawing").should_be_called() drawing.import_drawings().should_be_called() subject.remove_drawing(ifc, drawing, drawing="drawing") diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py index f91d6cea25..3f4c9ea4d2 100644 --- a/src/blenderbim/test/tool/test_drawing.py +++ b/src/blenderbim/test/tool/test_drawing.py @@ -61,9 +61,15 @@ class TestCreateCamera(NewFile): class TestCreateSvgSheet(NewFile): def test_run(self): ifc = ifcopenshell.file() - document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION") - subject.create_svg_sheet(document, "A1") - assert os.path.isfile(os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")) + document = ifc.createIfcDocumentInformation( + Identification="X", + Name="FOOBAR", + Scope="DOCUMENTATION", + Location=os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", "X - FOOBAR.svg"), + ) + uri = subject.create_svg_sheet(document, "A1") + assert uri.endswith(".svg") + assert os.path.isfile(uri) class TestDeleteCollection(NewFile): @@ -204,11 +210,13 @@ class TestEnsureUniqueIdentification(NewFile): class TestExportTextLiteralAttributes(NewFile): def test_run(self): TestImportTextAttributes().test_run() - assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [{ - "Literal": "Literal", - "Path": "RIGHT", - "BoxAlignment": "bottom-left", - }] + assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [ + { + "Literal": "Literal", + "Path": "RIGHT", + "BoxAlignment": "bottom-left", + } + ] class TestGetAnnotationContext(NewFile): @@ -236,23 +244,29 @@ class TestGetBodyContext(NewFile): class TestGetDocumentUri(NewFile): - def test_get_sheet_uri(self): + def test_run(self): + ifc = ifcopenshell.file() + document = ifc.createIfcDocumentInformation( + Identification="X", Name="FOOBAR", Scope="DOCUMENTATION", 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") - result = subject.get_document_uri(document) - assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg") - - def test_get_schedule_uri(self): - ifc = ifcopenshell.file() - document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE") - result = subject.get_document_uri(document) - assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", "X - FOOBAR.svg") + 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")) def test_run_ifc2x3(self): ifc = ifcopenshell.file(schema="IFC2X3") - document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION") - result = subject.get_document_uri(document) - assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg") + tool.Ifc.set(ifc) + reference = ifc.createIfcDocumentReference(Location="Location") + document = ifc.createIfcDocumentInformation( + DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION", 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")) class TestGetDrawingCollection(NewFile): @@ -311,23 +325,6 @@ class TestGetName(NewFile): assert subject.get_name(ifc.createIfcWall(Name="Foobar")) == "Foobar" -class TestGetScheduleLocation(NewFile): - def test_run(self): - ifc = ifcopenshell.file() - ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") - schedule = ifcopenshell.api.run("document.add_information", ifc) - schedule.Location = "uri" - reference = ifcopenshell.api.run("document.add_reference", ifc, information=schedule) - subject.get_schedule_location(schedule) == "uri" - - def test_run_ifc2x3(self): - ifc = ifcopenshell.file(schema="IFC2X3") - tool.Ifc.set(ifc) - reference = ifc.createIfcDocumentReference(Location="uri") - schedule = ifc.createIfcDocumentInformation(DocumentReferences=[reference]) - subject.get_schedule_location(schedule) == "uri" - - class TestGenerateDrawingMatrix(NewFile): def test_returning_the_origin_as_a_fallback(self): assert subject.generate_drawing_matrix("PLAN_VIEW", None) == mathutils.Matrix()