Files
create/src/Mod/Arch/ArchFence.py
furti f58c20afec Fix some problems with ArchFence
The commit that introduces the colorization option added some problems.
First the "sectionFaceNumbers" property is only set when an execute is
performed. When this does not happen, an exception is thrown when the
document is saved that contains a fence created with an older version
of FreeCAD.

Also the new Props on the ViewProvider where not set because the
"onDocumentRestored" is not reckognized for ViewProviders.
2019-06-27 11:48:50 -03:00

479 lines
15 KiB
Python

import math
import FreeCAD
import ArchComponent
import Draft
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 = Draft.calculatePlacementsOnPath(
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 equals 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)
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 / 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()