From a7b3fabac03cf9921624697c8d38267049154234 Mon Sep 17 00:00:00 2001 From: Roy-043 Date: Thu, 9 Feb 2023 15:51:17 +0100 Subject: [PATCH 1/2] [Arch] fix Arch_Site --- src/Mod/Arch/ArchSite.py | 264 +++++++++++++++++++++++---------------- 1 file changed, 156 insertions(+), 108 deletions(-) diff --git a/src/Mod/Arch/ArchSite.py b/src/Mod/Arch/ArchSite.py index 8edeb8d792..17115def1b 100644 --- a/src/Mod/Arch/ArchSite.py +++ b/src/Mod/Arch/ArchSite.py @@ -618,7 +618,7 @@ class _Site(ArchIFC.IfcProduct): if not "ProjectedArea" in pl: obj.addProperty("App::PropertyArea","ProjectedArea","Site",QT_TRANSLATE_NOOP("App::Property","The area of the projection of this object onto the XY plane")) if not "Perimeter" in pl: - obj.addProperty("App::PropertyLength","Perimeter","Site",QT_TRANSLATE_NOOP("App::Property","The perimeter length of this terrain")) + obj.addProperty("App::PropertyLength","Perimeter","Site",QT_TRANSLATE_NOOP("App::Property","The perimeter length of the projected area")) if not "AdditionVolume" in pl: obj.addProperty("App::PropertyVolume","AdditionVolume","Site",QT_TRANSLATE_NOOP("App::Property","The volume of earth to be added to this terrain")) if not "SubtractionVolume" in pl: @@ -650,8 +650,6 @@ class _Site(ArchIFC.IfcProduct): def execute(self,obj): """Method run when the object is recomputed. - If the site has no Shape or Terrain property assigned, do nothing. - Perform additions and subtractions on terrain, and assign to the site's Shape. """ @@ -659,48 +657,66 @@ class _Site(ArchIFC.IfcProduct): if not hasattr(obj,'Shape'): # old-style Site return - pl = obj.Placement + pl = FreeCAD.Placement(obj.Placement) shape = None - if obj.Terrain: - if hasattr(obj.Terrain,'Shape'): - if obj.Terrain.Shape: - if not obj.Terrain.Shape.isNull(): - shape = obj.Terrain.Shape.copy() + if obj.Terrain is not None \ + and hasattr(obj.Terrain,'Shape') \ + and not obj.Terrain.Shape.isNull() \ + and obj.Terrain.Shape.isValid(): + shape = obj.Terrain.Shape.copy() + shape = shape.transformGeometry(shape.Placement.Matrix) + shape.Placement = FreeCAD.Placement() - if shape: - shells = [] - for sub in obj.Subtractions: - if hasattr(sub,'Shape'): - if sub.Shape: - if sub.Shape.Solids: - for sol in sub.Shape.Solids: - rest = shape.cut(sol) - shells.append(sol.Shells[0].common(shape.extrude(obj.ExtrusionVector))) - shape = rest - for sub in obj.Additions: - if hasattr(sub,'Shape'): - if sub.Shape: - if sub.Shape.Solids: - for sol in sub.Shape.Solids: - rest = shape.cut(sol) - shells.append(sol.Shells[0].cut(shape.extrude(obj.ExtrusionVector))) - shape = rest - if not shape.isNull(): - if shape.isValid(): - for shell in shells: - shape = shape.fuse(shell) - if obj.RemoveSplitter: - shape = shape.removeSplitter() - obj.Shape = shape - if not pl.isNull(): - obj.Placement = pl - self.computeAreas(obj) + if shape.Solids: + for sub in obj.Additions: + if hasattr(sub,'Shape') and sub.Shape and sub.Shape.Solids: + for sol in sub.Shape.Solids: + shape = shape.fuse(sol) + for sub in obj.Subtractions: + if hasattr(sub,'Shape') and sub.Shape and sub.Shape.Solids: + for sol in sub.Shape.Solids: + shape = shape.cut(sol) + elif shape.Faces: + shells = [] + for sub in obj.Additions: + if hasattr(sub,'Shape') and sub.Shape and sub.Shape.Solids: + for sol in sub.Shape.Solids: + rest = shape.cut(sol) + shells.append(sol.Shells[0].cut(shape.extrude(obj.ExtrusionVector))) + shape = rest + for sub in obj.Subtractions: + if hasattr(sub,'Shape') and sub.Shape and sub.Shape.Solids: + for sol in sub.Shape.Solids: + rest = shape.cut(sol) + shells.append(sol.Shells[0].common(shape.extrude(obj.ExtrusionVector))) + shape = rest + for shell in shells: + shape = shape.fuse(shell) + + if not shape.isNull() and shape.isValid(): + if obj.RemoveSplitter: + shape = shape.removeSplitter() + obj.Shape = shape.transformGeometry(pl.inverse().Matrix) + obj.Placement = pl + else: + shape = None + + if shape is None: + import Part + obj.Shape = Part.Shape() + obj.Placement = pl + + self.computeAreas(obj) + + if FreeCAD.GuiUp: + vobj = obj.ViewObject + if vobj.Proxy is not None: + vobj.Proxy.updateDisplaymodeTerrainSwitches(vobj) def onChanged(self,obj,prop): """Method called when the object has a property changed. - If Terrain has changed, hide the base object terrain, then run - .execute(). + If Terrain has changed, hide the base object terrain. Also call ArchIFC.IfcProduct.onChanged(). @@ -715,88 +731,76 @@ class _Site(ArchIFC.IfcProduct): if obj.Terrain: if FreeCAD.GuiUp: obj.Terrain.ViewObject.hide() - self.execute(obj) def computeAreas(self,obj): """Compute the area, perimeter length, and volume of the terrain shape. - Compute the area of the terrain projected onto an XY hyperplane, IE: + Compute the area of the terrain projected onto an XY plane, IE: the area of the terrain if viewed from a birds eye view. Compute the length of the perimeter of this birds eye view area. - Compute the volume of the terrain that needs to be subtracted and - added on account of the Additions and Subtractions to the site. + Compute the volume of the terrain that needs to be added and subtracted + on account of the Additions and Subtractions of the site. Assign these values to their respective site properties. """ - if not obj.Shape: - return - if obj.Shape.isNull(): - return - if not obj.Shape.isValid(): - return - if not obj.Shape.Faces: - return if not hasattr(obj,"Perimeter"): # check we have a latest version site return - if not obj.Terrain: + + if not obj.Shape.Faces: + if obj.ProjectedArea.Value != 0: + obj.ProjectedArea = 0 + if obj.Perimeter.Value != 0: + obj.Perimeter = 0 + if obj.AdditionVolume.Value != 0: + obj.AdditionVolume = 0 + if obj.SubtractionVolume.Value != 0: + obj.SubtractionVolume = 0 return + import TechDraw, Part + area = 0 + perim = 0 + addvol = 0 + subvol = 0 + edges = [] + + for face in obj.Shape.Faces: + if face.normalAt(0,0).getAngle(FreeCAD.Vector(0,0,1)) < 1.5707: + edges.extend(TechDraw.project(face,FreeCAD.Vector(0,0,1))[0].Edges) + outer = TechDraw.findOuterWire(edges) + # compute area - fset = [] - for f in obj.Shape.Faces: - if f.normalAt(0,0).getAngle(FreeCAD.Vector(0,0,1)) < 1.5707: - fset.append(f) - if fset: - import TechDraw, Part - pset = [] - for f in fset: - try: - pf = Part.Face(Part.Wire(TechDraw.project(f,FreeCAD.Vector(0,0,1))[0].Edges)) - except Part.OCCError: - # error in computing the area. Better set it to zero than show a wrong value - if obj.ProjectedArea.Value != 0: - print("Error computing areas for ",obj.Label) - obj.ProjectedArea = 0 - else: - pset.append(pf) - if pset: - self.flatarea = pset.pop() - for f in pset: - self.flatarea = self.flatarea.fuse(f) - self.flatarea = self.flatarea.removeSplitter() - if obj.ProjectedArea.Value != self.flatarea.Area: - obj.ProjectedArea = self.flatarea.Area + try: + area = Part.Face(outer).Area # outer.Area does not always work. + except Part.OCCError: + print("Error computing areas for", obj.Label) + area = 0 # compute perimeter - lut = {} - for e in obj.Shape.Edges: - lut.setdefault(e.hashCode(),[]).append(e) - l = 0 - for e in lut.values(): - if len(e) == 1: # keep only border edges - l += e[0].Length - if l: - if obj.Perimeter.Value != l: - obj.Perimeter = l + perim = outer.Length # compute volumes if obj.Terrain.Shape.Solids: shapesolid = obj.Terrain.Shape.copy() else: shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector) - addvol = 0 - subvol = 0 - for sub in obj.Subtractions: - subvol += sub.Shape.common(shapesolid).Volume for sub in obj.Additions: addvol += sub.Shape.cut(shapesolid).Volume - if obj.SubtractionVolume.Value != subvol: - obj.SubtractionVolume = subvol + for sub in obj.Subtractions: + subvol += sub.Shape.common(shapesolid).Volume + + # update properties + if obj.ProjectedArea.Value != area: + obj.ProjectedArea = area + if obj.Perimeter.Value != perim: + obj.Perimeter = perim if obj.AdditionVolume.Value != addvol: obj.AdditionVolume = addvol + if obj.SubtractionVolume.Value != subvol: + obj.SubtractionVolume = subvol def addObject(self,obj,child): @@ -807,6 +811,14 @@ class _Site(ArchIFC.IfcProduct): g.append(child) obj.Group = g + def __getstate__(self): + + return None + + def __setstate__(self,state): + + return None + class _ViewProviderSite: """A View Provider for the Site object. @@ -858,10 +870,6 @@ class _ViewProviderSite: if not "UpdateDeclination" in pl: vobj.addProperty("App::PropertyBool", "UpdateDeclination", "Compass", QT_TRANSLATE_NOOP("App::Property", "Update the Declination value based on the compass rotation")) - def onDocumentRestored(self,vobj): - """Method run when the document is restored. Re-add the Arch component properties.""" - self.setProperties(vobj) - def getIcon(self): """Return the path to the appropriate icon. @@ -956,18 +964,8 @@ class _ViewProviderSite: FreeCADGui.ActiveDocument.setEdit(self.Object, 1) def attach(self,vobj): - """Add display modes' data to the coin scenegraph. - - Add each display mode as a coin node, whose parent is this view - provider. - - Each display mode's node includes the data needed to display the object - in that mode. This might include colors of faces, or the draw style of - lines. This data is stored as additional coin nodes which are children - of the display mode node. - - Doe not add display modes, but do add the solar diagram and compass to - the scenegraph. + """Adds the solar diagram and compass to the coin scenegraph, but does + not add display modes. """ self.Object = vobj.Object @@ -1023,8 +1021,58 @@ class _ViewProviderSite: elif prop == "ProjectedArea": self.updateCompassScale(obj.ViewObject) + def addDisplaymodeTerrainSwitches(self,vobj): + """Adds 'terrain' switches to the 4 default display modes. + + If the Terrain property of the site is None, the 'normal' display can + be switched off with these switches. This avoids 'ghosts' of the objects + in the Group property. + See: + https://forum.freecad.org/viewtopic.php?f=10&t=74731 + https://forum.freecad.org/viewtopic.php?t=75658 + https://forum.freecad.org/viewtopic.php?t=75883 + """ + + if not hasattr(self, "terrain_switches"): + main_switch = vobj.RootNode.getChild(2) # The display mode switch. + if main_switch.getNumChildren() == 4: # Check if all display modes are available. + from pivy import coin + self.terrain_switches = [] + for node in tuple(main_switch.getChildren()): + new_switch = coin.SoSwitch() + sep1 = coin.SoSeparator() + sep1.setName("NoTerrain") + sep2 = coin.SoSeparator() + sep2.setName("Terrain") + child_list = list(node.getChildren()) + for child in child_list: + sep2.addChild(child) + new_switch.addChild(sep1) + new_switch.addChild(sep2) + new_switch.whichChild = 0 + node.addChild(new_switch) + for i in range(len(child_list)): + node.removeChild(0) # Remove the original children. + self.terrain_switches.append(new_switch) + + def updateDisplaymodeTerrainSwitches(self,vobj): + """Updates the 'terrain' switches.""" + + if not hasattr(self, "terrain_switches"): + return + + idx = 0 if self.Object.Terrain is None else 1 + for switch in self.terrain_switches: + switch.whichChild = idx + def onChanged(self,vobj,prop): + # onChanged is called multiple times when a document is opened. + # Some display mode nodes can be missing during initial calls. + # The two methods called below must take that into account. + self.addDisplaymodeTerrainSwitches(vobj) + self.updateDisplaymodeTerrainSwitches(vobj) + if prop == "SolarDiagramPosition": if hasattr(vobj,"SolarDiagramPosition"): p = vobj.SolarDiagramPosition From 72849c53d52c11d158e3aced1413bdf429152b0d Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sun, 12 Feb 2023 22:21:03 +0100 Subject: [PATCH 2/2] [Arch] fix Arch_Site: fix transform issue --- src/Mod/Arch/ArchSite.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Mod/Arch/ArchSite.py b/src/Mod/Arch/ArchSite.py index 17115def1b..c4b16cb9df 100644 --- a/src/Mod/Arch/ArchSite.py +++ b/src/Mod/Arch/ArchSite.py @@ -657,14 +657,18 @@ class _Site(ArchIFC.IfcProduct): if not hasattr(obj,'Shape'): # old-style Site return + import Part pl = FreeCAD.Placement(obj.Placement) shape = None if obj.Terrain is not None \ and hasattr(obj.Terrain,'Shape') \ and not obj.Terrain.Shape.isNull() \ and obj.Terrain.Shape.isValid(): - shape = obj.Terrain.Shape.copy() - shape = shape.transformGeometry(shape.Placement.Matrix) + shape = Part.Shape(obj.Terrain.Shape) + # Fuse and cut operations return a shape with a default placement. + # We need to transform our shape accordingly to get a consistent + # result with or without fuse or cut operations: + shape = shape.transformGeometry((shape.Placement * pl).Matrix) shape.Placement = FreeCAD.Placement() if shape.Solids: @@ -696,13 +700,14 @@ class _Site(ArchIFC.IfcProduct): if not shape.isNull() and shape.isValid(): if obj.RemoveSplitter: shape = shape.removeSplitter() + # Transform the shape to counteract the effect of placement pl + # and then apply that placement: obj.Shape = shape.transformGeometry(pl.inverse().Matrix) obj.Placement = pl else: shape = None if shape is None: - import Part obj.Shape = Part.Shape() obj.Placement = pl @@ -783,14 +788,14 @@ class _Site(ArchIFC.IfcProduct): perim = outer.Length # compute volumes - if obj.Terrain.Shape.Solids: - shapesolid = obj.Terrain.Shape.copy() - else: - shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector) + shape = Part.Shape(obj.Terrain.Shape) + shape.Placement = obj.Placement * shape.Placement + if not obj.Terrain.Shape.Solids: + shape = shape.extrude(obj.ExtrusionVector) for sub in obj.Additions: - addvol += sub.Shape.cut(shapesolid).Volume + addvol += sub.Shape.cut(shape).Volume for sub in obj.Subtractions: - subvol += sub.Shape.common(shapesolid).Volume + subvol += sub.Shape.common(shape).Volume # update properties if obj.ProjectedArea.Value != area: