FreeCAD / src /Mod /BIM /ArchFence.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# SPDX-License-Identifier: LGPL-2.1-or-later
# *****************************************************************************
# * *
# * Copyright (c) 2019 furti <daniel.furtlehner@gmx.net> *
# * *
# * 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 *
# * <https://www.gnu.org/licenses/>. *
# * *
# *****************************************************************************
# 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