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) {