From bddf423e6c15fc6b462dbc4fb38f7ee3dc70c2c5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 1 Apr 2024 14:03:22 +0500 Subject: [PATCH] experimental svg to dxf conversion for drawings Example - https://imgur.com/a/AkvLlbZ --- src/blenderbim/Makefile | 2 + .../blenderbim/bim/module/drawing/__init__.py | 1 + .../blenderbim/bim/module/drawing/operator.py | 49 +++++++++++++- .../blenderbim/bim/module/drawing/ui.py | 5 ++ src/blenderbim/blenderbim/tool/drawing.py | 67 +++++++++++++++++-- 5 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 0cf98aef3c..2294594eeb 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -363,6 +363,8 @@ endif cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages # Provides Brickschema functionality cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages + # Required for SVG to DXF conversion + cd dist/working && . env/bin/activate && $(PIP) install "ezxdf" --target=./site-packages cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/ rm -rf dist/working diff --git a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py index 61af897a73..643955fee5 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.CleanWireframes, operator.ContractSheet, operator.ContractTargetView, + operator.ConvertSVGToDXF, operator.CreateDrawing, operator.CreateSheets, operator.DisableAddAnnotationType, diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index c918f94982..806fe810e8 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -44,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper import blenderbim.bim.export_ifc from blenderbim.bim.module.drawing.decoration import CutDecorator from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData -from typing import NamedTuple, List +from typing import NamedTuple, List, Union from lxml import etree from mathutils import Vector, Color, Matrix from timeit import default_timer as timer @@ -2685,3 +2685,50 @@ class AddReferenceImage(bpy.types.Operator, Operator): ) tool.Style.reload_material_from_ifc(material) tool.Geometry.record_object_materials(obj) + + +class ConvertSVGToDXF(bpy.types.Operator): + bl_idname = "bim.convert_svg_to_dxf" + bl_label = "Convert SVG to DXF" + bl_options = {"REGISTER", "UNDO"} + view: bpy.props.StringProperty() + bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings" + convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + # convert all drawings on shift+click + # make sure to use SKIP_SAVE on property, otherwise it might get stuck + if event.type == "LEFTMOUSE" and event.shift: + self.open_all = True + return self.execute(context) + + def execute(self, context): + if self.convert_all: + drawings = [ + tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected + ] + else: + drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] + + drawing_uris: list[Path] = [] + drawings_not_found: list[str] = [] + + for drawing in drawings: + drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing)) + if drawing_uri is None or not os.path.exists(drawing_uri): + drawings_not_found.append(drawing.Name) + else: + drawing_uris.append(Path(drawing_uri)) + + if drawings_not_found: + msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format( + "\n".join(drawings_not_found) + ) + self.report({"ERROR"}, msg) + return {"CANCELLED"} + + for drawing_uri in drawing_uris: + tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf")) + + self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.") + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 2ccdb9ae8c..33a40450fd 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel): col = row.column() col.alignment = "RIGHT" + convert_to_dxf = row.row(align=True) + op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA") + op.view = active_drawing.name + convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0 + op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="") open_drawing_button = row.row(align=True) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 4543049b34..0aa39bc0ec 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -27,7 +27,6 @@ import shutil import logging import shapely import platform -from shapely.ops import unary_union import mathutils import subprocess import numpy as np @@ -43,6 +42,7 @@ import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.helper as helper +from shapely.ops import unary_union from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS, ANNOTATION_TYPES_DATA from lxml import etree @@ -50,6 +50,7 @@ from mathutils import Vector, Matrix from fractions import Fraction import collections from typing import Optional, Union +from pathlib import Path class Drawing(blenderbim.core.tool.Drawing): @@ -385,7 +386,9 @@ 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, description=None): + def get_document_uri( + cls, document: ifcopenshell.entity_instance, description: Optional[str] = None + ) -> Union[str, None]: if getattr(document, "Location", None): if os.path.isabs(document.Location): return document.Location @@ -430,7 +433,7 @@ class Drawing(blenderbim.core.tool.Drawing): return rel.RelatingGroup @classmethod - def get_drawing_document(cls, drawing): + def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: for rel in drawing.HasAssociations: if rel.is_a("IfcRelAssociatesDocument"): return rel.RelatingDocument @@ -1103,7 +1106,6 @@ class Drawing(blenderbim.core.tool.Drawing): reference_element: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance, ) -> ifcopenshell.entity_instance: - if reference_element.is_a("IfcGridAxis"): return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context) @@ -1991,3 +1993,60 @@ class Drawing(blenderbim.core.tool.Drawing): rotate180z = mathutils.Matrix.Rotation(math.radians(180.0), 4, "Z") return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() @ rotate180z return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() + + @classmethod + def convert_svg_to_dxf(cls, svg_filepath: Path, dxf_filepath: Path) -> None: + import ezdxf + import xml.etree.ElementTree as ET + + SVG = "{http://www.w3.org/2000/svg}" + IFC = "{http://www.ifcopenshell.org/ns}" + + doc = ezdxf.new("R2010") + msp = doc.modelspace() + svg = ET.parse(svg_filepath).getroot() + + def finalize_dxf(): + doc.saveas(dxf_filepath) + + drawing = svg.findall(f"{SVG}g[@{IFC}name]") + if not drawing: + finalize_dxf() + return + drawing = drawing[0] + + NUMBER = r"-?\d+\.?\d+" + COORD = rf"{NUMBER},{NUMBER}" + POLYLINE_PATTERN = rf"M{COORD} (?:L{COORD} ?)+Z? ?" + MULTI_POLYLINE_PATTERN = rf"^({POLYLINE_PATTERN})+$" + + for element_g in drawing.findall(f"{SVG}g"): + paths = element_g.findall(f"{SVG}path") + + for path in paths: + path = path.attrib["d"] + + if not re.match(MULTI_POLYLINE_PATTERN, path): + # print(f'Path "{path}" doesn\'t match expected pattern {MULTI_POLYLINE_PATTERN}') + continue + + for polyline_path in re.findall(POLYLINE_PATTERN, path): + points = re.findall(rf"{NUMBER}", polyline_path) + points = [float(p) for p in points] + POINT_SIZE = 2 + + grouped_points = [] + for i in range(0, len(points), POINT_SIZE): + point = points[i : i + POINT_SIZE] + point[1] *= -1 + grouped_points.append(point) + points = grouped_points + + # Z marks closed polylines + is_closed_polyline = polyline_path.rstrip().endswith("Z") + if is_closed_polyline or len(points) > 2: + msp.add_lwpolyline(points, close=is_closed_polyline) + else: # LINE + msp.add_line(*points) + + finalize_dxf()