From bcdfcce95c9b34a2053927663069f4ddb329dec4 Mon Sep 17 00:00:00 2001 From: JULIEN MASNADA Date: Fri, 20 Dec 2024 09:46:39 +0100 Subject: [PATCH] Improved SweetHome 3D importer (#17165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed access to Addon::Metadat::Url attributes * Fixed invalid vector in distance calculation * SH3D importer initial version * Cleaned up and added baseboard * Make sure notificationWidth is properly enabled/disabled * Added furnitureGroup, color prefs, light weight mesh object * Allow to join walls * Prepare to join wall, improved status feedback * Removing trailing white space * SH3D importer initial version * Cleaned up and added baseboard * Make sure notificationWidth is properly enabled/disabled * Added furnitureGroup, color prefs, light weight mesh object * Allow to join walls * Prepare to join wall, improved status feedback * Removing trailing white space * fixing tipo, 80 charlines, etc * Adding a basic import test * Work in local but fails on pipeline. Commenting out. * Adding testcase and join wall path * Use ruled surface to fix failed sweep * Fixed faces order when joining walls * Fixed missing sample importer file * Allow to change pref just before import * Fixed excessive debug output * Allow to import from string. Test use embedded string * Fixed tipo in comment Co-authored-by: João Matos * Improved door import Also added coloring for wall section's edges when debuging * Moved debug init script to FreeCAD-Docker repo --------- Co-authored-by: João Matos Co-authored-by: Yorik van Havre --- src/Mod/BIM/CMakeLists.txt | 16 + src/Mod/BIM/Init.py | 2 +- src/Mod/BIM/InitGui.py | 1 + src/Mod/BIM/Resources/Arch.qrc | 1 + .../Resources/ui/preferences-sh3d-import.ui | 337 ++++ src/Mod/BIM/TestArch.py | 85 +- src/Mod/BIM/importers/importSH3D.py | 157 +- src/Mod/BIM/importers/importSH3DHelper.py | 1711 +++++++++++++++++ src/Mod/BIM/importers/samples/Sample.sh3d | Bin 0 -> 87826 bytes src/Mod/Draft/draftgeoutils/arcs.py | 18 +- src/Mod/Draft/draftgeoutils/general.py | 13 +- src/Mod/Draft/draftgeoutils/intersections.py | 37 +- src/Mod/Draft/draftgeoutils/offsets.py | 15 + src/Mod/Draft/draftutils/params.py | 3 +- 14 files changed, 2247 insertions(+), 149 deletions(-) create mode 100644 src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui create mode 100644 src/Mod/BIM/importers/importSH3DHelper.py create mode 100644 src/Mod/BIM/importers/samples/Sample.sh3d diff --git a/src/Mod/BIM/CMakeLists.txt b/src/Mod/BIM/CMakeLists.txt index 1e50d4be66..1607425e36 100644 --- a/src/Mod/BIM/CMakeLists.txt +++ b/src/Mod/BIM/CMakeLists.txt @@ -59,12 +59,14 @@ SET(importers_SRCS importers/importWebGL.py importers/importJSON.py importers/importSH3D.py + importers/importSH3DHelper.py importers/import3DS.py importers/importSHP.py importers/importGBXML.py importers/exportIFCStructuralTools.py importers/exportIFC.py importers/exportIFCHelper.py + importers/samples/Sample.sh3d ) SET(Dice3DS_SRCS @@ -198,6 +200,10 @@ SET(BIMGuiIcon_SVG Resources/icons/BIMWorkbench.svg ) +SET(ImportersSample_Files + importers/samples/Sample.sh3d +) + ADD_CUSTOM_TARGET(BIM ALL SOURCES ${Arch_SRCS} ${Arch_QRC_SRCS} @@ -210,6 +216,10 @@ ADD_CUSTOM_TARGET(BIM ALL ${BIMGuiIcon_SVG} ) +ADD_CUSTOM_TARGET(ImporterPythonTestData ALL + SOURCES ${ImportersSample_Files} +) + fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/Mod/BIM" ${Arch_SRCS}) fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/Mod/BIM" ${Dice3DS_SRCS}) fc_copy_sources(BIM "${CMAKE_BINARY_DIR}/Mod/BIM" ${importers_SRCS}) @@ -223,6 +233,12 @@ fc_target_copy_resource(BIM ${Arch_presets} ) +fc_target_copy_resource(ImporterPythonTestData + ${CMAKE_SOURCE_DIR}/src/Mod/BIM + ${CMAKE_BINARY_DIR}/Mod/BIM + ${ImportersSample_Files}) + + IF (BUILD_GUI) fc_target_copy_resource(BIM ${CMAKE_CURRENT_BINARY_DIR} diff --git a/src/Mod/BIM/Init.py b/src/Mod/BIM/Init.py index 10f29ae49f..ee23f69fdf 100644 --- a/src/Mod/BIM/Init.py +++ b/src/Mod/BIM/Init.py @@ -33,5 +33,5 @@ FreeCAD.addExportType("JavaScript Object Notation (*.json)","importers.importJSO FreeCAD.addImportType("Collada (*.dae *.DAE)","importers.importDAE") FreeCAD.addExportType("Collada (*.dae)","importers.importDAE") FreeCAD.addImportType("3D Studio mesh (*.3ds *.3DS)","importers.import3DS") -FreeCAD.addImportType("SweetHome3D XML export (*.zip *.ZIP)","importers.importSH3D") +FreeCAD.addImportType("SweetHome3D (*.sh3d)","importers.importSH3D") FreeCAD.addImportType("Shapefile (*.shp *.SHP)","importers.importSHP") diff --git a/src/Mod/BIM/InitGui.py b/src/Mod/BIM/InitGui.py index fb104f377b..2add05b491 100644 --- a/src/Mod/BIM/InitGui.py +++ b/src/Mod/BIM/InitGui.py @@ -677,6 +677,7 @@ t = QT_TRANSLATE_NOOP("QObject", "Import-Export") FreeCADGui.addPreferencePage(":/ui/preferences-ifc.ui", t) FreeCADGui.addPreferencePage(":/ui/preferences-ifc-export.ui", t) FreeCADGui.addPreferencePage(":/ui/preferences-dae.ui", t) +FreeCADGui.addPreferencePage(":/ui/preferences-sh3d-import.ui", t) # Add unit tests FreeCAD.__unit_test__ += ["TestArch"] diff --git a/src/Mod/BIM/Resources/Arch.qrc b/src/Mod/BIM/Resources/Arch.qrc index 04568cfbba..ec428c2d43 100644 --- a/src/Mod/BIM/Resources/Arch.qrc +++ b/src/Mod/BIM/Resources/Arch.qrc @@ -268,6 +268,7 @@ ui/preferences-dae.ui ui/preferences-ifc-export.ui ui/preferences-ifc.ui + ui/preferences-sh3d-import.ui ui/preferencesNativeIFC.ui diff --git a/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui b/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui new file mode 100644 index 0000000000..79f891e788 --- /dev/null +++ b/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui @@ -0,0 +1,337 @@ + + + Gui::Dialog::DlgSettingsArch + + + + 0 + 0 + 555 + 729 + + + + SH3D import + + + + 6 + + + 9 + + + + + General options + + + + + + Show this dialog when importing + + + sh3dShowDialog + + + Mod/Arch + + + + + + + Shows verbose debug messages during import of SH3D files in the Report + view panel. Log level message must be allowed for this setting to have an effect. + + + Show debug messages + + + sh3dDebug + + + Mod/Arch + + + + + + + + + + Import options + + + + + + Whether to import the model's doors and windows + + + Doors and Windows + + + sh3dImportDoorsAndWindows + + + Mod/Arch + + + + + + + Whether to import the model's furnitures + + + Furnitures + + + sh3dImportFurnitures + + + Mod/Arch + + + + + + + Whether to create Arch::Equipment for each furniture defined in the model (NOTE: this can negatively impact the import process speed) + + + Create Arch::Equipment + + + sh3dCreateArchEquipment + + + Mod/Arch + + + + + + + Whether to join the different Arch::Wall together + + + Join Arch::Wall + + + sh3dJoinArchWall + + + Mod/Arch + + + + + + + Whether to import the model's lights. Note that you also need to import + the model's furnitures. + + + Lights (requires Render) + + + sh3dImportLights + + + Mod/Arch + + + + + + + Whether to import the model's cameras + + + Cameras (requires Render) + + + sh3dImportCameras + + + Mod/Arch + + + + + + + Merge imported element with existing FC object + + + Merge into existing document + + + sh3dMerge + + + Mod/Arch + + + + + + + + + Default Floor Color + + + sh3dDefaultFloorColor + + + + + + + + 0 + 0 + + + + This color might be used when a room does not define its own color. + + + + 150 + 169 + 186 + + + + sh3dDefaultFloorColor + + + Mod/Arch + + + + + + + + + + + Default Ceiling Color + + + sh3dDefaultCeilingColor + + + + + + + + 0 + 0 + + + + This color might be used when a room does not define its own color. + + + + 255 + 255 + 255 + + + + sh3dDefaultCeilingColor + + + Mod/Arch + + + + + + + + + Create a default Render project with the newly Site + + + Create Render Project (requires Render) + + + sh3dCreateRenderProject + + + Mod/Arch + + + + + + + Fit view while importing. + + + Fit view while importing + + + sh3dFitView + + + Mod/Arch + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + qPixmapFromMimeSource + + + Gui::PrefSpinBox + QSpinBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefComboBox + QComboBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefLineEdit + QLineEdit +
Gui/PrefWidgets.h
+
+
+ + +
\ No newline at end of file diff --git a/src/Mod/BIM/TestArch.py b/src/Mod/BIM/TestArch.py index 27bbb9bff2..a8042084b1 100644 --- a/src/Mod/BIM/TestArch.py +++ b/src/Mod/BIM/TestArch.py @@ -759,6 +759,20 @@ class ArchTest(unittest.TestCase): App.ActiveDocument.recompute() assert wall.Visibility + def testImportSH3D(self): + """Import a SweetHome 3D file + """ + operation = "importers.importSH3D" + _msg(" Test '{}'".format(operation)) + import BIM.importers.importSH3DHelper + importer = BIM.importers.importSH3DHelper.SH3DImporter(None) + importer.import_sh3d_from_string(SH3D_HOME) + assert App.ActiveDocument.Project + assert App.ActiveDocument.Site + assert App.ActiveDocument.BuildingPart.Label == "Building" + assert App.ActiveDocument.BuildingPart001.Label == "Level" + assert App.ActiveDocument.Wall + def testViewGeneration(self): """Tests the whole TD view generation workflow""" @@ -804,8 +818,77 @@ class ArchTest(unittest.TestCase): view.Y = "15cm" App.ActiveDocument.recompute() assert True - def tearDown(self): App.closeDocument("ArchTest") pass + + +SH3D_HOME = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" diff --git a/src/Mod/BIM/importers/importSH3D.py b/src/Mod/BIM/importers/importSH3D.py index 01f8ef282b..5a1cc189e5 100644 --- a/src/Mod/BIM/importers/importSH3D.py +++ b/src/Mod/BIM/importers/importSH3D.py @@ -23,18 +23,13 @@ __title__ = "FreeCAD SweetHome3D Importer" __author__ = "Yorik van Havre" __url__ = "https://www.freecad.org" -import math import os -import tempfile import xml.sax import zipfile -from builtins import open as pyopen import FreeCAD -import Arch -import Draft -import Mesh -import Part +from FreeCAD import Base + ## @package importSH3D # \ingroup ARCH @@ -69,140 +64,16 @@ def insert(filename,docname): def read(filename): "reads the file and creates objects in the active document" - z = zipfile.ZipFile(filename) - homexml = z.read("Home.xml") - handler = SH3DHandler(z) - xml.sax.parseString(homexml,handler) + import BIM.importers.importSH3DHelper + if DEBUG: + from importlib import reload + reload(BIM.importers.importSH3DHelper) + + pi = Base.ProgressIndicator() + try: + importer = BIM.importers.importSH3DHelper.SH3DImporter(pi) + importer.import_sh3d_from_filename(filename) + finally: + pi.stop() + FreeCAD.ActiveDocument.recompute() - if not handler.makeIndividualWalls: - delete = [] - walls = [] - for k,lines in handler.lines.items(): - sk = FreeCAD.ActiveDocument.addObject("Sketcher::SketchObject","Walls_trace") - for l in lines: - for edge in l.Shape.Edges: - sk.addGeometry(edge.Curve) - delete.append(l.Name) - FreeCAD.ActiveDocument.recompute() - k = k.split(";") - walls.append(Arch.makeWall(baseobj=sk,width=float(k[0]),height=float(k[1]))) - for d in delete: - FreeCAD.ActiveDocument.removeObject(d) - w = walls.pop() - w.Additions = walls - w.Subtractions = handler.windows - g = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup","Furniture") - g.Group = handler.furniture - FreeCAD.ActiveDocument.recompute() - - -class SH3DHandler(xml.sax.ContentHandler): - - def __init__(self,z): - - super().__init__() - self.makeIndividualWalls = False - self.z = z - self.windows = [] - self.furniture = [] - self.lines = {} - - def startElement(self, tag, attributes): - - if tag == "wall": - name = attributes["id"] - p1 = FreeCAD.Vector(float(attributes["xStart"])*10,float(attributes["yStart"])*10,0) - p2 = FreeCAD.Vector(float(attributes["xEnd"])*10,float(attributes["yEnd"])*10,0) - height = float(attributes["height"])*10 - thickness = float(attributes["thickness"])*10 - if DEBUG: print("Creating wall: ",name) - line = Draft.makeLine(p1,p2) - if self.makeIndividualWalls: - wall = Arch.makeWall(baseobj=line,width=thickness,height=height,name=name) - wall.Label = name - else: - self.lines.setdefault(str(thickness)+";"+str(height),[]).append(line) - - elif tag == "pieceOfFurniture": - name = attributes["name"] - data = self.z.read(attributes["model"]) - th,tf = tempfile.mkstemp(suffix=".obj") - f = pyopen(tf,"wb") - f.write(data) - f.close() - os.close(th) - m = Mesh.read(tf) - fx = (float(attributes["width"])/100)/m.BoundBox.XLength - fy = (float(attributes["height"])/100)/m.BoundBox.YLength - fz = (float(attributes["depth"])/100)/m.BoundBox.ZLength - mat = FreeCAD.Matrix() - mat.scale(1000*fx,1000*fy,1000*fz) - mat.rotateX(math.pi/2) - mat.rotateZ(math.pi) - if DEBUG: print("Creating furniture: ",name) - if "angle" in attributes: - mat.rotateZ(float(attributes["angle"])) - m.transform(mat) - os.remove(tf) - p = m.BoundBox.Center.negative() - p = p.add(FreeCAD.Vector(float(attributes["x"])*10,float(attributes["y"])*10,0)) - p = p.add(FreeCAD.Vector(0,0,m.BoundBox.Center.z-m.BoundBox.ZMin)) - m.Placement.Base = p - obj = FreeCAD.ActiveDocument.addObject("Mesh::Feature",name) - obj.Mesh = m - self.furniture.append(obj) - - elif tag == "doorOrWindow": - name = attributes["name"] - data = self.z.read(attributes["model"]) - th,tf = tempfile.mkstemp(suffix=".obj") - f = pyopen(tf,"wb") - f.write(data) - f.close() - os.close(th) - m = Mesh.read(tf) - fx = (float(attributes["width"])/100)/m.BoundBox.XLength - fy = (float(attributes["height"])/100)/m.BoundBox.YLength - fz = (float(attributes["depth"])/100)/m.BoundBox.ZLength - mat = FreeCAD.Matrix() - mat.scale(1000*fx,1000*fy,1000*fz) - mat.rotateX(math.pi/2) - m.transform(mat) - b = m.BoundBox - v1 = FreeCAD.Vector(b.XMin,b.YMin-500,b.ZMin) - v2 = FreeCAD.Vector(b.XMax,b.YMin-500,b.ZMin) - v3 = FreeCAD.Vector(b.XMax,b.YMax+500,b.ZMin) - v4 = FreeCAD.Vector(b.XMin,b.YMax+500,b.ZMin) - sub = Part.makePolygon([v1,v2,v3,v4,v1]) - sub = Part.Face(sub) - sub = sub.extrude(FreeCAD.Vector(0,0,b.ZLength)) - os.remove(tf) - shape = Arch.getShapeFromMesh(m) - if not shape: - shape=Part.Shape() - shape.makeShapeFromMesh(m.Topology,0.100000) - shape = shape.removeSplitter() - if shape: - if DEBUG: print("Creating window: ",name) - if "angle" in attributes: - shape.rotate(shape.BoundBox.Center,FreeCAD.Vector(0,0,1),math.degrees(float(attributes["angle"]))) - sub.rotate(shape.BoundBox.Center,FreeCAD.Vector(0,0,1),math.degrees(float(attributes["angle"]))) - p = shape.BoundBox.Center.negative() - p = p.add(FreeCAD.Vector(float(attributes["x"])*10,float(attributes["y"])*10,0)) - p = p.add(FreeCAD.Vector(0,0,shape.BoundBox.Center.z-shape.BoundBox.ZMin)) - if "elevation" in attributes: - p = p.add(FreeCAD.Vector(0,0,float(attributes["elevation"])*10)) - shape.translate(p) - sub.translate(p) - obj = FreeCAD.ActiveDocument.addObject("Part::Feature",name+"_body") - obj.Shape = shape - subobj = FreeCAD.ActiveDocument.addObject("Part::Feature",name+"_sub") - subobj.Shape = sub - if FreeCAD.GuiUp: - subobj.ViewObject.hide() - win = Arch.makeWindow(baseobj=obj,name=name) - win.Label = name - win.Subvolume = subobj - self.windows.append(win) - else: - print("importSH3D: Error creating shape for door/window "+name) diff --git a/src/Mod/BIM/importers/importSH3DHelper.py b/src/Mod/BIM/importers/importSH3DHelper.py new file mode 100644 index 0000000000..4d982fcfc7 --- /dev/null +++ b/src/Mod/BIM/importers/importSH3DHelper.py @@ -0,0 +1,1711 @@ +# *************************************************************************** +# * Copyright (c) 2024 Julien Masnada * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Helper functions that are used by SH3D importer.""" +import math +import os +import re +import uuid +import xml.etree.ElementTree as ET +import zipfile + +import Arch +import Draft +import DraftGeomUtils +import DraftVecUtils +import draftutils.gui_utils as gui_utils +import Mesh +import numpy +import Part +from draftutils.messages import _err, _log, _msg, _wrn +from draftutils.params import get_param_arch + +import FreeCAD as App + +if App.GuiUp: + import FreeCADGui as Gui + from draftutils.translate import translate +else: + # \cond + def translate(_, text): + return text + # \endcond + +# Used to make section edges more visible (https://coolors.co/5bc0eb-fde74c-9bc53d-e55934-fa7921) +DEBUG_EDGES_COLORS = ["5bc0eb", "fde74c", "9bc53d", "e55934", "fa7921"] +DEBUG_POINT_COLORS = ["011627", "ff0022", "41ead4", "fdfffc", "b91372"] + +try: + from Render import Camera, PointLight + from Render.project import Project + RENDER_IS_AVAILABLE = True +except : + RENDER_IS_AVAILABLE = False + +# Sometimes, the Part::Sweep creates a "twisted" sweep that +# impeeds the creation of the corresponding wall. +FIX_INVALID_SWEEP = False + +# SweetHome3D is in cm while FreeCAD is in mm +FACTOR = 10 +DEFAULT_WALL_WIDTH = 100 +TOLERANCE = float(.1) + +ORIGIN = App.Vector(0, 0, 0) +X_NORM = App.Vector(1, 0, 0) +Y_NORM = App.Vector(0, 1, 0) +Z_NORM = App.Vector(0, 0, 1) + +# The Windows lookup map. This is really brittle and a better system should +# be found. Arch.WindowPresets = ["Fixed", "Open 1-pane", "Open 2-pane", +# "Sash 2-pane", "Sliding 2-pane", "Simple door", "Glass door", +# "Sliding 4-pane", "Awning"] +# unzip -p all-windows.sh3d Home.xml | \ +# grep 'catalogId=' | \ +# sed -e 's/.*catalogId=//;s/ name=.*/: ("Open 2-pane","Window"),/' | sort -u +# unzip -p all-doors.sh3d Home.xml | \ +# grep 'catalogId=' | \ +# sed -e 's/.*catalogId=//;s/ name=.*/: ("Simple door","Door")/' | sort -u +DOOR_MODELS = { + 'eTeks#doorFrame': ("Opening only", "Opening Element"), + 'eTeks#door': ("Simple door","Door"), + 'eTeks#frontDoor': ("Simple door","Door"), + 'eTeks#garageDoor': ("Simple door","Door"), + 'eTeks#openDoor': ("Simple door","Door"), + 'eTeks#roundDoorFrame': ("Opening only", "Opening Element"), + 'eTeks#roundedDoor': ("Simple door","Door"), + 'Kator Legaz#exterior-door-01': ("Simple door","Door"), + 'Kator Legaz#exterior-door-02': ("Simple door","Door"), + 'Kator Legaz#exterior-door-03': ("Glass door","Door"), + 'Kator Legaz#exterior-door-05': ("Simple door","Door"), + 'Kator Legaz#exterior-door-07': ("Glass door","Door"), + 'Kator Legaz#screen-door': ("Simple door","Door"), + 'Scopia#door': ("Simple door","Door"), + 'Scopia#double_door_2': ("Simple door","Door"), + 'Scopia#double_door': ("Glass door","Door"), + 'Scopia#double_door_with_little_part': ("Glass door","Door"), + 'Scopia#elevator-door': ("Simple door","Door"), + 'Scopia#garage-door2': ("Simple door","Door"), + 'Scopia#garage-door': ("Simple door","Door"), + 'Scopia#glassDoor2': ("Glass door","Door"), + 'Scopia#glass_door': ("Glass door","Door"), + 'Scopia#puerta': ("Simple door","Door"), + + 'eTeks#doubleFrenchWindow126x200': ("Open 2-pane","Window"), + 'eTeks#doubleHungWindow80x122': ("Open 2-pane","Window"), + 'eTeks#doubleOutwardOpeningWindow': ("Open 2-pane","Window"), + 'eTeks#doubleWindow126x123': ("Open 2-pane","Window"), + 'eTeks#doubleWindow126x163': ("Open 2-pane","Window"), + 'eTeks#fixedTriangleWindow85x85': ("Open 2-pane","Window"), + 'eTeks#fixedWindow85x123': ("Open 2-pane","Window"), + 'eTeks#frenchWindow85x200': ("Open 2-pane","Window"), + 'eTeks#halfRoundWindow': ("Open 2-pane","Window"), + 'eTeks#roundWindow': ("Open 2-pane","Window"), + 'eTeks#sliderWindow126x200': ("Open 2-pane","Window"), + 'eTeks#window85x123': ("Open 2-pane","Window"), + 'eTeks#window85x163': ("Open 2-pane","Window"), + 'Kator Legaz#window-01': ("Open 2-pane","Window"), + 'Kator Legaz#window-08-02': ("Open 2-pane","Window"), + 'Kator Legaz#window-08': ("Open 2-pane","Window"), + 'Scopia#turn-window': ("Open 2-pane","Window"), + 'Scopia#window_2x1_medium_with_large_pane': ("Open 2-pane","Window"), + 'Scopia#window_2x1_with_sliders': ("Open 2-pane","Window"), + 'Scopia#window_2x3_arched': ("Open 2-pane","Window"), + 'Scopia#window_2x3': ("Open 2-pane","Window"), + 'Scopia#window_2x3_regular': ("Open 2-pane","Window"), + 'Scopia#window_2x4_arched': ("Open 2-pane","Window"), + 'Scopia#window_2x4': ("Open 2-pane","Window"), + 'Scopia#window_2x6': ("Open 2-pane","Window"), + 'Scopia#window_3x1': ("Open 2-pane","Window"), + 'Scopia#window_4x1': ("Open 2-pane","Window"), + 'Scopia#window_4x3_arched': ("Open 2-pane","Window"), + 'Scopia#window_4x3': ("Open 2-pane","Window"), + 'Scopia#window_4x5': ("Open 2-pane","Window"), + +} + +class SH3DImporter: + """The main class to import an SH3D file. + + As an implementation detail, note that we do not use an + xml.sax parser as the XML elements found in the SH3D file + do not follow a natural / dependency order (i.e. doors and + windows depend upon wall but are usually defined *before* + the different elements) + """ + + def __init__(self, progress_bar=None): + """Create a SH3DImporter instance to import the given SH3D file. + + Args: + progress_bar (ProgressIndicator,optional): a ProgressIndicator + called to let the User monitor the import process + """ + super().__init__() + self.filename = None + self.progress_bar = progress_bar + self.preferences = {} + self.handlers = {} + self.total_object_count = 0 + self.current_object_count = 0 + self.zip = None + self.fc_objects = {} + self.project = None + self.site = None + self.building = None + self.default_floor = None + self.floors = {} + self.walls = [] + + def import_sh3d_from_string(self, home:str): + """Import the SH3D Home from a String. + + Args: + home (str): the string containing the XML of the home + to be imported. + + Raises: + ValueError: if an invalid SH3D file is detected + """ + self._get_preferences() + self._setup_handlers() + + if self.progress_bar: + self.progress_bar.start(f"Importing SweetHome 3D Home. Please wait ...", -1) + self._import_home(ET.fromstring(home)) + + def import_sh3d_from_filename(self, filename:str): + """Import the SH3D file. + + Args: + filename (str): the filename of the SH3D file to be imported. + + Raises: + ValueError: if an invalid SH3D file is detected + """ + self.filename = filename + if App.GuiUp and get_param_arch("sh3dShowDialog"): + Gui.showPreferences("Import-Export", 7) + + self._get_preferences() + self._setup_handlers() + + if self.progress_bar: + self.progress_bar.start(f"Importing SweetHome 3D file '{self.filename}'. Please wait ...", -1) + with zipfile.ZipFile(self.filename, 'r') as zip: + self.zip = zip + entries = zip.namelist() + if "Home.xml" not in entries: + raise ValueError(f"Invalid SweetHome3D file {self.filename}: missing Home.xml") + self._import_home(ET.fromstring(zip.read("Home.xml"))) + + def _import_home(self, home): + doc = App.ActiveDocument + self.total_object_count = self._get_object_count(home) + _msg(f"Importing home '{home.get('name')}' ...") + # Create the groups to organize the different resources together + self._create_groups() + + # Get all the FreeCAD object in the active doc, in order to allow + # for merge of existing object + if self.preferences["MERGE"]: + for object in doc.Objects: + if hasattr(object, 'id'): + self.fc_objects[object.id] = object + + # Let's create the project and site for this import + self._setup_project(home) + + # Import the element if any. If none are defined + # create a default one. + if home.find(path='level') != None: + self._import_elements(home, 'level') + else: + # Has the default floor already been created from a + # previous import? + if self.preferences["DEBUG"]: _log("No level defined. Using default level ...") + self.default_floor = self.fc_objects.get('Level') if 'Level' in self.fc_objects else self._create_default_floor() + self.add_floor(self.default_floor) + + # Importing elements ... + self._import_elements(home, 'room') + + # Importing elements ... + self._import_elements(home, 'wall') + + self._refresh() + if App.GuiUp and self.preferences["FIT_VIEW"]: + Gui.SendMsgToActiveView("ViewFit") + + # Importing elements ... + if self.preferences["IMPORT_DOORS_AND_WINDOWS"]: + self._import_elements(home, 'doorOrWindow') + self._refresh() + + # Importing && elements ... + if self.preferences["IMPORT_FURNITURES"]: + self._import_elements(home, 'pieceOfFurniture') + for furniture_group in home.findall('furnitureGroup'): + self._import_elements(furniture_group, 'pieceOfFurniture', False) + self._refresh() + + # Importing elements ... + if self.preferences["IMPORT_LIGHTS"]: + self._import_elements(home, 'light') + self._refresh() + + # Importing elements ... + if self.preferences["IMPORT_CAMERAS"]: + self._import_elements(home, 'observerCamera') + self._import_elements(home, 'camera') + self._refresh() + + if self.preferences["CREATE_RENDER_PROJECT"] and self.project: + Project.create(doc, renderer="Povray", template="povray_standard.pov") + Gui.Selection.clearSelection() + Gui.Selection.addSelection(self.project) + Gui.runCommand('Render_View', 0) + self._refresh() + + _msg(f"Successfully imported home '{home.get('name')}' ...") + + + def _get_object_count(self, home): + """Get an approximate count of object to be imported + """ + count = 0 + for tag in self.handlers.keys(): + count = count + len(list(home.findall(tag))) + return count + + def _get_preferences(self): + """Retrieve the SH3D preferences available in Mod/Arch.""" + self.preferences = { + 'DEBUG': get_param_arch("sh3dDebug"), + 'IMPORT_DOORS_AND_WINDOWS': get_param_arch("sh3dImportDoorsAndWindows"), + 'IMPORT_FURNITURES': get_param_arch("sh3dImportFurnitures"), + 'IMPORT_LIGHTS': get_param_arch("sh3dImportLights") and RENDER_IS_AVAILABLE, + 'IMPORT_CAMERAS': get_param_arch("sh3dImportCameras") and RENDER_IS_AVAILABLE, + 'MERGE': get_param_arch("sh3dMerge"), + 'CREATE_ARCH_EQUIPMENT': get_param_arch("sh3dCreateArchEquipment"), + 'JOIN_ARCH_WALL': get_param_arch("sh3dJoinArchWall"), + 'CREATE_RENDER_PROJECT': get_param_arch("sh3dCreateRenderProject") and RENDER_IS_AVAILABLE, + 'FIT_VIEW': get_param_arch("sh3dFitView"), + 'DEFAULT_FLOOR_COLOR': color_fc2sh(get_param_arch("sh3dDefaultFloorColor")), + 'DEFAULT_CEILING_COLOR': color_fc2sh(get_param_arch("sh3dDefaultCeilingColor")), + } + + def _setup_handlers(self): + self.handlers = { + 'level': LevelHandler(self), + 'room': RoomHandler(self), + 'wall': WallHandler(self), + } + if self.preferences["IMPORT_DOORS_AND_WINDOWS"]: + self.handlers['doorOrWindow'] = DoorOrWindowHandler(self) + + if self.preferences["IMPORT_FURNITURES"]: + self.handlers['pieceOfFurniture'] = FurnitureHandler(self) + self.handlers['furnitureGroup'] = None + + if self.preferences["IMPORT_LIGHTS"]: + self.handlers['light'] = LightHandler(self) + + if self.preferences["IMPORT_CAMERAS"]: + camera_handler = CameraHandler(self) + self.handlers['observerCamera'] = camera_handler + self.handlers['camera'] = camera_handler + + def _refresh(self): + App.ActiveDocument.recompute() + if App.GuiUp: + Gui.updateGui() + + def set_property(self, obj, type_, name, description, value, valid_values=None): + """Set the attribute of the given object as an FC property + + Note that the method has a default behavior when the value is not specified. + + Args: + obj (object): The FC object to add a property to + type_ (str): the type of property to add + name (str): the name of the property to add + description (str): a short description of the property to add + value (xml.etree.ElementTree.Element|str): The property's value. Defaults to None. + valid_values (list): an optional list of valid values + """ + + self._add_property(obj, type_, name, description) + if valid_values: + setattr(obj, name, valid_values) + if value is None: + if self.preferences["DEBUG"]:_log(f"Setting obj.{name}=None") + return + if type(value) is ET.Element: + if type_ == "App::PropertyString": + value = str(value.get(name, "")) + elif type_ == "App::PropertyFloat": + value = float(value.get(name, 0)) + elif type_ == "App::PropertyInteger": + value = int(value.get(name, 0)) + elif type_ == "App::PropertyBool": + value = bool(value.get(name, True)) + if self.preferences["DEBUG"]: + _log(f"Setting @{obj}.{name} = {value}") + setattr(obj, name, value) + + def _add_property(self, obj, property_type, name, description): + """Add an property to the FC object. + + All properties will be added under the 'SweetHome3D' group + + Args: + obj (object): TheFC object to add a property to + property_type (str): the type of property to add + name (str): the name of the property to add + description (str): a short description of the property to add + """ + if name not in obj.PropertiesList: + obj.addProperty(property_type, name, "SweetHome3D", description) + + def get_fc_object(self, id, sh_type): + """Returns the FC doc element corresponding to the imported id and sh_type + + Args: + id (str): the id of the element to lookup + sh_type (str, optional): The SweetHome type of the element to be imported. Defaults to None. + + Returns: + FCObject: The FC object that correspond to the imported SH element + """ + if self.preferences["MERGE"] and id in self.fc_objects: + fc_object = self.fc_objects[id] + if sh_type: + assert fc_object.shType == sh_type, f"Invalid shType: expected {sh_type}, got {fc_object.shType}" + if self.preferences["DEBUG"]: + _log(translate("BIM", f"Merging imported element '{id}' with existing element of type '{type(fc_object)}'")) + return fc_object + if self.preferences["DEBUG"]: + _log(translate("BIM", f"No element found with id '{id}' and type '{sh_type}'")) + return None + + def add_floor(self, floor): + self.floors[floor.id] = floor + self.building.addObject(floor) + + def get_floor(self, level_id): + """Returns the Floor associated with the level_id. + + Returns the first level if only one defined or level_id is None + + Args: + levels (list): The list of imported levels + level_id (string): the level @id + + Returns: + level: The level + """ + if self.default_floor or not level_id: + return self.default_floor + return self.floors.get(level_id, None) + + def add_wall(self, wall): + self.walls.append(wall) + + def _create_groups(self): + """Create FreeCAD Group for the different imported elements + """ + doc = App.ActiveDocument + if self.preferences["IMPORT_LIGHTS"] and not doc.getObject("Lights"): + _log(f"Creating Lights group ...") + doc.addObject("App::DocumentObjectGroup", "Lights") + if self.preferences["IMPORT_CAMERAS"] and not doc.getObject("Cameras"): + _log(f"Creating Cameras group ...") + doc.addObject("App::DocumentObjectGroup", "Cameras") + + def _setup_project(self, elm): + """Create the Arch::Project and Arch::Site for this import + + Args: + elm (str): the element + + """ + if 'Project' in self.fc_objects: + self.project = self.fc_objects.get('Project') + else: + self.project = self._create_project() + if 'Site' in self.fc_objects: + self.site = self.fc_objects.get('Site') + else: + self.site = self._create_site() + if elm.get('name') in self.fc_objects: + self.building = self.fc_objects.get(elm.get('name')) + else: + self.building = self._create_building(elm) + self.project.addObject(self.site) + self.site.addObject(self.building) + + def _create_project(self): + """Create a default Arch::Project object + """ + project = Arch.makeProject([]) + self.set_property(project, "App::PropertyString", "id", "The element's id", "Project") + return project + + def _create_site(self): + """Create a default Arch::Site object + """ + site = Arch.makeSite([]) + self.set_property(site, "App::PropertyString", "id", "The element's id", "Site") + return site + + def _create_building(self, elm): + """Create a default Arch::Building object + + Args: + elm (str): the element + + Returns: + the Arch::Building + """ + building = Arch.makeBuilding([]) + self.set_property(building, "App::PropertyString", "shType", "The element type", 'building') + self.set_property(building, "App::PropertyString", "id", "The element's id", elm.get('name')) + for property in elm.findall('property'): + name = re.sub('[^A-Za-z0-9]+', '', property.get('name')) + value = property.get('value') + self.set_property(building, "App::PropertyString", name, "", value) + return building + + def _create_default_floor(self): + """Create a default Arch::Floor object + """ + floor = Arch.makeFloor() + floor.Label = 'Level' + floor.Placement.Base.z = 0 + floor.Height = 2500 + + self.set_property(floor, "App::PropertyString", "shType", "The element type", 'level') + self.set_property(floor, "App::PropertyString", "id", "The element's id", 'Level') + self.set_property(floor, "App::PropertyFloat", "floorThickness", "The floor's slab thickness", dim_fc2sh(floor.Height)) + if self.preferences["IMPORT_FURNITURES"]: + group = floor.newObject("App::DocumentObjectGroup", "Furnitures") + self.set_property(floor, "App::PropertyString", "FurnitureGroupName", "The DocumentObjectGroup name for all furnitures in this floor", group.Name) + group = floor.newObject("App::DocumentObjectGroup", "Baseboards") + self.set_property(floor, "App::PropertyString", "BaseboardGroupName", "The DocumentObjectGroup name for all baseboards on this floor", group.Name) + + return floor + + def _import_elements(self, parent, tag, update_progress=True): + """Generic function to import a specific element. + + This function will lookup the handler registered for the elements + `tag` and then call it on each item. It also provides some update + on the whole process. + + Args: + parent (Element): the parent of the elements to be imported. + Usually the element. + tag (str): the tag of the elements to be imported. + update_progress (bool, optional): whether to update the + progress. Set to false when importing a group of elements. + Defaults to True. + """ + tags = list(self.handlers.keys()) + elements = parent.findall(tag) + if update_progress and self.progress_bar: + self.progress_bar.stop() + self.progress_bar.start(f"Step {tags.index(tag)+1}/{len(tags)}: importing {len(elements)} '{tag}' elements. Please wait ...", len(elements)) + _msg(f"Importing {len(elements)} '{tag}' elements ...") + def _process(tuple): + (i, elm) = tuple + _msg(f"Importing {tag}#{i} ({self.current_object_count + 1}/{self.total_object_count}) ...") + try: + self.handlers[tag].process(parent, i, elm) + except Exception as e: + _err(f"Failed to import <{tag}>#{i} ({elm.get('id', elm.get('name'))}):") + _err(str(e)) + if update_progress and self.progress_bar: + self.progress_bar.next() + self.current_object_count = self.current_object_count + 1 + list(map(_process, enumerate(elements))) + +class BaseHandler: + """The base class for all importers.""" + + def __init__(self, importer: SH3DImporter): + self.importer = importer + + def setp(self, obj, type_, name, description, value=None, valid_values=None): + """Set a property on the object + + Args: + obj (FreeCAD): the object on which to set the property + type_ (str): the property type + name (str): the property name + description (str): the property description + value (xml.etree.ElementTree.Element|str, optional): The + property's value. Defaults to None. + valid_values (list, optional): The property's enumerated values. + Defaults to None. + """ + self.importer.set_property(obj, type_, name, description, value, valid_values) + + def get_fc_object(self, id, sh_type): + """Returns the FC object with the specified id and sh_type + + Args: + id (str): the id of the element to lookup + sh_type (str, optional): The SweetHome type of the element to be + imported. Defaults to None. + + Returns: + FCObject: The FC object that correspond to the imported SH element + """ + return self.importer.get_fc_object(id, sh_type) + + def get_floor(self, level_id): + """Returns the Floor associated with the level_id. + + Returns the first level if there is just one level or if level_id is + None + + Args: + levels (list): The list of imported levels + level_id (string): the level @id + + Returns: + level: The level + """ + return self.importer.get_floor(level_id) + + +class LevelHandler(BaseHandler): + """A helper class to import a SH3D `` object.""" + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + + def process(self, parent, i, elm): + """Creates and returns a Arch::Floor + + Args: + i (int): the ordinal of the imported element + elm (Element): the xml element + """ + floor = None + if self.importer.preferences["MERGE"]: + floor = self.get_fc_object(elm.get("id"), 'level') + + if not floor: + floor = Arch.makeFloor() + + floor.Label = elm.get('name') + floor.Placement.Base.z = dim_sh2fc(float(elm.get('elevation'))) + floor.Height = dim_sh2fc(float(elm.get('height'))) + self._set_properties(floor, elm) + + floor.ViewObject.Visibility = elm.get('visible', 'true') == 'true' + + if self.importer.preferences["IMPORT_FURNITURES"]: + group = floor.newObject("App::DocumentObjectGroup", "Furnitures") + self.setp(floor, "App::PropertyString", "FurnitureGroupName", "The DocumentObjectGroup name for all furnitures on this floor", group.Name) + group = floor.newObject("App::DocumentObjectGroup", "Baseboards") + self.setp(floor, "App::PropertyString", "BaseboardGroupName", "The DocumentObjectGroup name for all baseboards on this floor", group.Name) + + self.importer.add_floor(floor) + + def _set_properties(self, obj, elm): + self.setp(obj, "App::PropertyString", "shType", "The element type", 'level') + self.setp(obj, "App::PropertyString", "id", "The floor's id", elm) + self.setp(obj, "App::PropertyFloat", "floorThickness", "The floor's slab thickness", dim_sh2fc(float(elm.get('floorThickness')))) + self.setp(obj, "App::PropertyInteger", "elevationIndex", "The floor number", elm) + self.setp(obj, "App::PropertyBool", "viewable", "Whether the floor is viewable", elm) + + +class RoomHandler(BaseHandler): + """A helper class to import a SH3D `` object. + + It also handles the elements found as children of the element. + """ + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + + def process(self, parent, i, elm): + """Creates and returns a Arch::Structure from the imported_room object + + Args: + i (int): the ordinal of the imported element + elm (Element): the xml element + """ + level_id = elm.get('level', None) + floor = self.get_floor(level_id) + assert floor != None, f"Missing floor '{level_id}' for '{elm.get('id')}' ..." + + points = [] + for point in elm.findall('point'): + x = float(point.get('x')) + y = float(point.get('y')) + z = dim_fc2sh(floor.Placement.Base.z) + points.append(coord_sh2fc(App.Vector(x, y, z))) + + slab = None + if self.importer.preferences["MERGE"]: + slab = self.get_fc_object(elm.get("id"), 'room') + + if not slab: + line = Draft.make_wire(points, placement=App.Placement(), closed=True, face=True, support=None) + slab = Arch.makeStructure(line, height=floor.floorThickness) + + slab.Label = elm.get('name', 'Room') + slab.IfcType = "Slab" + slab.Normal = -Z_NORM + + color = elm.get('floorColor', self.importer.preferences["DEFAULT_FLOOR_COLOR"]) + set_color_and_transparency(slab, color) + self._set_properties(slab, elm) + floor.addObject(slab) + + def _set_properties(self, obj, elm): + floor_color = elm.get('floorColor',self.importer.preferences["DEFAULT_FLOOR_COLOR"]) + ceiling_color = elm.get('ceilingColor', self.importer.preferences["DEFAULT_CEILING_COLOR"]) + + self.setp(obj, "App::PropertyString", "shType", "The element type", 'room') + self.setp(obj, "App::PropertyString", "id", "The slab's id", elm.get('id', str(uuid.uuid4()))) + self.setp(obj, "App::PropertyFloat", "nameAngle", "The room's name angle", elm) + self.setp(obj, "App::PropertyFloat", "nameXOffset", "The room's name x offset", elm) + self.setp(obj, "App::PropertyFloat", "nameYOffset", "The room's name y offset", elm) + self.setp(obj, "App::PropertyBool", "areaVisible", "Whether the area of the room is displayed in the plan view", elm) + self.setp(obj, "App::PropertyFloat", "areaAngle", "The room's area annotation angle", elm) + self.setp(obj, "App::PropertyFloat", "areaXOffset", "The room's area annotation x offset", elm) + self.setp(obj, "App::PropertyFloat", "areaYOffset", "The room's area annotation y offset", elm) + self.setp(obj, "App::PropertyBool", "floorVisible", "Whether the floor of the room is displayed", elm) + self.setp(obj, "App::PropertyString", "floorColor", "The room's floor color", floor_color) + self.setp(obj, "App::PropertyFloat", "floorShininess", "The room's floor shininess", elm) + self.setp(obj, "App::PropertyBool", "ceilingVisible", "Whether the ceiling of the room is displayed", elm) + self.setp(obj, "App::PropertyString", "ceilingColor", "The room's ceiling color", ceiling_color) + self.setp(obj, "App::PropertyFloat", "ceilingShininess", "The room's ceiling shininess", elm) + self.setp(obj, "App::PropertyBool", "ceilingFlat", "", elm) + + +class WallHandler(BaseHandler): + """A helper class to import a SH3D `` object.""" + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + self.wall_sections = {} + + def process(self, parent, i, elm): + """Creates and returns a Arch::Structure from the imported_wall object + + Args: + parent (Element): the parent Element of the wall to be imported + i (int): the ordinal of the imported element + elm (Element): the xml element + """ + level_id = elm.get('level', None) + floor = self.get_floor(level_id) + assert floor != None, f"Missing floor '{level_id}' for '{elm.get('id')}' ..." + + wall = None + if self.importer.preferences["MERGE"]: + wall = self.get_fc_object(elm.get("id"), 'wall') + + if not wall: + prev = self._get_sibling_wall(parent, elm, 'wallAtStart') + next = self._get_sibling_wall(parent, elm, 'wallAtEnd') + wall = self._create_wall(floor, prev, next, elm) + if not wall: + _log(f"No wall created for {elm.get('id')}. Skipping!") + return + + self._set_wall_colors(wall, elm) + + wall.IfcType = "Wall" + wall.Label = f"wall{i}" + + self._set_properties(wall, elm) + + floor.addObject(wall) + self.importer.add_wall(wall) + + if self.importer.preferences["IMPORT_FURNITURES"]: + App.ActiveDocument.recompute([wall]) + for baseboard in elm.findall('baseboard'): + self._import_baseboard(floor, wall, baseboard) + + def _get_sibling_wall(self, parent, wall, sibling_attribute_name): + sibling_wall_id = wall.get(sibling_attribute_name, None) + if not sibling_wall_id: + return None + sibling_wall = parent.find(f"./wall[@id='{sibling_wall_id}']") + if sibling_wall is None: + wall_id = wall.get('id') + raise ValueError(f"Invalid SweetHome3D file: wall {wall_id} reference an unknown wall {sibling_wall_id}") + return sibling_wall + + def _set_properties(self, obj, elm): + self.setp(obj, "App::PropertyString", "shType", "The element type", 'wall') + self.setp(obj, "App::PropertyString", "id", "The wall's id", elm) + self.setp(obj, "App::PropertyString", "wallAtStart", "The Id of the contiguous wall at the start of this wall", elm) + self.setp(obj, "App::PropertyString", "wallAtEnd", "The Id of the contiguous wall at the end of this wall", elm) + self.setp(obj, "App::PropertyString", "pattern", "The pattern of this wall in plan view", elm) + self.setp(obj, "App::PropertyFloat", "leftSideShininess", "The wall's left hand side shininess", elm) + self.setp(obj, "App::PropertyFloat", "rightSideShininess", "The wall's right hand side shininess", elm) + + def _create_wall(self, floor, prev, next, elm): + """Create an Arch::Structure from an SH3D Element. + + The constructed wall will either be a straight wall or a curved + wall depending on the `elm` attributes. + + Args: + floor (Arch::Structure): The floor the wall belongs to + prev (Element): the xml element for the previous sibling wall + next (Element): the xml element for the next sibling wall + elm (Element): the xml element for the wall to be imported + + Returns: + Arch::Wall: the newly created wall + """ + wall_details = self._get_wall_details(floor, elm) + assert wall_details is not None, f"Fail to get details of wall {elm.get('id')}. Bailing out! {elm} / {wall_details}" + + # Both the wall at start or the wall at end can be None. + prev_wall_details = self._get_wall_details(floor, prev) + next_wall_details = self._get_wall_details(floor, next) + + # Is the wall curved (i.e. arc_extent != 0) ? + if wall_details[5] != 0: + section_start, section_end, spine = self._create_curved_segment( + wall_details, + prev_wall_details, + next_wall_details) + else: + section_start, section_end, spine = self._create_straight_segment( + wall_details, + prev_wall_details, + next_wall_details) + + sweep = App.ActiveDocument.addObject('Part::Sweep') + sweep.Sections = [section_start, section_end] + sweep.Spine = spine + sweep.Solid = True + sweep.Frenet = False + section_start.Visibility = False + section_end.Visibility = False + spine.Visibility = False + App.ActiveDocument.recompute([sweep]) + # Sometimes the Part::Sweep creates a "twisted" sweep which + # result in a broken wall. The solution is to use a compound + # object based on ruled surface instead. + if FIX_INVALID_SWEEP and (sweep.Shape.isNull() or not sweep.Shape.isValid()): + _log(f"Part::Sweep for wall#{elm.get('id')} is invalid. Using ruled surface instead ...") + ruled_surface = App.ActiveDocument.addObject('Part::RuledSurface') + ruled_surface.Curve1 = section_start + ruled_surface.Curve2 = section_end + App.ActiveDocument.recompute([ruled_surface]) + _log(f"Creating compound object ...") + compound = App.ActiveDocument.addObject('Part::Compound') + compound.Links = [ruled_surface, section_start, section_end] + App.ActiveDocument.recompute([compound]) + _log(f"Creating solid ...") + solid = App.ActiveDocument.addObject("Part::Feature") + solid.Shape = Part.Solid(Part.Shell(compound.Shape.Faces)) + doc = App.ActiveDocument + doc.removeObject(compound.Label) + doc.recompute() + doc.removeObject(ruled_surface.Label) + doc.recompute() + doc.removeObject(sweep.Label) + doc.recompute() + doc.removeObject(spine.Label) + doc.recompute() + doc.removeObject(section_start.Label) + doc.removeObject(section_end.Label) + wall = Arch.makeWall(solid) + else: + wall = Arch.makeWall(sweep) + return wall + + def _get_wall_details(self, floor, elm): + """Returns the relevant element for the given wall. + + Args: + floor (Slab): the Slab the wall belongs to + elm (Element): the wall being imported + + Returns: + Vector: the wall's starting point + vector: the wall's ending point + float: the thickness + float: the wall's height at the starting point + float: the wall's height at the ending point + float: the wall's arc in degrees + """ + if elm is None: + return None + x_start = float(elm.get('xStart')) + y_start = float(elm.get('yStart')) + x_end = float(elm.get('xEnd')) + y_end = float(elm.get('yEnd')) + z = dim_fc2sh(floor.Placement.Base.z) + + thickness = dim_sh2fc(elm.get('thickness')) + arc_extent = ang_sh2fc(elm.get('arcExtent', 0)) + height_start = dim_sh2fc(elm.get('height', dim_fc2sh(floor.Height))) + height_end = dim_sh2fc(elm.get('heightAtEnd', dim_fc2sh(height_start))) + + start = coord_sh2fc(App.Vector(x_start, y_start, z)) + end = coord_sh2fc(App.Vector(x_end, y_end, z)) + + return (start, end, thickness, height_start, height_end, arc_extent) + + def _create_straight_segment(self, wall_details, prev_wall_details, next_wall_details): + """Returns the sections and spine for a straight wall. + + Args: + wall_details (tuple): the wall details for the wall being imported + prev_wall_details (tuple): the details for the previous sibling + next_wall_details (tuple): the details for the next sibling + + Returns: + Rectangle, Rectangle, spine: both section and the line for the wall + """ + (start, end, _, _, _, _) = wall_details + + section_start = self._get_section(wall_details, True, prev_wall_details) + section_end = self._get_section(wall_details, False, next_wall_details) + + spine = Draft.makeLine(start, end) + App.ActiveDocument.recompute([section_start, section_end, spine]) + if self.importer.preferences["DEBUG"]: + _log(f"_create_straight_segment(): wall {self._pv(start)}->{self._pv(end)} => section_start={self._ps(section_start)}, section_end={self._ps(section_end)}") + + return section_start, section_end, spine + + def _create_curved_segment(self, wall_details, prev_wall_details, next_wall_details): + """Returns the sections and spine for a curved wall. + + Args: + wall_details (tuple): the wall details for the wall being imported + prev_wall_details (tuple): the details for the previous sibling + next_wall_details (tuple): the details for the next sibling + + Returns: + Rectangle, Rectangle, spine: both section and the arc for the wall + # """ + (start, end, _, _, _, arc_extent) = wall_details + + section_start = self._get_section(wall_details, True, prev_wall_details) + section_end = self._get_section(wall_details, False, next_wall_details) + + a1, a2, (invert_angle, center, radius) = self._get_normal_angles(wall_details) + + placement = App.Placement(center, App.Rotation()) + # BEWARE: makeCircle always draws counter-clockwise (i.e. in positive + # direction in xYz coordinate system). We therefore need to invert + # the start and end angle (as in SweetHome the wall is drawn in + # clockwise fashion). + if invert_angle: + spine = Draft.makeCircle(radius, placement, False, a1, a2) + else: + spine = Draft.makeCircle(radius, placement, False, a2, a1) + + App.ActiveDocument.recompute([section_start, section_end, spine]) + if self.importer.preferences["DEBUG"]: + _log(f"_create_curved_segment(): wall {self._pv(start)}->{self._pv(end)} => section_start={self._ps(section_start)}, section_end={self._ps(section_end)}") + + return section_start, section_end, spine + + def _get_section(self, wall_details, at_start, sibling_details): + """Returns a rectangular section at the specified coordinate. + + Returns a Rectangle that is then used as a section in the Part::Sweep + used to construct a wall. Depending whether the wall should be joined + with its siblings, the rectangle is either created and rotated around + the endpoint of the line that will be used as the spline of the sweep + or it is calculated as the intersection profile of the 2 walls. + + Args: + wall_details (tuple): The details of the wall + at_start (bool): indicate whether the section is for the start + point or the end point of the wall. + sibling_details (tuple): The details of the sibling wall + + Returns: + Rectangle: the section properly positioned + """ + if self.importer.preferences["JOIN_ARCH_WALL"] and sibling_details: + # In case the walls are to be joined we determine the intersection + # of both wall which depends on their respective thickness. + # Calculate the left and right side of each wall + (start, end, thickness, height_start, height_end, _) = wall_details + (s_start, s_end, s_thickness, _, _, _) = sibling_details + + lside, rside = self._get_sides(start, end, thickness) + s_lside, s_rside = self._get_sides(s_start, s_end, s_thickness) + i_start, i_end = self._get_intersection_edge(lside, rside, s_lside, s_rside) + + height = height_start if at_start else height_end + i_start_z = i_start + App.Vector(0, 0, height) + i_end_z = i_end + App.Vector(0, 0, height) + + if self.importer.preferences["DEBUG"]: + _log(f"Joining wall {self._pv(end-start)}@{self._pv(start)} and wall {self._pv(s_end-s_start)}@{self._pv(s_start)}") + _log(f" wall: {self._pe(lside)},{self._pe(rside)}") + _log(f" sibling: {self._pe(s_lside)},{self._pe(s_rside)}") + _log(f"intersec: {self._pv(i_start)},{self._pv(i_end)}") + section = Draft.makeRectangle([i_start, i_end, i_end_z, i_start_z]) + if self.importer.preferences["DEBUG"]: + _log(f"section: {section}") + else: + (start, end, thickness, height_start, height_end, _) = wall_details + height = height_start if at_start else height_end + center = start if at_start else end + a1, a2, _ = self._get_normal_angles(wall_details) + z_rotation = a1 if at_start else a2 + section = Draft.makeRectangle(thickness, height) + Draft.move([section], App.Vector(-thickness/2, 0, 0)) + Draft.rotate([section], 90, ORIGIN, X_NORM) + Draft.rotate([section], z_rotation, ORIGIN, Z_NORM) + Draft.move([section], center) + + if self.importer.preferences["DEBUG"]: + App.ActiveDocument.recompute() + view = section.ViewObject + line_colors = [view.LineColor] * len(section.Shape.Edges) + for i in range(0, len(line_colors)): + line_colors[i] = hex2rgb(DEBUG_EDGES_COLORS[i%len(DEBUG_EDGES_COLORS)]) + view.LineColorArray = line_colors + point_colors = [view.PointColor] * len(section.Shape.Vertexes) + for i in range(0, len(point_colors)): + point_colors[i] = hex2rgb(DEBUG_POINT_COLORS[i%len(DEBUG_POINT_COLORS)]) + view.PointColorArray = point_colors + view.PointSize = 5 + + return section + + def _get_intersection_edge(self, lside, rside, sibling_lside, sibling_rside): + """Returns the intersection edge of the 4 input edges. + + Args: + lside (Edge): the wall left handside + rside (Edge): the wall right handside + sibling_lside (Edge): the sibling wall left handside + sibling_rside (Edge): the sibling wall right handside + + Returns: + Edge: the Edge starting at the left handsides intersection and the + the right handsides intersection. + """ + points = DraftGeomUtils.findIntersection(lside, sibling_lside, True, True) + left = points[0] if len(points) else lside.Vertexes[0].Point + points = DraftGeomUtils.findIntersection(rside, sibling_rside, True, True) + right = points[0] if len(points) else rside.Vertexes[0].Point + edge = DraftGeomUtils.edg(left, right) + return edge.Vertexes[1].Point, edge.Vertexes[0].Point + + def _get_normal_angles(self, wall_details): + """Return the angles of the normal at the endpoints of the wall. + + This method returns the normal angle of the sections that constitute + the wall sweep. These angles can then be used to create the + corresponding sections. Depending on whether the wall section is + straight or curved, the section will be calculated slightly + differently. + + Args: + wall_details (tuple): The details of the wall + + Returns: + float: the angle of the normal at the starting point + float: the angle of the normal at the ending point + bool: the angle of the normal at the ending point + Vector: the center of the circle for a curved wall section + float: the radius of said circle + """ + (start, end, thickness, height_start, height_end, arc_extent) = wall_details + + angle_start = angle_end = 0 + invert_angle = False + center = radius = None + if arc_extent == 0: + angle_start = angle_end = 90-math.degrees(DraftVecUtils.angle(end-start, X_NORM)) + else: + # Calculate the circle that pases through the center of both rectangle + # and has the correct angle between p1 and p2 + chord = DraftVecUtils.dist(start, end) + radius = abs(chord / (2*math.sin(arc_extent/2))) + + circles = DraftGeomUtils.circleFrom2PointsRadius(start, end, radius) + # We take the center that preserve the arc_extent orientation (in FC + # coordinate). The orientation is calculated from start to end + center = circles[0].Center + if numpy.sign(arc_extent) != numpy.sign(DraftVecUtils.angle(start-center, end-center, Z_NORM)): + invert_angle = True + center = circles[1].Center + + # radius1 and radius2 are the vector from center to start and end respectively + radius1 = start - center + radius2 = end - center + + angle_start = math.degrees(DraftVecUtils.angle(X_NORM, radius1, Z_NORM)) + angle_end = math.degrees(DraftVecUtils.angle(X_NORM, radius2, Z_NORM)) + + return angle_start, angle_end, (invert_angle, center, radius) + + def _get_sides(self, start, end, thickness): + """Return 2 edges corresponding to the left and right side of the wall. + + Args: + start (Vector): the wall's starting point + end (Vector): the wall's ending point + thickness (float): the wall's thickness + + Returns: + Edge: the left handside edge of the wall + Edge: the right handside edge of the wall + """ + normal = self._get_normal(start, end, start+Z_NORM) + loffset = DraftVecUtils.scale(-normal, thickness/2) + roffset = DraftVecUtils.scale(normal, thickness/2) + edge = DraftGeomUtils.edg(start, end) + lside = DraftGeomUtils.offset(edge, loffset) + rside = DraftGeomUtils.offset(edge, roffset) + if self.importer.preferences["DEBUG"]: + _log(f"_get_sides(): wall {self._pv(end-start)}@{self._pv(start)} => normal={self._pv(normal)}, lside={self._pe(lside)}, rside={self._pe(rside)}") + return lside, rside + + def _get_normal(self, a, b, c): + """Return the normal of a plane defined by 3 points. + + NOTE: the order of your point is important as the coordinate + will go from a to b to c + + Args: + a (Vector): the first point + b (Vector): the second point + c (Vector): the third point + + Returns: + Vector: the normalized vector of the plane's normal + """ + return (b - a).cross(c - a).normalize() + + def _ps(self, section): + # Pretty print a Section in a condensed way + v = section.Shape.Vertexes + return f"[{self._pv(v[0].Point)}, {self._pv(v[1].Point)}, {self._pv(v[2].Point)}, {self._pv(v[3].Point)}]" + + def _pe(self, edge): + # Print an Edge in a condensed way + v = edge.Vertexes + return f"[{self._pv(v[0].Point)}, {self._pv(v[1].Point)}]" + + def _pv(self, vect): + # Print an Vector in a condensed way + return f"({round(getattr(vect, 'X', getattr(vect,'x')))},{round(getattr(vect, 'Y', getattr(vect,'y')))})" + + def _set_wall_colors(self, wall, elm): + """Set the `wall`'s color taken from `elm`. + + Using `ViewObject.DiffuseColor` attribute to set the different + color faces. Note that when the faces are changing (i.e. when + adding doors & windows). This will generate the wrong color + """ + topColor = elm.get('topColor', self.importer.preferences["DEFAULT_FLOOR_COLOR"]) + set_color_and_transparency(wall, topColor) + leftSideColor = hex2rgb(elm.get('leftSideColor', topColor)) + rightSideColor = hex2rgb(elm.get('rightSideColor', topColor)) + topColor = hex2rgb(topColor) + diffuse_color = [topColor, leftSideColor, topColor, rightSideColor, topColor, topColor] + if ang_sh2fc(elm.get('arcExtent', 0)) > 0: + diffuse_color = [topColor, rightSideColor, topColor, leftSideColor, topColor, topColor] + + if hasattr(wall.ViewObject, "DiffuseColor"): + wall.ViewObject.DiffuseColor = diffuse_color + + def _import_baseboard(self, floor, wall, elm): + """Creates and returns a Part::Extrusion from the imported_baseboard object + + Args: + floor (Slab): the Slab the wall belongs to + wall (Wall): the Arch wall + elm (Element): the wall being imported + + Returns: + Part::Extrusion: the newly created object + """ + wall_width = float(wall.Width) + baseboard_width = dim_sh2fc(elm.get('thickness')) + baseboard_height = dim_sh2fc(elm.get('height')) + vertexes = wall.Shape.Vertexes + + # The left side is defined as the face on the left hand side when going + # from (xStart,yStart) to (xEnd,yEnd). Assume the points are always + # created in the same order. We then have on the lefthand side the points + # 1 and 2, while on the righthand side we have the points 4 and 6 + side = elm.get('attribute') + if side == 'leftSideBaseboard': + p_start = vertexes[0].Point + p_end = vertexes[2].Point + p_normal = vertexes[4].Point + elif side == 'rightSideBaseboard': + p_start = vertexes[4].Point + p_end = vertexes[6].Point + p_normal = vertexes[0].Point + else: + raise ValueError(f"Invalid SweetHome3D file: invalid baseboard with 'attribute'={side}") + + v_normal = p_normal - p_start + v_baseboard = v_normal * (baseboard_width/wall_width) + p0 = p_start + p1 = p_end + p2 = p_end - v_baseboard + p3 = p_start - v_baseboard + + baseboard_id = f"{wall.id}-{side}" + baseboard = None + if self.importer.preferences["MERGE"]: + baseboard = self.get_fc_object(baseboard_id, 'baseboard') + + if not baseboard: + # I first add a rectangle + base = Draft.makeRectangle([p0, p1, p2, p3], face=True, support=None) + base.Visibility = False + # and then I extrude + baseboard = App.ActiveDocument.addObject('Part::Extrusion', f"{wall.Label} {side}") + baseboard.Base = base + + baseboard.DirMode = "Custom" + baseboard.Dir = Z_NORM + baseboard.DirLink = None + baseboard.LengthFwd = baseboard_height + baseboard.LengthRev = 0 + baseboard.Solid = True + baseboard.Reversed = False + baseboard.Symmetric = False + baseboard.TaperAngle = 0 + baseboard.TaperAngleRev = 0 + + set_color_and_transparency(baseboard, elm.get('color')) + + self.setp(baseboard, "App::PropertyString", "shType", "The element type", 'baseboard') + self.setp(baseboard, "App::PropertyString", "id", "The element's id", baseboard_id) + self.setp(baseboard, "App::PropertyLink", "parent", "The element parent", wall) + + if 'BaseboardGroupName' not in floor.PropertiesList: + group = floor.newObject("App::DocumentObjectGroup", "Baseboards") + self.setp(floor, "App::PropertyString", "BaseboardGroupName", "The DocumentObjectGroup name for all baseboards on this floor", group.Name) + + floor.getObject(floor.BaseboardGroupName).addObject(baseboard) + + +class BaseFurnitureHandler(BaseHandler): + """The base class for importing different class of furnitures.""" + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + + def set_furniture_common_properties(self, obj, elm): + self.setp(obj, "App::PropertyString", "id", "The furniture's id", elm) + self.setp(obj, "App::PropertyString", "name", "The furniture's name", elm) + self.setp(obj, "App::PropertyFloat", "angle", "The angle of the furniture", elm) + self.setp(obj, "App::PropertyBool", "visible", "Whether the object is visible", elm) + self.setp(obj, "App::PropertyBool", "movable", "Whether the object is movable", elm) + self.setp(obj, "App::PropertyString", "description", "The object's description", elm) + self.setp(obj, "App::PropertyString", "information", "The object's information", elm) + self.setp(obj, "App::PropertyString", "license", "The object's license", elm) + self.setp(obj, "App::PropertyString", "creator", "The object's creator", elm) + self.setp(obj, "App::PropertyBool", "modelMirrored", "Whether the object is mirrored", bool(elm.get('modelMirrored', False))) + self.setp(obj, "App::PropertyBool", "nameVisible", "Whether the object's name is visible", bool(elm.get('nameVisible', False))) + self.setp(obj, "App::PropertyFloat", "nameAngle", "The object's name angle", elm) + self.setp(obj, "App::PropertyFloat", "nameXOffset", "The object's name X offset", elm) + self.setp(obj, "App::PropertyFloat", "nameYOffset", "The object's name Y offset", elm) + self.setp(obj, "App::PropertyFloat", "price", "The object's price", elm) + + def set_piece_of_furniture_common_properties(self, obj, elm): + self.setp(obj, "App::PropertyString", "level", "The furniture's level", elm) + self.setp(obj, "App::PropertyString", "catalogId", "The furniture's catalog id", elm) + self.setp(obj, "App::PropertyFloat", "dropOnTopElevation", "", elm) + self.setp(obj, "App::PropertyString", "model", "The object's mesh file", elm) + self.setp(obj, "App::PropertyString", "icon", "The object's icon", elm) + self.setp(obj, "App::PropertyString", "planIcon", "The object's icon for the plan view", elm) + self.setp(obj, "App::PropertyString", "modelRotation", "The object's model rotation", elm) + self.setp(obj, "App::PropertyString", "modelCenteredAtOrigin", "The object's center", elm) + self.setp(obj, "App::PropertyBool", "backFaceShown", "Whether the object's back face is shown", elm) + self.setp(obj, "App::PropertyString", "modelFlags", "The object's flags", elm) + self.setp(obj, "App::PropertyFloat", "modelSize", "The object's size", elm) + self.setp(obj, "App::PropertyBool", "doorOrWindow", "Whether the object is a door or Window", bool(elm.get('doorOrWindow', False))) + self.setp(obj, "App::PropertyBool", "resizable", "Whether the object is resizable", elm) + self.setp(obj, "App::PropertyBool", "deformable", "Whether the object is deformable", elm) + self.setp(obj, "App::PropertyBool", "texturable", "Whether the object is texturable", elm) + self.setp(obj, "App::PropertyString", "staircaseCutOutShape", "", elm) + self.setp(obj, "App::PropertyFloat", "shininess", "The object's shininess", elm) + self.setp(obj, "App::PropertyFloat", "valueAddedTaxPercentage", "The object's VAT percentage", elm) + self.setp(obj, "App::PropertyString", "currency", "The object's price currency", str(elm.get('currency', 'EUR'))) + + def set_piece_of_furniture_horizontal_rotation_properties(self, obj, elm): + self.setp(obj, "App::PropertyBool", "horizontallyRotatable", "Whether the object horizontally rotatable", elm) + self.setp(obj, "App::PropertyFloat", "pitch", "The object's pitch", elm) + self.setp(obj, "App::PropertyFloat", "roll", "The object's roll", elm) + self.setp(obj, "App::PropertyFloat", "widthInPlan", "The object's width in the plan view", elm) + self.setp(obj, "App::PropertyFloat", "depthInPlan", "The object's depth in the plan view", elm) + self.setp(obj, "App::PropertyFloat", "heightInPlan", "The object's height in the plan view", elm) + + + def _get_mesh(self, elm): + model = elm.get('model') + if model not in self.importer.zip.namelist(): + raise ValueError(f"Invalid SweetHome3D file: missing model {model} for furniture {elm.get('id')}") + model_path_obj = None + try: + # Since mesh.read(model_data) does not work on BytesIO extract it first + tmp_dir = App.ActiveDocument.TransientDir + if os.path.isdir(os.path.join(tmp_dir, model)): + tmp_dir = os.path.join(tmp_dir, str(uuid.uuid4())) + model_path = self.importer.zip.extract(member=model, path=tmp_dir) + model_path_obj = model_path+".obj" + os.rename(model_path, model_path_obj) + mesh = Mesh.Mesh() + mesh.read(model_path_obj) + finally: + os.remove(model_path_obj) + return mesh + + +class DoorOrWindowHandler(BaseFurnitureHandler): + """A helper class to import a SH3D `` object.""" + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + + def process(self, parent, i, elm): + """Creates and returns a Arch::Door from the imported_door object + + Args: + i (int): the ordinal of the imported element + elm (Element): the xml element + """ + door_id = f"{elm.get('id', elm.get('name'))}-{i}" + level_id = elm.get('level', None) + floor = self.get_floor(level_id) + assert floor != None, f"Missing floor '{level_id}' for '{door_id}' ..." + + + feature = None + if self.importer.preferences["MERGE"]: + feature = self.get_fc_object(door_id, 'doorOrWindow') + + if not feature: + feature = self._create_door(floor, elm) + + assert feature != None, f"Missing feature for {door_id} ..." + + self._set_properties(feature, elm) + self.set_furniture_common_properties(feature, elm) + self.set_piece_of_furniture_common_properties(feature, elm) + self.setp(feature, "App::PropertyString", "id", "The furniture's id", door_id) + + def _set_properties(self, obj, elm): + self.setp(obj, "App::PropertyString", "shType", "The element type", 'doorOrWindow') + self.setp(obj, "App::PropertyFloat", "wallThickness", "", float(elm.get('wallThickness', 1))) + self.setp(obj, "App::PropertyFloat", "wallDistance", "", elm) + self.setp(obj, "App::PropertyFloat", "wallWidth", "", float(elm.get('wallWidth', 1))) + self.setp(obj, "App::PropertyFloat", "wallLeft", "", elm) + self.setp(obj, "App::PropertyFloat", "wallHeight", "", float(elm.get('wallHeight', 1))) + self.setp(obj, "App::PropertyFloat", "wallTop", "", elm) + self.setp(obj, "App::PropertyBool", "wallCutOutOnBothSides", "", elm) + self.setp(obj, "App::PropertyBool", "widthDepthDeformable", "", elm) + self.setp(obj, "App::PropertyString", "cutOutShape", "", elm) + self.setp(obj, "App::PropertyBool", "boundToWall", "", elm) + + def _create_door(self, floor, elm): + # The window in SweetHome3D is defined with a width, depth, height. + # Furthermore the (x.y.z) is the center point of the lower face of the + # window. In FC the placement is defined on the face of the whole that + # will contain the windows. The makes this calculation rather + # cumbersome. + x_center = float(elm.get('x')) + y_center = float(elm.get('y')) + z_center = float(elm.get('elevation', 0)) + z_center += dim_fc2sh(floor.Placement.Base.z) + + # This is the FC coordinate of the center point of the lower face of the + # window. This then needs to be moved to the proper face on the wall and + # offset properly with respect to the wall's face. + center = coord_sh2fc(App.Vector(x_center, y_center, z_center)) + + wall_width = -DEFAULT_WALL_WIDTH + wall = self._get_wall(center) + if wall: + wall_width = wall.Width + else: + _err(f"Missing wall for {elm.get('id')}. Defaulting to width {DEFAULT_WALL_WIDTH} ...") + + width = dim_sh2fc(elm.get('width')) + depth = dim_sh2fc(elm.get('depth')) + height = dim_sh2fc(elm.get('height')) + angle = float(elm.get('angle', 0)) + mirrored = bool(elm.get('modelMirrored', False)) + + # this is the vector that allow me to go from the center to the corner + # of the bounding box. Note that the angle of the rotation is negated + # because the y axis is reversed in SweetHome3D + center2corner = App.Vector(-width/2, -wall_width/2, 0) + rotation = App.Rotation(App.Vector(0, 0, 1), math.degrees(-angle)) + center2corner = rotation.multVec(center2corner) + + corner = center.add(center2corner) + pl = App.Placement( + corner, # translation + App.Rotation(math.degrees(-angle), 0, 90), # rotation + ORIGIN # rotation@coordinate + ) + + # NOTE: the windows are not imported as meshes, but we use a simple + # correspondence between a catalog ID and a specific window preset from + # the parts library. + catalog_id = elm.get('catalogId') + (windowtype, ifc_type) = DOOR_MODELS.get(catalog_id, (None, None)) + if not windowtype: + _wrn(f"Unknown catalogId {catalog_id} for element {elm.get('id')}. Defaulting to 'Simple Door'") + (windowtype, ifc_type) = ('Simple door', 'Door') + + h1 = 10 + h2 = 10 + h3 = 0 + w1 = min(depth, wall_width) + w2 = 10 + o1 = 0 + o2 = w1 / 2 + window = Arch.makeWindowPreset(windowtype, width, height, h1, h2, h3, w1, w2, o1, o2, pl) + window.IfcType = ifc_type + if ifc_type == 'Door' and mirrored: + window.OperationType = "SINGLE_SWING_RIGHT" + + # Adjust symbol plan, Sweet Home has the opening in the opposite side by default + window.ViewObject.Proxy.invertOpening() + if mirrored: + window.ViewObject.Proxy.invertHinge() + + if wall: + window.Hosts = [wall] + return window + + def _get_wall(self, point): + """Returns the wall that contains the given point. + + Args: + point (FreeCAD.Vector): the point to test for + + Returns: + Arch::Wall: the wall that contains the given point + """ + for wall in self.importer.walls: + try: + if wall.Shape.BoundBox.isInside(point): + return wall + except FloatingPointError: + pass + return None + + +class FurnitureHandler(BaseFurnitureHandler): + """A helper class to import a SH3D `` object.""" + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + + def process(self, parent, i, elm): + """Creates and returns a Mesh from the imported_furniture object + + Args: + i (int): the ordinal of the imported element + elm (Element): the xml element + """ + furniture_id = f"{elm.get('id', elm.get('name'))}-{i}" + level_id = elm.get('level', None) + floor = self.get_floor(level_id) + assert floor != None, f"Missing floor '{level_id}' for '{furniture_id}' ..." + + feature = None + if self.importer.preferences["MERGE"]: + feature = self.get_fc_object(furniture_id, 'pieceOfFurniture') + + if not feature: + feature = self._create_equipment(elm) + self.setp(feature, "App::PropertyString", "shType", "The element type", 'pieceOfFurniture') + self.set_furniture_common_properties(feature, elm) + self.set_piece_of_furniture_common_properties(feature, elm) + self.set_piece_of_furniture_horizontal_rotation_properties(feature, elm) + self.setp(feature, "App::PropertyString", "id", "The furniture's id", furniture_id) + + if 'FurnitureGroupName' not in floor.PropertiesList: + group = floor.newObject("App::DocumentObjectGroup", "Furnitures") + self.setp(floor, "App::PropertyString", "FurnitureGroupName", "The DocumentObjectGroup name for all furnitures on this floor", group.Name) + + floor.getObject(floor.FurnitureGroupName).addObject(feature) + + # We add the object to the list of known object that can then + # be referenced elsewhere in the SH3D model (i.e. lights). + self.importer.fc_objects[feature.id] = feature + + def _create_equipment(self, elm): + + floor = self.get_floor(elm.get('level')) + + width = dim_sh2fc(float(elm.get('width'))) + depth = dim_sh2fc(float(elm.get('depth'))) + height = dim_sh2fc(float(elm.get('height'))) + x = float(elm.get('x', 0)) + y = float(elm.get('y', 0)) + z = float(elm.get('elevation', 0.0)) + angle = float(elm.get('angle', 0.0)) + pitch = float(elm.get('pitch', 0.0)) # X Axis + roll = float(elm.get('roll', 0.0)) # Y Axis + name = elm.get('name') + mirrored = bool(elm.get('modelMirrored', "false") == "true") + + # The meshes are normalized, facing up. + # Center, Scale, X Rotation && Z Rotation (in FC axes), Move + mesh = self._get_mesh(elm) + bb = mesh.BoundBox + transform = App.Matrix() + transform.move(-bb.Center) + # NOTE: the model is facing up, thus y and z are inverted + transform.scale(width/bb.XLength, height/bb.YLength, depth/bb.ZLength) + transform.rotateX(math.pi/2) + transform.rotateX(-pitch) + transform.rotateY(roll) + transform.rotateZ(-angle) + level_elevation = dim_fc2sh(floor.Placement.Base.z) + distance = App.Vector(x, y, level_elevation + z + (dim_fc2sh(height) / 2)) + transform.move(coord_sh2fc(distance)) + mesh.transform(transform) + + if self.importer.preferences["CREATE_ARCH_EQUIPMENT"]: + shape = Part.Shape() + shape.makeShapeFromMesh(mesh.Topology, 0.100000) + equipment = Arch.makeEquipment(name=name) + equipment.Shape = shape + equipment.purgeTouched() + else: + equipment = App.ActiveDocument.addObject("Mesh::Feature", name) + equipment.Mesh = mesh + + return equipment + + +class LightHandler(FurnitureHandler): + """A helper class to import a SH3D `` object.""" + + def __init__(self, importer: SH3DImporter): + super().__init__(importer) + + def process(self, parent, i, elm): + """_summary_ + + Args: + i (int): the ordinal of the imported element + elm (Element): the xml element + """ + light_id = f"{elm.get('id', elm.get('name'))}-{i}" + level_id = elm.get('level', None) + floor = self.get_floor(level_id) + assert floor != None, f"Missing floor '{level_id}' for '{light_id}' ..." + + if self.importer.preferences["IMPORT_FURNITURES"]: + super().process(i, elm) + light_apppliance = self.get_fc_object(light_id, 'pieceOfFurniture') + assert light_apppliance != None, f"Missing furniture {light_id} ..." + self.setp(light_apppliance, "App::PropertyFloat", "power", "The power of the light", float(elm.get('power', 0.5))) + + # Import the lightSource sub-elments + for j, sub_elm in enumerate(elm.findall('lightSource')): + light_source = None + light_source_id = f"{light_id}-{j}" + if self.importer.preferences["MERGE"]: + light_source = self.get_fc_object(light_source_id, 'lightSource') + + if not light_source: + _, light_source, _ = PointLight.create() + + x = float(sub_elm.get('x')) + y = float(sub_elm.get('y')) + z = float(sub_elm.get('z')) + diameter = float(sub_elm.get('diameter')) + color = sub_elm.get('color') + + light_source.Label = elm.get('name') + light_source.Placement.Base = coord_sh2fc(App.Vector(x, y, z)) + light_source.Radius = dim_sh2fc(diameter / 2) + light_source.Color = hex2rgb(color) + + self.setp(light_source, "App::PropertyString", "shType", "The element type", 'lightSource') + self.setp(light_source, "App::PropertyString", "id", "The elment's id", light_source_id) + self.setp(light_source, "App::PropertyLink", "lightAppliance", "The furniture", light_apppliance) + + App.ActiveDocument.Lights.addObject(light_source) + + +class CameraHandler(BaseHandler): + """A helper class to import a SH3D `` or `` objects.""" + + def __init__(self, handler): + super().__init__(handler) + + def process(self, parent, i, elm): + """Creates and returns a Render Camera from the imported_camera object + + Args: + i (int): the ordinal of the imported element + elm (Element): the xml element + + Returns: + object: the newly created object + """ + x = float(elm.get('x')) + y = float(elm.get('y')) + z = float(elm.get('z')) + yaw = float(elm.get('yaw')) + pitch = float(elm.get('pitch')) + + attribute = elm.get('attribute') + if attribute != "storedCamera": + _log(translate("BIM", f"Type of <{elm.tag}> #{i} is not supported: '{attribute}'. Skipping!")) + return + + camera_id = f"{attribute}-{i}" + camera = None + if self.importer.preferences["MERGE"]: + camera = self.get_fc_object(camera_id, attribute) + + if not camera: + _, camera, _ = Camera.create() + App.ActiveDocument.Cameras.addObject(camera) + + # ¿How to convert fov to FocalLength? + fieldOfView = float(elm.get('fieldOfView')) + fieldOfView = math.degrees(fieldOfView) + + camera.Label = elm.get('name', attribute.title()) + camera.Placement.Base = coord_sh2fc(App.Vector(x, y, z)) + # NOTE: the coordinate system is screen like, thus roll & picth are inverted ZY'X'' + camera.Placement.Rotation.setYawPitchRoll( + math.degrees(math.pi-yaw), 0, math.degrees(math.pi/2-pitch)) + camera.Projection = "Perspective" + camera.AspectRatio = 1.33333333 # /home/environment/@photoAspectRatio + + self._set_properties(camera, elm) + + def _set_properties(self, obj, elm): + self.setp(obj, "App::PropertyString", "shType", "The element type", 'camera') + self.setp(obj, "App::PropertyString", "id", "The object ID", elm) + self.setp(obj, "App::PropertyEnumeration", "attribute", "The type of camera", elm.get('attribute'), valid_values=["topCamera", "observerCamera", "storedCamera", "cameraPath"]) + self.setp(obj, "App::PropertyBool", "fixedSize", "Whether the object is fixed size", bool(elm.get('fixedSize', False))) + self.setp(obj, "App::PropertyEnumeration", "lens", "The object's lens (PINHOLE | NORMAL | FISHEYE | SPHERICAL)", str(elm.get('lens', "PINHOLE")), valid_values=["PINHOLE", "NORMAL", "FISHEYE", "SPHERICAL"]) + self.setp(obj, "App::PropertyFloat", "yaw", "The object's yaw", elm) + self.setp(obj, "App::PropertyFloat", "pitch", "The object's pitch", elm) + self.setp(obj, "App::PropertyFloat", "time", "Unknown", elm) + self.setp(obj, "App::PropertyFloat", "fieldOfView", "The object's FOV", elm) + self.setp(obj, "App::PropertyString", "renderer", "The object's renderer", elm) + + +def dim_sh2fc(dimension): + """Convert SweetHome dimension (cm) to FreeCAD dimension (mm) + + Args: + dimension (float): The dimension in SweetHome + + Returns: + float: the FreeCAD dimension + """ + return float(dimension)*FACTOR + + +def dim_fc2sh(dimension): + """Convert FreeCAD dimension (mm) to SweetHome dimension (cm) + + Args: + dimension (float): The dimension in FreeCAD + + Returns: + float: the SweetHome dimension + """ + return float(dimension)/FACTOR + + +def coord_sh2fc(vector): + """Converts SweetHome to FreeCAD coordinate + + Args: + FreeCAD.Vector (FreeCAD.Vector): The coordinate in SweetHome + + Returns: + FreeCAD.Vector: the FreeCAD coordinate + """ + return App.Vector(vector.x*FACTOR, -vector.y*FACTOR, vector.z*FACTOR) + + +def ang_sh2fc(angle): + """Convert SweetHome angle (º) to FreeCAD angle (º) + + SweetHome angles are clockwise positive while FreeCAD are anti-clockwise + positive + + Args: + angle (float): The angle in SweetHome + + Returns: + float: the FreeCAD angle + """ + return -float(angle) + + +def set_color_and_transparency(obj, color): + if not App.GuiUp or not color: + return + if hasattr(obj.ViewObject, "ShapeColor"): + obj.ViewObject.ShapeColor = hex2rgb(color) + if hasattr(obj.ViewObject, "Transparency"): + obj.ViewObject.Transparency = _hex2transparency(color) + + +def color_fc2sh(hexcode): + # 0xRRGGBBAA => AARRGGBB + hex_str = hex(int(hexcode))[2:] + return ''.join([hex_str[6:], hex_str[0:6]]) + + +def hex2rgb(hexcode): + # We might have transparency as the first 2 digit + offset = 0 if len(hexcode) == 6 else 2 + return ( + int(hexcode[offset:offset+2], 16), # Red + int(hexcode[offset+2:offset+4], 16), # Green + int(hexcode[offset+4:offset+6], 16) # Blue + ) + + +def _hex2transparency(hexcode): + return 100 - int(int(hexcode[0:2], 16) * 100 / 255) diff --git a/src/Mod/BIM/importers/samples/Sample.sh3d b/src/Mod/BIM/importers/samples/Sample.sh3d new file mode 100644 index 0000000000000000000000000000000000000000..44ee9213355833b83d3cac93c223521c9a4eef41 GIT binary patch literal 87826 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 v+h(0M6b=nsRu9HEe$rXlE^Iy710MpwX3ZG7CQ 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 the first edge. + Base::Vector3 -> the starting point of the first line. In which case + `infinite1` must also be a point. + edge2 + Part.Edge, Circle, Line -> the second edge. + Base::Vector3 -> the ending point of the second line. In which case + `infinite2` must also be a point. + the second edge. In case of a point, `infinite2` must also be a point. + infinite1 + bool, optional -> whether `edge1` should be continued to infinity. + Default to `False`. + Base::Vector3 -> if `edge1` is a point, must also be a point. + infinite2 + bool, optional -> whether `edge2` should be continued to infinity. + Default to `False`. + Base::Vector3 -> if `edge2` is a point, must also be a point. + ex1: bool, optional + In case `edge1` is a point, indicate whether the line should be + continued to infinity. Default to `False` + ex2: bool, optional + In case `edge2` is a point, indicate whether the line should be + continued to infinity. Default to `False` + dts: bool, optional + NOT_DOCUMENTED. Default to `True` + findAll: bool, optional + In case either `edge1` or `edge2` is a circle, indicates whether + to find all intersection points. Default to `False` + + Returns + ------- + list + A list of intersection points + """ def getLineIntersections(pt1, pt2, pt3, pt4, infinite1, infinite2): if pt1: # first check if we don't already have coincident endpoints diff --git a/src/Mod/Draft/draftgeoutils/offsets.py b/src/Mod/Draft/draftgeoutils/offsets.py index 89a6543843..ec9e64eefe 100644 --- a/src/Mod/Draft/draftgeoutils/offsets.py +++ b/src/Mod/Draft/draftgeoutils/offsets.py @@ -135,6 +135,21 @@ def offset(edge, vector, trim=False): and a complete circle will be returned. None if there is a problem. + + Parameters + ---------- + edge: Part.Shape + the edge to offset + vector: Base::Vector3 + the vector by which the edge is to be offset + trim: bool, optional + If `edge` is an arc and `trim` is `True`, the resulting + arc will be trimmed to the proper angle + + Returns + ------- + Part.Shape + The offset shape """ if (not isinstance(edge, Part.Shape) or not isinstance(vector, App.Vector)): diff --git a/src/Mod/Draft/draftutils/params.py b/src/Mod/Draft/draftutils/params.py index 5b4814a787..69a9ee5c99 100644 --- a/src/Mod/Draft/draftutils/params.py +++ b/src/Mod/Draft/draftutils/params.py @@ -538,7 +538,8 @@ def _get_param_dictionary(): ":/ui/preferences-archdefaults.ui", ":/ui/preferences-dae.ui", ":/ui/preferences-ifc.ui", - ":/ui/preferences-ifc-export.ui"): + ":/ui/preferences-ifc-export.ui", + ":/ui/preferences-sh3d-import.ui",): # https://stackoverflow.com/questions/14750997/load-txt-file-from-resources-in-python fd = QtCore.QFile(fnm)