diff --git a/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui b/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui
index ffc9bcc6d3..d916e0bde7 100644
--- a/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui
+++ b/src/Mod/BIM/Resources/ui/preferences-sh3d-import.ui
@@ -65,6 +65,23 @@
Import options
+ -
+
+
+ Merge imported element with existing FC object
+
+
+ Merge into existing document
+
+
+ sh3dMerge
+
+
+ Mod/Arch
+
+
+
+
-
@@ -163,21 +180,22 @@
-
-
+
- Merge imported element with existing FC object
+ Create a default Render project with the newly created Site
- Merge into existing document
+ Create Render Project (requires Render)
- sh3dMerge
+ sh3dCreateRenderProject
Mod/Arch
+
-
-
@@ -259,21 +277,117 @@
-
-
+
- Create a default Render project with the newly created Site
+ Create a default IFC project with the newly created Site.
- Create Render Project (requires Render)
+ Create IFC Project
- sh3dCreateRenderProject
+ sh3dCreateIFCProject
Mod/Arch
+ -
+
+
+ Create a Mesh to represent the default ground level.
+
+
+ Create ground level Mesh
+
+
+ sh3dCreateGroundMesh
+
+
+ Mod/Arch
+
+
+
+ -
+
+
-
+
+
+ Default ground Color
+
+
+ sh3dDefaultGroundColor
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ This color might be used when the environment does not define a color for the ground.
+
+
+
+ 168
+ 168
+ 168
+
+
+
+ sh3dDefaultGroundColor
+
+
+ Mod/Arch
+
+
+
+
+
+ -
+
+
-
+
+
+ Default sky Color
+
+
+ sh3dDefaultSkyColor
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ This color might be used when the environment does not define a color for the sky.
+
+
+
+ 204
+ 228
+ 252
+
+
+
+ sh3dDefaultSkyColor
+
+
+ Mod/Arch
+
+
+
+
+
-
@@ -290,22 +404,6 @@
- -
-
-
- Create a default IFC project with the newly created Site.
-
-
- Create IFC Project
-
-
- sh3dCreateIFCProject
-
-
- Mod/Arch
-
-
-
diff --git a/src/Mod/BIM/importers/importSH3DHelper.py b/src/Mod/BIM/importers/importSH3DHelper.py
index 34a0679fbb..52f10c12b9 100644
--- a/src/Mod/BIM/importers/importSH3DHelper.py
+++ b/src/Mod/BIM/importers/importSH3DHelper.py
@@ -22,6 +22,7 @@
import math
import os
import re
+import traceback
import uuid
import xml.etree.ElementTree as ET
import zipfile
@@ -30,10 +31,11 @@ import Arch
import Draft
import DraftGeomUtils
import DraftVecUtils
-import draftutils.gui_utils as gui_utils
import Mesh
+import MeshPart
import numpy
import Part
+from draftobjects.facebinder import Facebinder
from draftutils.messages import _err, _log, _msg, _wrn
from draftutils.params import get_param_arch
@@ -42,6 +44,7 @@ import FreeCAD as App
if App.GuiUp:
import FreeCADGui as Gui
from draftutils.translate import translate
+ from draftviewproviders.view_facebinder import ViewProviderFacebinder
else:
# \cond
def translate(_, text):
@@ -59,14 +62,18 @@ try:
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)
+DEFAULT_MATERIAL = App.Material(
+ DiffuseColor=(1.00,0.00,0.00),
+ AmbientColor=(0.33,0.33,0.33),
+ SpecularColor=(0.53,0.53,0.53),
+ EmissiveColor=(0.00,0.00,0.00),
+ Shininess=(0.90),
+ Transparency=(0.00)
+ )
ORIGIN = App.Vector(0, 0, 0)
X_NORM = App.Vector(1, 0, 0)
@@ -173,6 +180,7 @@ class SH3DImporter:
self.default_floor = None
self.floors = {}
self.walls = []
+ self.space_upper_faces = []
def import_sh3d_from_string(self, home:str):
"""Import the SH3D Home from a String.
@@ -242,7 +250,6 @@ class SH3DImporter:
# 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')
@@ -250,6 +257,11 @@ class SH3DImporter:
# Importing elements ...
self._import_elements(home, 'wall')
+ self._refresh()
+
+ if self.preferences["CREATE_GROUND_MESH"]:
+ self._create_ground_mesh(home)
+
self._refresh()
if App.GuiUp and self.preferences["FIT_VIEW"]:
Gui.SendMsgToActiveView("ViewFit")
@@ -257,6 +269,19 @@ class SH3DImporter:
# Importing elements ...
if self.preferences["IMPORT_DOORS_AND_WINDOWS"]:
self._import_elements(home, 'doorOrWindow')
+ for furniture_group in home.findall('furnitureGroup'):
+ self._import_elements(furniture_group, 'doorOrWindow', False)
+ self._refresh()
+ group = App.ActiveDocument.Facebinders
+ for element in group.Group:
+ faces = []
+ new_sel_subshapes = []
+ for (sel_object, sel_subshapes) in element.Faces:
+ for sel_subshape in sel_subshapes:
+ sel_subshape = sel_subshape[1:] if sel_subshape.startswith('?') else sel_subshape
+ new_sel_subshapes.append(sel_subshape)
+ faces.append((sel_object, new_sel_subshapes))
+ element.Faces = faces
self._refresh()
# Importing && elements ...
@@ -286,7 +311,6 @@ class SH3DImporter:
_msg(f"Successfully imported home '{home.get('name')}' ...")
-
def _get_object_count(self, home):
"""Get an approximate count of object to be imported
"""
@@ -311,6 +335,9 @@ class SH3DImporter:
'CREATE_IFC_PROJECT': get_param_arch("sh3dCreateIFCProject"),
'DEFAULT_FLOOR_COLOR': color_fc2sh(get_param_arch("sh3dDefaultFloorColor")),
'DEFAULT_CEILING_COLOR': color_fc2sh(get_param_arch("sh3dDefaultCeilingColor")),
+ 'CREATE_GROUND_MESH': get_param_arch("sh3dCreateGroundMesh"),
+ 'DEFAULT_GROUND_COLOR': color_fc2sh(get_param_arch("sh3dDefaultGroundColor")),
+ 'DEFAULT_SKY_COLOR': color_fc2sh(get_param_arch("sh3dDefaultSkyColor")),
}
def _setup_handlers(self):
@@ -321,6 +348,7 @@ class SH3DImporter:
}
if self.preferences["IMPORT_DOORS_AND_WINDOWS"]:
self.handlers['doorOrWindow'] = DoorOrWindowHandler(self)
+ self.handlers['furnitureGroup'] = None
if self.preferences["IMPORT_FURNITURES"]:
self.handlers['pieceOfFurniture'] = FurnitureHandler(self)
@@ -339,7 +367,7 @@ class SH3DImporter:
if App.GuiUp:
Gui.updateGui()
- def set_property(self, obj, type_, name, description, value, valid_values=None):
+ def set_property(self, obj, type_, name, description, value, valid_values=None, group="SweetHome3D"):
"""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.
@@ -353,13 +381,13 @@ class SH3DImporter:
valid_values (list): an optional list of valid values
"""
- self._add_property(obj, type_, name, description)
+ self._add_property(obj, type_, name, description, group)
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(value) is ET.Element or type(value) is type(dict()):
if type_ == "App::PropertyString":
value = str(value.get(name, ""))
elif type_ == "App::PropertyFloat":
@@ -372,7 +400,7 @@ class SH3DImporter:
_log(f"Setting @{obj}.{name} = {value}")
setattr(obj, name, value)
- def _add_property(self, obj, property_type, name, description):
+ def _add_property(self, obj, property_type, name, description, group="SweetHome3D"):
"""Add an property to the FC object.
All properties will be added under the 'SweetHome3D' group
@@ -384,7 +412,7 @@ class SH3DImporter:
description (str): a short description of the property to add
"""
if name not in obj.PropertiesList:
- obj.addProperty(property_type, name, "SweetHome3D", description)
+ obj.addProperty(property_type, name, group, description)
def get_fc_object(self, id, sh_type):
"""Returns the FC doc element corresponding to the imported id and sh_type
@@ -427,9 +455,40 @@ class SH3DImporter:
return self.default_floor
return self.floors.get(level_id, None)
+ def get_space(self, floor, p):
+ """Returns the Space this point belongs to.
+
+ An point belongs to a space if it is the closest space below that point
+
+ Args:
+ floor (level): the floor the point's parent belongs to.
+ p (Point): the point for which to determine the closest space.
+
+ Returns:
+ Space: the space the object belongs to or None
+ """
+ closest_space = None
+ for (space_floor, space, space_face) in self.space_upper_faces:
+ if not space_face: #?!?
+ continue
+ space_face_z = space_face.CenterOfMass.z
+ projection = App.Vector(p.x, p.y, space_face_z)
+ # Checks that:
+ # - the point's projection is inside the face
+ # - the point is above the face
+ # - the point's parent and the face's are on the same level
+ # NOTE: If two rooms overlap on the same level, the result is
+ # undefined...
+ if space_face.isInside(projection, 1, True) and space_face_z < p.z and space_floor.id == floor.id:
+ closest_space = space
+ return closest_space
+
def add_wall(self, wall):
self.walls.append(wall)
+ def get_walls(self):
+ return self.walls
+
def _create_groups(self):
"""Create FreeCAD Group for the different imported elements
"""
@@ -456,6 +515,8 @@ class SH3DImporter:
self.site = self.fc_objects.get('Site')
else:
self.site = self._create_site()
+ self._set_site_properties(elm)
+
if elm.get('name') in self.fc_objects:
self.building = self.fc_objects.get(elm.get('name'))
else:
@@ -501,21 +562,32 @@ class SH3DImporter:
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
+ return self.handlers['level'].create_default_floor()
- 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)
+ def _create_ground_mesh(self, elm):
+ bb = self.building.Shape.BoundBox
+ dx = bb.XLength/2
+ dy = bb.YLength/2
+ SO = App.Vector(bb.XMin-dx, bb.YMin-dy, 0)
+ NO = App.Vector(bb.XMin-dx, bb.YMax+dy, 0)
+ NE = App.Vector(bb.XMax+dx, bb.YMax+dy, 0)
+ SE = App.Vector(bb.XMax+dx, bb.YMin-dy, 0)
+ edge0 = Part.makeLine(SO, NO)
+ edge1 = Part.makeLine(NO, NE)
+ edge2 = Part.makeLine(NE, SE)
+ edge3 = Part.makeLine(SE, SO)
+ # ground = App.ActiveDocument.addObject("Part::Feature", "Ground")
+ ground_face = Part.makeFace([ Part.Wire([edge0, edge1, edge2, edge3]) ])
- return floor
+ ground = App.ActiveDocument.addObject("Mesh::Feature", "Ground")
+ ground.Mesh = MeshPart.meshFromShape(Shape=ground_face, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
+ ground.Label = "Ground"
+
+ set_color_and_transparency(ground, self.site.groundColor)
+ ground.ViewObject.Transparency = 50
+ # TODO: apply possible within the element
+
+ self.site.addObject(ground)
def _import_elements(self, parent, tag, update_progress=True):
"""Generic function to import a specific element.
@@ -546,11 +618,80 @@ class SH3DImporter:
except Exception as e:
_err(f"Failed to import <{tag}>#{i} ({elm.get('id', elm.get('name'))}):")
_err(str(e))
+ _err(traceback.format_exc())
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)))
+ def _set_site_properties(self, elm):
+ # All information in environment?, backgroundImage?, print?, compass
+ # are added to the site object. Some are furthermore added to the ground
+ environments = elm.findall('environment')
+ if len(environments) > 0:
+ environment = environments[0]
+ ground_color = environment.get('groundColor',self.preferences["DEFAULT_GROUND_COLOR"])
+ sky_color = environment.get('ceilingColor', self.preferences["DEFAULT_SKY_COLOR"])
+ lightColor = environment.get('lightColor', self.preferences["DEFAULT_SKY_COLOR"])
+ ceillingLightColor = environment.get('ceillingLightColor', self.preferences["DEFAULT_SKY_COLOR"])
+
+ self.set_property(self.site, "App::PropertyString", "groundColor", "", ground_color)
+ self.set_property(self.site, "App::PropertyBool", "backgroundImageVisibleOnGround3D", "", environment)
+ self.set_property(self.site, "App::PropertyString", "skyColor", "", sky_color)
+ self.set_property(self.site, "App::PropertyString", "lightColor", "", lightColor)
+ self.set_property(self.site, "App::PropertyFloat", "wallsAlpha", "", environment)
+ self.set_property(self.site, "App::PropertyBool", "allLevelsVisible", "", environment)
+ self.set_property(self.site, "App::PropertyBool", "observerCameraElevationAdjusted", "", environment)
+ self.set_property(self.site, "App::PropertyString", "ceillingLightColor", "", ceillingLightColor)
+ self.set_property(self.site, "App::PropertyEnumeration", "drawingMode", "", str(environment.get('drawingMode', 'FILL')), valid_values=["FILL", "OUTLINE", "FILL_AND_OUTLINE"])
+ self.set_property(self.site, "App::PropertyFloat", "subpartSizeUnderLight", "", environment)
+ self.set_property(self.site, "App::PropertyInteger", "photoWidth", "", environment)
+ self.set_property(self.site, "App::PropertyInteger", "photoHeight", "", environment)
+ self.set_property(self.site, "App::PropertyEnumeration", "photoAspectRatio", "", str(environment.get('photoAspectRatio', 'VIEW_3D_RATIO')), valid_values=["FREE_RATIO", "VIEW_3D_RATIO", "RATIO_4_3", "RATIO_3_2", "RATIO_16_9", "RATIO_2_1", "RATIO_24_10", "SQUARE_RATIO"])
+ self.set_property(self.site, "App::PropertyInteger", "photoQuality", "", environment)
+ self.set_property(self.site, "App::PropertyInteger", "videoWidth", "", environment)
+ self.set_property(self.site, "App::PropertyEnumeration", "videoAspectRatio", "", str(environment.get('videoAspectRatio', 'RATIO_4_3')), valid_values=["RATIO_4_3", "RATIO_16_9", "RATIO_24_10"])
+ self.set_property(self.site, "App::PropertyInteger", "photoQuality", "", environment)
+ self.set_property(self.site, "App::PropertyInteger", "videoQuality", "", environment)
+ self.set_property(self.site, "App::PropertyString", "videoSpeed", "", environment)
+ self.set_property(self.site, "App::PropertyInteger", "videoFrameRate", "", environment)
+ else:
+ _msg(f"No tag found in <{elm.tag}>")
+
+ bg_imgs = elm.findall('backgroundImage')
+ if len(bg_imgs) > 0:
+ bg_img = bg_imgs[0]
+ self.set_property(self.site, "App::PropertyString", "image", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "scaleDistance", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "scaleDistanceXStart", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "scaleDistanceYStart", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "scaleDistanceXEnd", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "scaleDistanceYEnd", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "xOrigin", "", bg_img)
+ self.set_property(self.site, "App::PropertyFloat", "yOrigin", "", bg_img)
+ self.set_property(self.site, "App::PropertyBool", "visible", "Whether the background image is visible", bg_img)
+ else:
+ _msg(f"No tag found in <{elm.tag}>")
+
+ compasses = elm.findall('compass')
+ if len(compasses) > 0:
+ compass = compasses[0]
+ self.set_property(self.site, "App::PropertyFloat", "x", "The compass's x", compass)
+ self.set_property(self.site, "App::PropertyFloat", "y", "The compass's y", compass)
+ self.set_property(self.site, "App::PropertyFloat", "diameter", "The compass's diameter in cm", compass)
+ self.set_property(self.site, "App::PropertyFloat", "northDirection", "The compass's angle to the north in degree", compass)
+ self.set_property(self.site, "App::PropertyFloat", "longitude", "The compass's longitude", compass)
+ self.set_property(self.site, "App::PropertyFloat", "latitude", "The compass's latitude", compass)
+ self.set_property(self.site, "App::PropertyString", "timeZone", "The compass's TimeZone", compass)
+ self.set_property(self.site, "App::PropertyBool", "visible", "Whether the compass is visible", compass)
+ self.site.Declination = ang_sh2fc(math.degrees(float(self.site.northDirection)))
+ self.site.Longitude = math.degrees(float(self.site.longitude))
+ self.site.Latitude = math.degrees(float(self.site.latitude))
+ self.site.EPWFile = '' # https://www.ladybug.tools/epwmap/ or https://climate.onebuilding.org
+ else:
+ _msg(f"No tag found in <{elm.tag}>")
+
+
class BaseHandler:
"""The base class for all importers."""
@@ -600,6 +741,62 @@ class BaseHandler:
"""
return self.importer.get_floor(level_id)
+ def get_space(self, floor, p):
+ """Returns the Space this point belongs to.
+
+ An point belongs to a space if it is the closest space below that point
+
+ Args:
+ floor (level): the floor the point's parent belongs to.
+ p (Point): the point for which to determine the closest space.
+
+ Returns:
+ Space: the space the object belongs to or None
+ """
+ return self.importer.get_space(floor, p)
+
+ def _get_upper_face(self, faces):
+ """Returns the upper face of a given list of faces
+
+ More specifically returns the face with the highest z.
+ It is used to figure out which space a furniture belongs to.
+
+ Args:
+ faces (list): The list of faces
+
+ Returns:
+ Face: the upper face
+ """
+ upper_face = None
+ com_max_z = -float('inf')
+ for face in faces:
+ com = face.CenterOfMass
+ if com.z > com_max_z:
+ upper_face = face
+ com_max_z = com.z
+ return upper_face
+
+ def _get_lower_face(self, faces):
+ """Returns the lower face of a given list of faces
+
+ More specifically returns the face with the lowest z.
+ It is used to figure out which space a furniture belongs to.
+
+ Args:
+ faces (list): The list of faces
+
+ Returns:
+ Face: the lower face
+ """
+ lower_face = None
+ com_min_z = float('inf')
+ for face in faces:
+ com = face.CenterOfMass
+ if com.z < com_min_z:
+ lower_face = face
+ com_min_z = com.z
+ return lower_face
+
class LevelHandler(BaseHandler):
"""A helper class to import a SH3D `` object."""
@@ -627,15 +824,21 @@ class LevelHandler(BaseHandler):
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._add_groups(floor)
self.importer.add_floor(floor)
+ def create_default_floor(self):
+ floor = Arch.makeFloor()
+ floor.Label = 'Level'
+ floor.Placement.Base.z = 0
+ floor.Height = 2500
+
+ self._set_properties(floor, dict({'shType': 'level', 'id':'Level', 'floorThickness':dim_fc2sh(250), 'elevationIndex': 0, 'viewable': True}))
+ self._add_groups(floor)
+ self.importer.add_floor(floor)
+
+ return 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)
@@ -644,6 +847,17 @@ class LevelHandler(BaseHandler):
self.setp(obj, "App::PropertyBool", "viewable", "Whether the floor is viewable", elm)
+ def _add_groups(self, floor):
+ group = floor.newObject("App::DocumentObjectGroup", "Facebinders")
+ self.setp(floor, "App::PropertyString", "FacebinderGroupName", "The DocumentObjectGroup name for all Facebinders on this floor", group.Name)
+
+ if self.importer.preferences["IMPORT_FURNITURES"]:
+ group = floor.newObject("App::DocumentObjectGroup", "Furnitures")
+ self.setp(floor, "App::PropertyString", "FurnitureGroupName", "The DocumentObjectGroup name for all furnitures in 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)
+
+
class RoomHandler(BaseHandler):
"""A helper class to import a SH3D `` object.
@@ -664,6 +878,8 @@ class RoomHandler(BaseHandler):
floor = self.get_floor(level_id)
assert floor != None, f"Missing floor '{level_id}' for '{elm.get('id')}' ..."
+ # A Room is composed of a space with the slab as the base object
+
points = []
for point in elm.findall('point'):
x = float(point.get('x'))
@@ -679,14 +895,42 @@ class RoomHandler(BaseHandler):
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.Label = elm.get('name', 'Room') + '-slab'
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)
+
+ slab.recompute(True)
+
+ # No 1-to-1 correspondance between SH3D and FC element.
+ # Creating a fake SH3D elemement in order to take advantage of the
+ # different lookup facilities. NOTE the suffix '-space' for both
+ # the sh_type and id...
+ space = None
+ if self.importer.preferences["MERGE"]:
+ space = self.get_fc_object(elm.get("id")+"-space", 'room-space')
+
+ if not space:
+ space = Arch.makeSpace(slab)
+ space.IfcType = "Space"
+ space.Label = elm.get('name', 'Room')
+ self._set_space_properties(space, elm)
+
+ self.importer.fc_objects[slab.id] = slab
+ self.importer.fc_objects[space.id] = space
+
+ upper_face = self._get_upper_face(slab.Shape.Faces)
+ if not upper_face:
+ _wrn(f"Couldn't find the upper face of slab {slab.Label} on level {floor.Label}!")
+ else:
+ self.importer.space_upper_faces.append((floor, space, upper_face))
+
+ slab.Visibility = True
+
+ floor.addObject(space)
def _set_properties(self, obj, elm):
floor_color = elm.get('floorColor',self.importer.preferences["DEFAULT_FLOOR_COLOR"])
@@ -709,6 +953,10 @@ class RoomHandler(BaseHandler):
self.setp(obj, "App::PropertyFloat", "ceilingShininess", "The room's ceiling shininess", elm)
self.setp(obj, "App::PropertyBool", "ceilingFlat", "", elm)
+ def _set_space_properties(self, obj, elm):
+ self.setp(obj, "App::PropertyString", "shType", "The element type", 'room-space')
+ self.setp(obj, "App::PropertyString", "id", "The slab's id", elm.get('id', str(uuid.uuid4()))+"-space")
+
class WallHandler(BaseHandler):
"""A helper class to import a SH3D `` object."""
@@ -729,32 +977,38 @@ class WallHandler(BaseHandler):
floor = self.get_floor(level_id)
assert floor != None, f"Missing floor '{level_id}' for '{elm.get('id')}' ..."
- wall = None
+ wall = base_object = 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)
+ wall, base_object = 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)
+ wall.recompute(True)
- floor.addObject(wall)
- self.importer.add_wall(wall)
+ self._create_facebinders(floor, wall, elm)
if self.importer.preferences["IMPORT_FURNITURES"]:
- App.ActiveDocument.recompute([wall])
for baseboard in elm.findall('baseboard'):
- self._import_baseboard(floor, wall, baseboard)
+ space = self._import_baseboard(floor, wall, baseboard)
+ if space:
+ space.Boundaries = space.Boundaries + [wall]
+
+ floor.addObject(wall)
+ if base_object:
+ floor.addObject(base_object)
+ base_object.Visibility = False
+ base_object.Label = base_object.Label + "-" + wall.Label
+
+ self.importer.add_wall(wall)
def _get_sibling_wall(self, parent, wall, sibling_attribute_name):
sibling_wall_id = wall.get(sibling_attribute_name, None)
@@ -772,8 +1026,6 @@ class WallHandler(BaseHandler):
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.
@@ -797,18 +1049,61 @@ class WallHandler(BaseHandler):
prev_wall_details = self._get_wall_details(floor, prev)
next_wall_details = self._get_wall_details(floor, next)
+ is_wall_straight = wall_details[5] == 0
+
# 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:
+ if is_wall_straight:
section_start, section_end, spine = self._create_straight_segment(
wall_details,
prev_wall_details,
next_wall_details)
+ else:
+ section_start, section_end, spine = self._create_curved_segment(
+ wall_details,
+ prev_wall_details,
+ next_wall_details)
+ base_object = None
+ sweep = self._make_sweep(section_start, section_end, spine)
+ # 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.
+ # See https://github.com/FreeCAD/FreeCAD/issues/18658 and related OCCT
+ # ticket
+ if (sweep.Shape.isNull() or not sweep.Shape.isValid()):
+ if is_wall_straight:
+ _log(f"Sweep's shape is invalid, using ruled surface instead ...")
+ App.ActiveDocument.removeObject(sweep.Label)
+ compound_solid, base_object = self._make_compound(section_start, section_end, spine)
+ wall = Arch.makeWall(compound_solid)
+ else:
+ _wrn(f"Sweep's shape is invalid, but mitigation is not available!")
+ wall = Arch.makeWall(sweep)
+ else:
+ wall = Arch.makeWall(sweep)
+
+ # Keep track of base object. Used for baseboard import
+ self.importer.set_property(wall, "App::PropertyLinkList", "BaseObjects", "The different base objects whose sweep failed. Kept for compatibility reasons", [section_start, section_end, spine])
+
+ # TODO: Width is incorrect when joining walls
+ wall.setExpression('Length', f'{spine.Label}.Length')
+ wall.setExpression('Width', f'({section_start.Label}.Length + {section_end.Label}.Length) / 2')
+ wall.setExpression('Height', f'({section_start.Label}.Height + {section_end.Label}.Height) / 2')
+
+ return wall, base_object
+
+ def _make_sweep(self, section_start, section_end, spine):
+ """Creates a Part::Sweep from sections and a spine.
+
+ Args:
+ section_start (Rectangle): the first section of the Sweep
+ section_end (Rectangle): the last section of the Sweep
+ spine (Line): the path of the Sweep
+
+ Returns:
+ Part::Sweep: the Part::Sweep
+ """
+ App.ActiveDocument.recompute([section_start, section_end, spine])
sweep = App.ActiveDocument.addObject('Part::Sweep')
sweep.Sections = [section_start, section_end]
sweep.Spine = spine
@@ -817,41 +1112,35 @@ class WallHandler(BaseHandler):
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)
- # For some reason the Length of the spine is not propagated to the
- # wall itself...
- wall.Length = spine.Length
- return wall
+ sweep.recompute(True)
+ return sweep
+
+ def _make_compound(self, section_start, section_end, spine):
+ """Creates a compound from sections
+
+ This is used as a mitigation for a criss-crossed Part::Sweep.
+
+ Args:
+ section_start (Rectangle): the first section of the Sweep
+ section_end (Rectangle): the last section of the Sweep
+ spine (Line): not really used...
+
+ Returns:
+ Compound: the compound
+ """
+ App.ActiveDocument.recompute([section_start, section_end, spine])
+ ruled_surface = App.ActiveDocument.addObject('Part::RuledSurface')
+ ruled_surface.Curve1 = section_start
+ ruled_surface.Curve2 = section_end
+ ruled_surface.recompute()
+ compound = App.activeDocument().addObject("Part::Compound")
+ compound.Links = [ruled_surface, section_start, section_end, spine]
+ compound.recompute()
+
+ compound_solid = App.ActiveDocument.addObject("Part::Feature")
+ compound_solid.Shape = Part.Solid(Part.Shell(compound.Shape.Faces))
+
+ return compound_solid, compound
def _get_wall_details(self, floor, elm):
"""Returns the relevant element for the given wall.
@@ -881,6 +1170,11 @@ class WallHandler(BaseHandler):
height_start = dim_sh2fc(elm.get('height', dim_fc2sh(floor.Height)))
height_end = dim_sh2fc(elm.get('heightAtEnd', dim_fc2sh(height_start)))
+ # NOTE: the wall height is adjusted with the floor thickness
+ # BUG: It should be adjusted for all floor except the last one.
+ height_start = height_start + floor.floorThickness
+ height_end = height_end + floor.floorThickness
+
start = coord_sh2fc(App.Vector(x_start, y_start, z))
end = coord_sh2fc(App.Vector(x_end, y_end, z))
@@ -932,10 +1226,17 @@ class WallHandler(BaseHandler):
# 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).
+ length = 0
if invert_angle:
spine = Draft.makeCircle(radius, placement, False, a1, a2)
+ length = abs(radius * math.radians(a2 - a1))
else:
spine = Draft.makeCircle(radius, placement, False, a2, a1)
+ length = abs(radius * math.radians(a1 - a2))
+
+ # The Length property is used in the Wall to calculate volume, etc...
+ # Since make Circle does not calculate this Length I calculate it here...
+ self.importer.set_property(spine, "App::PropertyFloat", "Length", "The length of the Arc", length, group="Draft")
App.ActiveDocument.recompute([section_start, section_end, spine])
if self.importer.preferences["DEBUG"]:
@@ -981,7 +1282,7 @@ class WallHandler(BaseHandler):
_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])
+ section = Draft.makeRectangle([i_start, i_end, i_end_z, i_start_z], face=True)
if self.importer.preferences["DEBUG"]:
_log(f"section: {section}")
else:
@@ -990,24 +1291,15 @@ class WallHandler(BaseHandler):
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)
+ section = Draft.makeRectangle(thickness, height, face=True)
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
+ section.recompute()
+ _color_section(section)
return section
@@ -1118,38 +1410,59 @@ class WallHandler(BaseHandler):
"""
return (b - a).cross(c - a).normalize()
- def _ps(self, section):
+ def _ps(self, section, print_z: bool = False):
# 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)}]"
+ if hasattr(section, 'Shape'):
+ v = section.Shape.Vertexes
+ else:
+ # a Part.Face
+ v = section.Vertexes
+ return f"[{self._pv(v[0].Point, print_z)}, {self._pv(v[1].Point, print_z)}, {self._pv(v[2].Point, print_z)}, {self._pv(v[3].Point, print_z)}]"
- def _pe(self, edge):
+ def _pe(self, edge, print_z: bool = False):
# Print an Edge in a condensed way
v = edge.Vertexes
- return f"[{self._pv(v[0].Point)}, {self._pv(v[1].Point)}]"
+ return f"[{self._pv(v[0].Point, print_z)}, {self._pv(v[1].Point, print_z)}]"
- def _pv(self, vect):
+ def _pv(self, v, print_z: bool = False, ndigits: None = None):
# Print an Vector in a condensed way
- return f"({round(getattr(vect, 'X', getattr(vect,'x')))},{round(getattr(vect, 'Y', getattr(vect,'y')))})"
+ if hasattr(v,'X'):
+ return f"({round(getattr(v, 'X'), ndigits)},{round(getattr(v, 'Y'), ndigits)}{',' + str(round(getattr(v, 'Z'), ndigits)) if print_z else ''})"
+ elif hasattr(v,'x'):
+ return f"({round(getattr(v, 'x'), ndigits)},{round(getattr(v, 'y'), ndigits)}{',' + str(round(getattr(v, 'z'), ndigits)) if print_z else ''})"
+ raise ValueError(f"Expected a Point or Vector, got {type(v)}")
- def _set_wall_colors(self, wall, elm):
- """Set the `wall`'s color taken from `elm`.
+ def _create_facebinders(self, floor, wall, elm):
+ """Set the wall's colors 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
+ Creates 2 FaceBinders (left and right) and sets the corresponding
+ color and the shininess of the wall.
+
+ Args:
+ wall (Arch::Wall): the wall to paint
+ elm (Element): the xml element for the wall to be imported
"""
- 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]
+ # The top color is the color of the "mass" of the wall
+ top_color = elm.get('topColor', self.importer.preferences["DEFAULT_FLOOR_COLOR"])
+ set_color_and_transparency(wall, top_color)
- if hasattr(wall.ViewObject, "DiffuseColor"):
- wall.ViewObject.DiffuseColor = diffuse_color
+ left_facebinder = Draft.make_facebinder(( wall, ("Face2", ) ))
+ left_facebinder.Extrusion = 1
+ left_facebinder.Label = wall.Label + "-fb-left"
+ left_side_color = elm.get('leftSideColor', top_color)
+ set_color_and_transparency(left_facebinder, left_side_color)
+ left_side_shininess = elm.get('leftSideShininess', 0)
+ set_shininess(left_facebinder, left_side_shininess)
+ floor.getObject(floor.FacebinderGroupName).addObject(left_facebinder)
+
+ right_facebinder = Draft.make_facebinder(( wall, ("Face4", ) ))
+ right_facebinder.Extrusion = 1
+ right_facebinder.Label = wall.Label + "-fb-right"
+ right_side_color = elm.get('rightSideColor', top_color)
+ set_color_and_transparency(right_facebinder, right_side_color)
+ right_side_shininess = elm.get('rightSideShininess', 0)
+ set_shininess(right_facebinder, right_side_shininess)
+ floor.getObject(floor.FacebinderGroupName).addObject(right_facebinder)
def _import_baseboard(self, floor, wall, elm):
"""Creates and returns a Part::Extrusion from the imported_baseboard object
@@ -1163,32 +1476,41 @@ class WallHandler(BaseHandler):
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
+ # This is brittle in case the wall is merged and the there are already
+ # some doors, windows, etc...
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}")
+ faces = wall.Base.Shape.Faces
+ face = faces[1] if side == 'leftSideBaseboard' else faces[3]
- 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
+ # Once I have the face, I get the lowest edge.
+ lowest_z = float('inf')
+ bottom_edge = None
+ for edge in face.Edges:
+ if edge and edge.CenterOfMass and edge.CenterOfMass.z < lowest_z:
+ lowest_z = edge.CenterOfMass.z
+ bottom_edge = edge
+
+ p_normal = face.normalAt(bottom_edge.CenterOfMass.x, bottom_edge.CenterOfMass.y)
+ p_normal.z = 0
+ offset_vector = p_normal.normalize().multiply(baseboard_width)
+ offset_bottom_edge = bottom_edge.translated(offset_vector)
+
+ if self.importer.preferences["DEBUG"]:
+ _log(f"Creating {side} for {wall.Label} from edge {self._pe(bottom_edge, True)} to {self._pe(offset_bottom_edge, True)} (normal={self._pv(p_normal, True, 4)})")
+
+ edge0 = bottom_edge.copy()
+ edge1 = Part.makeLine(bottom_edge.Vertexes[1].Point, offset_bottom_edge.Vertexes[1].Point)
+ edge2 = offset_bottom_edge
+ edge3 = Part.makeLine(offset_bottom_edge.Vertexes[0].Point, bottom_edge.Vertexes[0].Point)
+
+ # make sure all edges are coplanar...
+ ref_z = bottom_edge.CenterOfMass.z
+ for edge in [edge0, edge1, edge2, edge3]:
+ edge.Vertexes[0].Point.z = edge.Vertexes[1].Point.z = ref_z
baseboard_id = f"{wall.id}-{side}"
baseboard = None
@@ -1196,11 +1518,10 @@ class WallHandler(BaseHandler):
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 = App.ActiveDocument.addObject("Part::Feature", "baseboard-base")
+ base.Shape = Part.makeFace([ Part.Wire([edge0, edge1, edge2, edge3]) ])
base.Visibility = False
- # and then I extrude
- baseboard = App.ActiveDocument.addObject('Part::Extrusion', f"{wall.Label} {side}")
+ baseboard = App.ActiveDocument.addObject('Part::Extrusion', f"{wall.Label}-{side}")
baseboard.Base = base
baseboard.DirMode = "Custom"
@@ -1220,11 +1541,17 @@ class WallHandler(BaseHandler):
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)
+ baseboard.recompute(True)
- floor.getObject(floor.BaseboardGroupName).addObject(baseboard)
+ space = self.get_space(floor, baseboard.Shape.BoundBox.Center)
+ if space:
+ space.Group = space.Group + [baseboard]
+ else:
+ _log(f"No space found to enclose {baseboard.Label}. Adding to generic group.")
+ floor.getObject(floor.BaseboardGroupName).addObject(baseboard)
+
+ # Returns the Space for the wall to be added to the space.Boundaries
+ return space
class BaseFurnitureHandler(BaseHandler):
@@ -1279,7 +1606,6 @@ class BaseFurnitureHandler(BaseHandler):
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():
@@ -1349,43 +1675,48 @@ class DoorOrWindowHandler(BaseFurnitureHandler):
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.
+ # window. In FC the placement is defined on the face of the wall that
+ # contains 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))
+ center.z += floor.Placement.Base.z
- 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} ...")
-
+ # First create a solid representing the window countour and find the
+ # walls containing that window
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
+ corner = center.add(App.Vector(-width/2, -depth/2, -height/2))
+
+ solid = Part.makeBox(width, depth, height)
+ solid.rotate(solid.CenterOfMass, Z_NORM, math.degrees(ang_sh2fc(angle)))
+ solid.translate(corner)
+
+ # Get all the walls hosting that door/window...
+ wall_width = -DEFAULT_WALL_WIDTH
+ walls = self._get_containing_walls(solid)
+ if len(walls) == 0:
+ _err(f"Missing wall for {elm.get('id')}. Defaulting to width {DEFAULT_WALL_WIDTH} ...")
+ else:
+ wall_width = walls[0].Width
+
+
center2corner = App.Vector(-width/2, -wall_width/2, 0)
- rotation = App.Rotation(App.Vector(0, 0, 1), math.degrees(-angle))
+ rotation = App.Rotation(Z_NORM, math.degrees(ang_sh2fc(angle)))
center2corner = rotation.multVec(center2corner)
-
corner = center.add(center2corner)
+
pl = App.Placement(
corner, # translation
- App.Rotation(math.degrees(-angle), 0, 90), # rotation
+ App.Rotation(math.degrees(ang_sh2fc(angle)), 0, 90), # rotation
ORIGIN # rotation@coordinate
)
@@ -1398,15 +1729,18 @@ class DoorOrWindowHandler(BaseFurnitureHandler):
_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
+ # See the https://wiki.freecad.org/Arch_Window for details about these values
+ h1 = 50
+ h2 = 50
+ h3 = 50
+ o1 = 40
+ w1 = float(wall_width)-o1 # make sure the door takes the whole wall (facebinder+baseboard)
+ w2 = 40
+ o2 = (w1-w2) / 2
window = Arch.makeWindowPreset(windowtype, width, height, h1, h2, h3, w1, w2, o1, o2, pl)
window.IfcType = ifc_type
+
+ mirrored = bool(elm.get('modelMirrored', False))
if ifc_type == 'Door' and mirrored:
window.OperationType = "SINGLE_SWING_RIGHT"
@@ -1415,26 +1749,24 @@ class DoorOrWindowHandler(BaseFurnitureHandler):
if mirrored:
window.ViewObject.Proxy.invertHinge()
- if wall:
- window.Hosts = [wall]
+ window.Hosts = walls
return window
- def _get_wall(self, point):
- """Returns the wall that contains the given point.
+ def _get_containing_walls(self, solid):
+ """Returns the wall(s) intersecting with the door/window.
Args:
- point (FreeCAD.Vector): the point to test for
+ solid (Part.Solid): the solid to test against each wall's
+ bounding box
Returns:
- Arch::Wall: the wall that contains the given point
+ list(Arch::Wall): the wall(s) containing the given solid
"""
- for wall in self.importer.walls:
- try:
- if wall.Shape.BoundBox.isInside(point):
- return wall
- except FloatingPointError:
- pass
- return None
+ host_walls = []
+ for wall in self.importer.get_walls():
+ if solid.common(wall.Shape).Volume > 0:
+ host_walls.append(wall)
+ return host_walls
class FurnitureHandler(BaseFurnitureHandler):
@@ -1460,7 +1792,11 @@ class FurnitureHandler(BaseFurnitureHandler):
feature = self.get_fc_object(furniture_id, 'pieceOfFurniture')
if not feature:
- feature = self._create_equipment(elm)
+ feature = self._create_equipment(floor, elm)
+
+ color = elm.get('color', self.importer.preferences["DEFAULT_FLOOR_COLOR"])
+ set_color_and_transparency(feature, color)
+
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)
@@ -1471,55 +1807,74 @@ class FurnitureHandler(BaseFurnitureHandler):
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)
+ if self.importer.preferences["CREATE_ARCH_EQUIPMENT"]:
+ p = feature.Shape.BoundBox.Center
+ else:
+ p = feature.Mesh.BoundBox.Center
+
+ space = self.get_space(floor, p)
+ if space:
+ space.Group = space.Group + [feature]
+ else:
+ _log(f"No space found to enclose {feature.Label}. Adding to generic group.")
+ 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'))
-
+ def _create_equipment(self, floor, elm):
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
+ height_in_plan = elm.get('heightInPlan', 0.0)
+ pitch = float(elm.get('pitch', 0.0)) # X SH3D Axis
+ roll = float(elm.get('roll', 0.0)) # Y SH3D Axis
+ angle = float(elm.get('angle', 0.0)) # Z SH3D Axis
name = elm.get('name')
+ model_rotation = elm.get('modelRotation', None)
mirrored = bool(elm.get('modelMirrored', "false") == "true")
- # The meshes are normalized, facing up.
+ # The meshes are normalized, centered, facing up.
# Center, Scale, X Rotation && Z Rotation (in FC axes), Move
mesh = self._get_mesh(elm)
bb = mesh.BoundBox
transform = App.Matrix()
+ # In FC the reference is the "upper left" corner
transform.move(-bb.Center)
- # NOTE: the model is facing up, thus y and z are inverted
+ if model_rotation:
+ rij = [ float(v) for v in model_rotation.split() ]
+ rotation = App.Rotation(
+ App.Vector(rij[0], rij[1], rij[2]),
+ App.Vector(rij[3], rij[4], rij[5]),
+ App.Vector(rij[6], rij[7], rij[8])
+ )
+ _msg(f"model_rotation is not yet implemented ...")
transform.scale(width/bb.XLength, height/bb.YLength, depth/bb.ZLength)
+ # NOTE: the model is facing up, thus y and z are inverted
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))
+ transform.rotateZ(ang_sh2fc(angle))
+
mesh.transform(transform)
if self.importer.preferences["CREATE_ARCH_EQUIPMENT"]:
shape = Part.Shape()
- shape.makeShapeFromMesh(mesh.Topology, 0.100000)
+ shape.makeShapeFromMesh(mesh.Topology, 1)
equipment = Arch.makeEquipment(name=name)
equipment.Shape = shape
- equipment.purgeTouched()
else:
equipment = App.ActiveDocument.addObject("Mesh::Feature", name)
equipment.Mesh = mesh
+ equipment.Placement.Base = coord_sh2fc(App.Vector(x, y, z))
+ equipment.Placement.Base.z += floor.Placement.Base.z
+ equipment.Placement.Base.z += mesh.BoundBox.ZLength / 2
+
return equipment
@@ -1674,7 +2029,7 @@ def coord_sh2fc(vector):
return App.Vector(vector.x*FACTOR, -vector.y*FACTOR, vector.z*FACTOR)
-def ang_sh2fc(angle):
+def ang_sh2fc(angle:float):
"""Convert SweetHome angle (º) to FreeCAD angle (º)
SweetHome angles are clockwise positive while FreeCAD are anti-clockwise
@@ -1706,13 +2061,40 @@ def color_fc2sh(hexcode):
def hex2rgb(hexcode):
# We might have transparency as the first 2 digit
+ if isinstance(hexcode, list) or isinstance(hexcode, tuple):
+ return hexcode
+ if not isinstance(hexcode, str):
+ assert False, "Invalid type when calling hex2rgb(), was expecting a list, tuple or string. Got "+str(hexcode)
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+2:offset+4], 16), # Green
int(hexcode[offset+4:offset+6], 16) # Blue
)
def _hex2transparency(hexcode):
+ if not isinstance(hexcode, str):
+ assert False, "Invalid type when calling _hex2transparency(), was expecting a list, tuple or string. Got "+str(hexcode)
return 100 - int(int(hexcode[0:2], 16) * 100 / 255)
+
+
+def _color_section(section):
+ 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
+
+
+def set_shininess(obj, shininess):
+ if not App.GuiUp or not shininess:
+ return
+ if hasattr(obj.ViewObject, "Shininess"):
+ # Shininess goes from 0 -> 0.25 in SH3d and 0 -> 100 in FC
+ obj.ViewObject.Shininess = int((100*shininess)/0.25)
diff --git a/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPy.xml b/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPy.xml
index 12b4675667..618ce2a588 100644
--- a/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPy.xml
+++ b/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPy.xml
@@ -14,7 +14,10 @@
Delete="true">
- Describes a portion of a circle
+ Low level API to create a PipeShell using OCC API
+
+ Ref: https://dev.opencascade.org/doc/refman/html/class_b_rep_offset_a_p_i___make_pipe_shell.html
+
diff --git a/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPyImp.cpp b/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPyImp.cpp
index a698415c22..9167f3bb0c 100644
--- a/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPyImp.cpp
+++ b/src/Mod/Part/App/BRepOffsetAPI_MakePipeShellPyImp.cpp
@@ -182,13 +182,13 @@ PyObject* BRepOffsetAPI_MakePipeShellPy::setAuxiliarySpine(PyObject *args)
PyObject* BRepOffsetAPI_MakePipeShellPy::add(PyObject *args, PyObject *kwds)
{
- PyObject *prof, *curv=Py_False, *keep=Py_False;
+ PyObject *profile, *withContact=Py_False, *withCorrection=Py_False;
static const std::array keywords_pro{"Profile", "WithContact", "WithCorrection", nullptr};
- if (Base::Wrapped_ParseTupleAndKeywords(args, kwds, "O!|O!O!", keywords_pro, &Part::TopoShapePy::Type, &prof,
- &PyBool_Type, &curv, &PyBool_Type, &keep)) {
+ if (Base::Wrapped_ParseTupleAndKeywords(args, kwds, "O!|O!O!", keywords_pro, &Part::TopoShapePy::Type, &profile,
+ &PyBool_Type, &withContact, &PyBool_Type, &withCorrection)) {
try {
- const TopoDS_Shape& s = static_cast(prof)->getTopoShapePtr()->getShape();
- this->getBRepOffsetAPI_MakePipeShellPtr()->Add(s, Base::asBoolean(curv), Base::asBoolean(keep));
+ const TopoDS_Shape& s = static_cast(profile)->getTopoShapePtr()->getShape();
+ this->getBRepOffsetAPI_MakePipeShellPtr()->Add(s, Base::asBoolean(withContact), Base::asBoolean(withCorrection));
Py_Return;
}
catch (Standard_Failure& e) {
@@ -198,16 +198,16 @@ PyObject* BRepOffsetAPI_MakePipeShellPy::add(PyObject *args, PyObject *kwds)
}
PyErr_Clear();
- PyObject *loc;
+ PyObject *location;
static const std::array keywords_loc{"Profile", "Location", "WithContact", "WithCorrection",
nullptr};
- if (Base::Wrapped_ParseTupleAndKeywords(args, kwds, "O!O!|O!O!", keywords_loc, &Part::TopoShapePy::Type, &prof,
- &Part::TopoShapeVertexPy::Type, &loc, &PyBool_Type, &curv, &PyBool_Type,
- &keep)) {
+ if (Base::Wrapped_ParseTupleAndKeywords(args, kwds, "O!O!|O!O!", keywords_loc, &Part::TopoShapePy::Type, &profile,
+ &Part::TopoShapeVertexPy::Type, &location, &PyBool_Type, &withContact, &PyBool_Type,
+ &withCorrection)) {
try {
- const TopoDS_Shape& s = static_cast(prof)->getTopoShapePtr()->getShape();
- const TopoDS_Vertex& v = TopoDS::Vertex(static_cast(loc)->getTopoShapePtr()->getShape());
- this->getBRepOffsetAPI_MakePipeShellPtr()->Add(s, v, Base::asBoolean(curv), Base::asBoolean(keep));
+ const TopoDS_Shape& s = static_cast(profile)->getTopoShapePtr()->getShape();
+ const TopoDS_Vertex& v = TopoDS::Vertex(static_cast(location)->getTopoShapePtr()->getShape());
+ this->getBRepOffsetAPI_MakePipeShellPtr()->Add(s, v, Base::asBoolean(withContact), Base::asBoolean(withCorrection));
Py_Return;
}
catch (Standard_Failure& e) {