392 lines
14 KiB
Python
392 lines
14 KiB
Python
#*****************************************************************************
|
|
#* Copyright (c) 2019 furti <daniel.furtlehner@gmx.net> *
|
|
#* *
|
|
#* This program is free software; you can redistribute it and/or modify *
|
|
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
#* as published by the Free Software Foundation; either version 2 of *
|
|
#* the License, or (at your option) any later version. *
|
|
#* for detail see the LICENCE text file. *
|
|
#* *
|
|
#* This program 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 Library General Public License for more details. *
|
|
#* *
|
|
#* You should have received a copy of the GNU Library General Public *
|
|
#* License along with this program; if not, write to the Free Software *
|
|
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
#* USA *
|
|
#* *
|
|
#*****************************************************************************
|
|
|
|
# Fence functionality for the Arch Workbench
|
|
|
|
import math
|
|
|
|
import FreeCAD
|
|
import ArchComponent
|
|
import draftobjects.patharray as patharray
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
import PySide.QtGui as QtGui
|
|
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.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"))
|
|
|
|
if not "Post" in pl:
|
|
obj.addProperty("App::PropertyLink", "Post", "Fence", QT_TRANSLATE_NOOP(
|
|
"App::Property", "A single fence post"))
|
|
|
|
if not "Path" in pl:
|
|
obj.addProperty("App::PropertyLink", "Path", "Fence", QT_TRANSLATE_NOOP(
|
|
"App::Property", "The Path the fence should follow"))
|
|
|
|
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"))
|
|
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"))
|
|
obj.setEditorMode("NumberOfPosts", 1)
|
|
|
|
self.Type = "Fence"
|
|
|
|
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]
|
|
|
|
return None
|
|
|
|
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."))
|
|
|
|
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
|