# SPDX-License-Identifier: LGPL-2.1-or-later # ***************************************************************************** # * * # * Copyright (c) 2019 furti * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD is distributed in the hope that it will be useful, but * # * WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # ***************************************************************************** # Fence functionality for the Arch Workbench import math import FreeCAD import ArchComponent import draftobjects.patharray as patharray if FreeCAD.GuiUp: from PySide.QtCore import QT_TRANSLATE_NOOP import PySide.QtGui as QtGui import FreeCADGui else: # \cond def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond EAST = FreeCAD.Vector(1, 0, 0) class _Fence(ArchComponent.Component): def __init__(self, obj): ArchComponent.Component.__init__(self, obj) self.Type = "Fence" self.setProperties(obj) # Does a IfcType exist? # obj.IfcType = "Fence" obj.MoveWithHost = False def setProperties(self, obj): ArchComponent.Component.setProperties(self, obj) pl = obj.PropertiesList if not "Section" in pl: obj.addProperty( "App::PropertyLink", "Section", "Fence", QT_TRANSLATE_NOOP("App::Property", "A single section of the fence"), locked=True, ) if not "Post" in pl: obj.addProperty( "App::PropertyLink", "Post", "Fence", QT_TRANSLATE_NOOP("App::Property", "A single fence post"), locked=True, ) if not "Path" in pl: obj.addProperty( "App::PropertyLink", "Path", "Fence", QT_TRANSLATE_NOOP("App::Property", "The Path the fence should follow"), locked=True, ) if not "NumberOfSections" in pl: obj.addProperty( "App::PropertyInteger", "NumberOfSections", "Fence", QT_TRANSLATE_NOOP("App::Property", "The number of sections the fence is built of"), locked=True, ) obj.setEditorMode("NumberOfSections", 1) if not "NumberOfPosts" in pl: obj.addProperty( "App::PropertyInteger", "NumberOfPosts", "Fence", QT_TRANSLATE_NOOP("App::Property", "The number of posts used to build the fence"), locked=True, ) obj.setEditorMode("NumberOfPosts", 1) def dumps(self): if hasattr(self, "sectionFaceNumbers"): return self.sectionFaceNumbers return None def loads(self, state): if state is not None and isinstance(state, tuple): self.sectionFaceNumbers = state[0] self.Type = "Fence" def execute(self, obj): import Part pathwire = self.calculatePathWire(obj) if not pathwire: FreeCAD.Console.PrintLog("ArchFence.execute: path " + obj.Path.Name + " has no edges\n") return if not obj.Section: FreeCAD.Console.PrintLog("ArchFence.execute: Section not set\n") return if not obj.Post: FreeCAD.Console.PrintLog("ArchFence.execute: Post not set\n") return pathLength = pathwire.Length sectionLength = obj.Section.Shape.BoundBox.XMax postLength = obj.Post.Shape.BoundBox.XMax obj.NumberOfSections = self.calculateNumberOfSections(pathLength, sectionLength, postLength) obj.NumberOfPosts = obj.NumberOfSections + 1 # We assume that the section was drawn in front view. # We have to rotate the shape down so that it is aligned # correctly by the algorithm later on downRotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), -90) postPlacements = self.calculatePostPlacements(obj, pathwire, downRotation) postShapes = self.calculatePosts(obj, postPlacements) sectionShapes, sectionFaceNumbers = self.calculateSections( obj, postPlacements, postLength, sectionLength ) allShapes = [] allShapes.extend(postShapes) allShapes.extend(sectionShapes) compound = Part.makeCompound(allShapes) self.sectionFaceNumbers = sectionFaceNumbers obj.Shape = compound def calculateNumberOfSections(self, pathLength, sectionLength, postLength): realSectionLength = sectionLength + postLength return math.ceil(pathLength / realSectionLength) def calculatePostPlacements(self, obj, pathwire, rotation): postWidth = obj.Post.Shape.BoundBox.YMax # We want to center the posts on the path. So move them the half width in transformationVector = FreeCAD.Vector(0, -postWidth / 2, 0) return patharray.placements_on_path( rotation, pathwire, obj.NumberOfPosts, transformationVector, True ) def calculatePosts(self, obj, postPlacements): posts = [] for placement in postPlacements: postCopy = obj.Post.Shape.copy() postCopy.Placement = placement posts.append(postCopy) return posts def calculateSections(self, obj, postPlacements, postLength, sectionLength): import Part shapes = [] # For the colorization algorithm we have to store the number of faces for each section # It is possible that a section is clipped. Then the number of faces is not equal to the # number of faces in the original section faceNumbers = [] for i in range(obj.NumberOfSections): startPlacement = postPlacements[i] endPlacement = postPlacements[i + 1] sectionLine = Part.LineSegment(startPlacement.Base, endPlacement.Base) sectionBase = sectionLine.value(postLength) if startPlacement.Rotation.isSame(endPlacement.Rotation): sectionRotation = endPlacement.Rotation else: direction = endPlacement.Base.sub(startPlacement.Base) sectionRotation = FreeCAD.Rotation(EAST, direction) placement = FreeCAD.Placement() placement.Base = sectionBase placement.Rotation = sectionRotation sectionCopy = obj.Section.Shape.copy() if sectionLength > sectionLine.length() - postLength: # Part.show(Part.Shape([sectionLine]), 'line') sectionCopy = self.clipSection( sectionCopy, sectionLength, sectionLine.length() - postLength ) sectionCopy = Part.Compound( [sectionCopy] ) # nest in compound to ensure correct Placement sectionCopy.Placement = placement shapes.append(sectionCopy) faceNumbers.append(len(sectionCopy.Faces)) return (shapes, faceNumbers) def clipSection(self, shape, length, clipLength): import Part boundBox = shape.BoundBox lengthToCut = length - clipLength halfLengthToCut = lengthToCut / 2 leftBox = Part.makeBox( halfLengthToCut, boundBox.YMax + 1, boundBox.ZMax + 1, FreeCAD.Vector(boundBox.XMin, boundBox.YMin, boundBox.ZMin), ) rightBox = Part.makeBox( halfLengthToCut, boundBox.YMax + 1, boundBox.ZMax + 1, FreeCAD.Vector( boundBox.XMin + halfLengthToCut + clipLength, boundBox.YMin, boundBox.ZMin ), ) newShape = shape.cut([leftBox, rightBox]) newBoundBox = newShape.BoundBox newShape.translate(FreeCAD.Vector(-newBoundBox.XMin, 0, 0)) return newShape.removeSplitter() def calculatePathWire(self, obj): if hasattr(obj.Path.Shape, "Wires") and obj.Path.Shape.Wires: return obj.Path.Shape.Wires[0] elif obj.Path.Shape.Edges: return Part.Wire(obj.Path.Shape.Edges) return None class _ViewProviderFence(ArchComponent.ViewProviderComponent): "A View Provider for the Fence object" def __init__(self, vobj): ArchComponent.ViewProviderComponent.__init__(self, vobj) # setProperties of ArchComponent will be overwritten # thus setProperties from ArchComponent will be explicit called to get the properties ArchComponent.ViewProviderComponent.setProperties(self, vobj) self.setProperties(vobj) def setProperties(self, vobj): pl = vobj.PropertiesList if not "UseOriginalColors" in pl: vobj.addProperty( "App::PropertyBool", "UseOriginalColors", "Fence", QT_TRANSLATE_NOOP( "App::Property", "When true, the fence will be colored like the original post and section.", ), locked=True, ) def attach(self, vobj): self.setProperties(vobj) return super().attach(vobj) def getIcon(self): import Arch_rc return ":/icons/Arch_Fence_Tree.svg" def claimChildren(self): children = [] if self.Object.Section: children.append(self.Object.Section) if self.Object.Post: children.append(self.Object.Post) if self.Object.Path: children.append(self.Object.Path) return children def updateData(self, obj, prop): colorProps = ["Shape", "Section", "Post", "Path"] if prop in colorProps: self.applyColors(obj) else: super().updateData(obj, prop) def onChanged(self, vobj, prop): if prop == "UseOriginalColors": self.applyColors(vobj.Object) else: super().onChanged(vobj, prop) def applyColors(self, obj): # Note that the clipSection function changes the face numbering of the # fence section. This happens even if the total number of faces does not # change. If UseOriginalColors is True, the end result of this function # will only be correct if all faces of the section have the same color. vobj = obj.ViewObject if not vobj.UseOriginalColors: vobj.ShapeAppearance = [vobj.ShapeAppearance[0]] else: post = obj.Post section = obj.Section # If post and/or section are Std_Parts they may not have a Shape attr (yet): if not hasattr(post, "Shape"): return if not hasattr(section, "Shape"): return numberOfPostFaces = len(post.Shape.Faces) numberOfSectionFaces = len(section.Shape.Faces) if hasattr(obj.Proxy, "sectionFaceNumbers"): sectionFaceNumbers = obj.Proxy.sectionFaceNumbers else: sectionFaceNumbers = [0] if numberOfPostFaces == 0 or sum(sectionFaceNumbers) == 0: return postColors = self.normalizeColors(post, numberOfPostFaces) defaultSectionColors = self.normalizeColors(section, numberOfSectionFaces) ownColors = [] # At first all posts are added to the shape for i in range(obj.NumberOfPosts): ownColors.extend(postColors) # Next all sections are added for i in range(obj.NumberOfSections): actualSectionFaceCount = sectionFaceNumbers[i] if actualSectionFaceCount == numberOfSectionFaces: ownColors.extend(defaultSectionColors) else: ownColors.extend(self.normalizeColors(section, actualSectionFaceCount)) vobj.DiffuseColor = ownColors def normalizeColors(self, obj, numberOfFaces): if obj.TypeId == "PartDesign::Body": # When colorizing a PartDesign Body we have two options # 1. The whole body got a shape color, that means the tip has only a single diffuse color set # so we use the shape color of the body # 2. "Set colors" was called on the tip and the individual faces where colorized. # We use the diffuseColors of the tip in that case if len(obj.Tip.ViewObject.DiffuseColor) > 1: colors = obj.Tip.ViewObject.DiffuseColor else: colors = obj.ViewObject.DiffuseColor else: import Draft colors = Draft.get_diffuse_color(obj) # To handle Std_Parts for example. numberOfColors = len(colors) if numberOfColors == 1: return colors * numberOfFaces if numberOfColors == numberOfFaces: return colors # It is possible, that we have fewer faces than colors when something # got clipped. Remove the unneeded colors at the beginning and end. # Even if clipSection did not change the face numbering this code would # not work properly. halfNumberOfFacesToRemove = (numberOfColors - numberOfFaces) / 2 start = int(math.ceil(halfNumberOfFacesToRemove)) end = start + numberOfFaces return colors[start:end] def hide(obj): if hasattr(obj, "ViewObject") and obj.ViewObject: obj.ViewObject.Visibility = False