Removes unused imports as reported by LGTM. There are exceptions: `import Arch_rc` is shown as an alert, but has side effects. It's not clear what the best thing to do in those cases is, so I've left them for now.
507 lines
17 KiB
Python
507 lines
17 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 __getstate__(self):
|
|
if hasattr(self, 'sectionFaceNumbers'):
|
|
return (self.sectionFaceNumbers)
|
|
|
|
return None
|
|
|
|
def __setstate__(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):
|
|
withoutLastPost = pathLength - postLength
|
|
realSectionLength = sectionLength + postLength
|
|
|
|
return math.ceil(withoutLastPost / 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)
|
|
|
|
placements = patharray.placements_on_path(rotation, pathwire,
|
|
obj.NumberOfSections + 1,
|
|
transformationVector, True)
|
|
|
|
# The placement of the last object is always the second entry in the list.
|
|
# So we move it to the end
|
|
placements.append(placements.pop(1))
|
|
|
|
return placements
|
|
|
|
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():
|
|
# Part.show(Part.Shape([sectionLine]), 'line')
|
|
sectionCopy = self.clipSection(
|
|
sectionCopy, sectionLength, sectionLine.length() - postLength)
|
|
|
|
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):
|
|
if not hasattr(obj.ViewObject, "UseOriginalColors") or not obj.ViewObject.UseOriginalColors:
|
|
obj.ViewObject.DiffuseColor = [obj.ViewObject.ShapeColor]
|
|
else:
|
|
post = obj.Post
|
|
section = obj.Section
|
|
|
|
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))
|
|
|
|
viewObject = obj.ViewObject
|
|
viewObject.DiffuseColor = ownColors
|
|
|
|
def normalizeColors(self, obj, numberOfFaces):
|
|
colors = obj.ViewObject.DiffuseColor
|
|
|
|
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
|
|
tipColors = obj.Tip.ViewObject.DiffuseColor
|
|
|
|
if len(tipColors) > 1:
|
|
colors = tipColors
|
|
|
|
numberOfColors = len(colors)
|
|
|
|
if numberOfColors == 1:
|
|
return colors * numberOfFaces
|
|
|
|
colorsToUse = colors.copy()
|
|
|
|
if numberOfColors == numberOfFaces:
|
|
return colorsToUse
|
|
else:
|
|
# It is possible, that we have less faces than colors when something got clipped.
|
|
# Remove the unneeded colors at the beginning and end
|
|
halfNumberOfFacesToRemove = (numberOfColors - numberOfFaces) / 2
|
|
start = int(math.ceil(halfNumberOfFacesToRemove))
|
|
end = start + numberOfFaces
|
|
|
|
return colorsToUse[start:end]
|
|
|
|
|
|
class _CommandFence:
|
|
"the Arch Fence command definition"
|
|
|
|
def GetResources(self):
|
|
return {'Pixmap': 'Arch_Fence',
|
|
'MenuText': QT_TRANSLATE_NOOP("Arch_Fence", "Fence"),
|
|
'ToolTip': QT_TRANSLATE_NOOP("Arch_Fence", "Creates a fence object from a selected section, post and path")}
|
|
|
|
def IsActive(self):
|
|
return not FreeCAD.ActiveDocument is None
|
|
|
|
def Activated(self):
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
|
|
if len(sel) != 3:
|
|
QtGui.QMessageBox.information(QtGui.QApplication.activeWindow(
|
|
), 'Arch Fence selection', 'Select a section, post and path in exactly this order to build a fence.')
|
|
|
|
return
|
|
|
|
section = sel[0]
|
|
post = sel[1]
|
|
path = sel[2]
|
|
|
|
makeFence(section, post, path)
|
|
|
|
|
|
def makeFence(section, post, path):
|
|
obj = FreeCAD.ActiveDocument.addObject(
|
|
'Part::FeaturePython', 'Fence')
|
|
|
|
_Fence(obj)
|
|
obj.Section = section
|
|
obj.Post = post
|
|
obj.Path = path
|
|
|
|
if FreeCAD.GuiUp:
|
|
_ViewProviderFence(obj.ViewObject)
|
|
|
|
hide(section)
|
|
hide(post)
|
|
hide(path)
|
|
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
return obj
|
|
|
|
|
|
def hide(obj):
|
|
if hasattr(obj, 'ViewObject') and obj.ViewObject:
|
|
obj.ViewObject.Visibility = False
|
|
|
|
|
|
if FreeCAD.GuiUp:
|
|
FreeCADGui.addCommand('Arch_Fence', _CommandFence())
|
|
|
|
if __name__ == '__main__':
|
|
# For testing purposes. When someone runs the File as a macro a default fence will be generated
|
|
import Part
|
|
|
|
def buildSection():
|
|
parts = []
|
|
|
|
parts.append(Part.makeBox(
|
|
2000, 50, 30, FreeCAD.Vector(0, 0, 1000 - 30)))
|
|
parts.append(Part.makeBox(2000, 50, 30))
|
|
parts.append(Part.makeBox(20, 20, 1000 -
|
|
60, FreeCAD.Vector(0, 15, 30)))
|
|
parts.append(Part.makeBox(20, 20, 1000 - 60,
|
|
FreeCAD.Vector(1980, 15, 30)))
|
|
|
|
for i in range(8):
|
|
parts.append(Part.makeBox(20, 20, 1000 - 60,
|
|
FreeCAD.Vector((2000.0 / 9 * (i + 1)) - 10, 15, 30)))
|
|
|
|
Part.show(Part.makeCompound(parts), "Section")
|
|
|
|
return FreeCAD.ActiveDocument.getObject('Section')
|
|
|
|
def buildPath():
|
|
sketch = FreeCAD.ActiveDocument.addObject(
|
|
'Sketcher::SketchObject', 'Path')
|
|
sketch.Placement = FreeCAD.Placement(
|
|
FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(0, 0, 0, 1))
|
|
|
|
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(
|
|
0, 0, 0), FreeCAD.Vector(20000, 0, 0)), False)
|
|
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(
|
|
20000, 0, 0), FreeCAD.Vector(20000, 20000, 0)), False)
|
|
|
|
return sketch
|
|
|
|
def buildPost():
|
|
post = Part.makeBox(100, 100, 1000, FreeCAD.Vector(0, 0, 0))
|
|
|
|
Part.show(post, 'Post')
|
|
|
|
return FreeCAD.ActiveDocument.getObject('Post')
|
|
|
|
def colorizeFaces(o, color=(0.6, 0.0, 0.0, 0.0), faceIndizes=[2]):
|
|
numberOfFaces = len(o.Shape.Faces)
|
|
vo = o.ViewObject
|
|
|
|
originalColors = vo.DiffuseColor
|
|
|
|
if len(originalColors) == 1:
|
|
newColors = originalColors * numberOfFaces
|
|
else:
|
|
newColors = originalColors.copy()
|
|
|
|
for i in faceIndizes:
|
|
newColors[i] = color
|
|
|
|
vo.DiffuseColor = newColors
|
|
|
|
section = buildSection()
|
|
path = buildPath()
|
|
post = buildPost()
|
|
|
|
colorizeFaces(post)
|
|
|
|
print(makeFence(section, post, path))
|
|
|
|
# _CommandFence().Activated()
|