1
#***************************************************************************
2
#* Copyright (c) 2011 Yorik van Havre <yorik@uncreated.net> *
4
#* This program is free software; you can redistribute it and/or modify *
5
#* it under the terms of the GNU Lesser General Public License (LGPL) *
6
#* as published by the Free Software Foundation; either version 2 of *
7
#* the License, or (at your option) any later version. *
8
#* for detail see the LICENCE text file. *
10
#* This program is distributed in the hope that it will be useful, *
11
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
12
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13
#* GNU Library General Public License for more details. *
15
#* You should have received a copy of the GNU Library General Public *
16
#* License along with this program; if not, write to the Free Software *
17
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
20
#***************************************************************************
22
"""This module provides tools to build Wall objects. Walls are simple
23
objects, usually vertical, typically obtained by giving a thickness to a base
24
line, then extruding it vertically.
28
TODO put examples here.
32
import FreeCAD,Draft,ArchComponent,DraftVecUtils,ArchCommands,math
33
from FreeCAD import Vector
34
from draftutils import params
35
import ArchSketchObject
39
from PySide import QtCore, QtGui
40
from draftutils.translate import translate
41
from PySide.QtCore import QT_TRANSLATE_NOOP
42
import draftguitools.gui_trackers as DraftTrackers
45
def translate(ctxt,txt):
47
def QT_TRANSLATE_NOOP(ctxt,txt):
53
# \brief The Wall object and tools
55
# This module provides tools to build Wall objects. Walls are simple objects,
56
# usually vertical, typically obtained by giving a thickness to a base line,
57
# then extruding it vertically.
59
__title__ = "FreeCAD Wall"
60
__author__ = "Yorik van Havre"
61
__url__ = "https://www.freecad.org"
65
def mergeShapes(w1,w2):
66
"""Not currently implemented.
68
Return a Shape built on two walls that share same properties and have a
72
if not areSameWallTypes([w1,w2]):
74
if (not hasattr(w1.Base,"Shape")) or (not hasattr(w2.Base,"Shape")):
76
if w1.Base.Shape.Faces or w2.Base.Shape.Faces:
82
eds = w1.Base.Shape.Edges + w2.Base.Shape.Edges
84
w = DraftGeomUtils.findWires(eds)
86
#print("found common wire")
87
normal,length,width,height = w1.Proxy.getDefaultValues(w1)
89
sh = w1.Proxy.getBase(w1,w[0],normal,width,height)
94
def areSameWallTypes(walls):
95
"""Check if a list of walls have the same height, width and alignment.
99
walls: list of <ArchComponent.Component>
104
True if the walls have the same height, width and alignment, False if
108
for att in ["Width","Height","Align"]:
111
if not hasattr(w,att):
114
value = getattr(w,att)
116
if type(value) == float:
117
if round(value,Draft.precision()) != round(getattr(w,att),Draft.precision()):
120
if value != getattr(w,att):
125
class _Wall(ArchComponent.Component):
128
Turns a <App::FeaturePython> into a wall object, then uses a
129
<Part::Feature> to create the wall's shape.
131
Walls are simple objects, usually vertical, typically obtained by giving a
132
thickness to a base line, then extruding it vertically.
136
obj: <App::FeaturePython>
137
The object to turn into a wall. Note that this is not the object that
138
forms the basis for the new wall's shape. That is given later.
141
def __init__(self, obj):
142
ArchComponent.Component.__init__(self, obj)
143
self.setProperties(obj)
146
def setProperties(self, obj):
147
"""Give the wall its wall specific properties, such as its alignment.
149
You can learn more about properties here:
150
https://wiki.freecad.org/property
154
obj: <part::featurepython>
155
The object to turn into a wall.
158
lp = obj.PropertiesList
159
if not "Length" in lp:
160
obj.addProperty("App::PropertyLength","Length","Wall",QT_TRANSLATE_NOOP("App::Property","The length of this wall. Not used if this wall is based on an underlying object"))
161
if not "Width" in lp:
162
obj.addProperty("App::PropertyLength","Width","Wall",QT_TRANSLATE_NOOP("App::Property","The width of this wall. Not used if this wall is based on a face. Disabled and ignored if Base object (ArchSketch) provides the information."))
164
# To be combined into Width when PropertyLengthList is available
165
if not "OverrideWidth" in lp:
166
obj.addProperty("App::PropertyFloatList","OverrideWidth","Wall",QT_TRANSLATE_NOOP("App::Property","This overrides Width attribute to set width of each segment of wall. Disabled and ignored if Base object (ArchSketch) provides Widths information, with getWidths() method (If a value is zero, the value of 'Width' will be followed). [ENHANCEMENT by ArchSketch] GUI 'Edit Wall Segment Width' Tool is provided in external SketchArch Add-on to let users to set the values interactively. 'Toponaming-Tolerant' if ArchSketch is used in Base (and SketchArch Add-on is installed). Warning : Not 'Toponaming-Tolerant' if just Sketch is used.")) # see DraftGeomUtils.offsetwire()
167
if not "OverrideAlign" in lp:
168
obj.addProperty("App::PropertyStringList","OverrideAlign","Wall",QT_TRANSLATE_NOOP("App::Property","This overrides Align attribute to set align of each segment of wall. Disabled and ignored if Base object (ArchSketch) provides Aligns information, with getAligns() method (If a value is not 'Left, Right, Center', the value of 'Align' will be followed). [ENHANCEMENT by ArchSketch] GUI 'Edit Wall Segment Align' Tool is provided in external SketchArch Add-on to let users to set the values interactively. 'Toponaming-Tolerant' if ArchSketch is used in Base (and SketchArch Add-on is installed). Warning : Not 'Toponaming-Tolerant' if just Sketch is used.")) # see DraftGeomUtils.offsetwire()
169
if not "OverrideOffset" in lp:
170
obj.addProperty("App::PropertyFloatList","OverrideOffset","Wall",QT_TRANSLATE_NOOP("App::Property","This overrides Offset attribute to set offset of each segment of wall. Disabled and ignored if Base object (ArchSketch) provides Offsets information, with getOffsets() method (If a value is zero, the value of 'Offset' will be followed). [ENHANCED by ArchSketch] GUI 'Edit Wall Segment Offset' Tool is provided in external Add-on ('SketchArch') to let users to select the edges interactively. 'Toponaming-Tolerant' if ArchSketch is used in Base (and SketchArch Add-on is installed). Warning : Not 'Toponaming-Tolerant' if just Sketch is used. Property is ignored if Base ArchSketch provided the selected edges. ")) # see DraftGeomUtils.offsetwire()
171
if not "Height" in lp:
172
obj.addProperty("App::PropertyLength","Height","Wall",QT_TRANSLATE_NOOP("App::Property","The height of this wall. Keep 0 for automatic. Not used if this wall is based on a solid"))
174
obj.addProperty("App::PropertyArea","Area","Wall",QT_TRANSLATE_NOOP("App::Property","The area of this wall as a simple Height * Length calculation"))
175
obj.setEditorMode("Area",1)
176
if not "Align" in lp:
177
obj.addProperty("App::PropertyEnumeration","Align","Wall",QT_TRANSLATE_NOOP("App::Property","The alignment of this wall on its base object, if applicable. Disabled and ignored if Base object (ArchSketch) provides the information."))
178
obj.Align = ['Left','Right','Center']
179
if not "Normal" in lp:
180
obj.addProperty("App::PropertyVector","Normal","Wall",QT_TRANSLATE_NOOP("App::Property","The normal extrusion direction of this object (keep (0,0,0) for automatic normal)"))
182
obj.addProperty("App::PropertyInteger","Face","Wall",QT_TRANSLATE_NOOP("App::Property","The face number of the base object used to build this wall"))
183
if not "Offset" in lp:
184
obj.addProperty("App::PropertyDistance","Offset","Wall",QT_TRANSLATE_NOOP("App::Property","The offset between this wall and its baseline (only for left and right alignments). Disabled and ignored if Base object (ArchSketch) provides the information."))
186
# See getExtrusionData(), removeSplitters are no longer used
187
#if not "Refine" in lp:
188
# obj.addProperty("App::PropertyEnumeration","Refine","Wall",QT_TRANSLATE_NOOP("App::Property","Select whether or not and the method to remove splitter of the Wall. Currently Draft removeSplitter and Part removeSplitter available but may not work on complex sketch."))
189
# obj.Refine = ['No','DraftRemoveSplitter','PartRemoveSplitter']
190
# TODO - To implement in Arch Component ?
192
if not "MakeBlocks" in lp:
193
obj.addProperty("App::PropertyBool","MakeBlocks","Blocks",QT_TRANSLATE_NOOP("App::Property","Enable this to make the wall generate blocks"))
194
if not "BlockLength" in lp:
195
obj.addProperty("App::PropertyLength","BlockLength","Blocks",QT_TRANSLATE_NOOP("App::Property","The length of each block"))
196
if not "BlockHeight" in lp:
197
obj.addProperty("App::PropertyLength","BlockHeight","Blocks",QT_TRANSLATE_NOOP("App::Property","The height of each block"))
198
if not "OffsetFirst" in lp:
199
obj.addProperty("App::PropertyLength","OffsetFirst","Blocks",QT_TRANSLATE_NOOP("App::Property","The horizontal offset of the first line of blocks"))
200
if not "OffsetSecond" in lp:
201
obj.addProperty("App::PropertyLength","OffsetSecond","Blocks",QT_TRANSLATE_NOOP("App::Property","The horizontal offset of the second line of blocks"))
202
if not "Joint" in lp:
203
obj.addProperty("App::PropertyLength","Joint","Blocks",QT_TRANSLATE_NOOP("App::Property","The size of the joints between each block"))
204
if not "CountEntire" in lp:
205
obj.addProperty("App::PropertyInteger","CountEntire","Blocks",QT_TRANSLATE_NOOP("App::Property","The number of entire blocks"))
206
obj.setEditorMode("CountEntire",1)
207
if not "CountBroken" in lp:
208
obj.addProperty("App::PropertyInteger","CountBroken","Blocks",QT_TRANSLATE_NOOP("App::Property","The number of broken blocks"))
209
obj.setEditorMode("CountBroken",1)
210
if not "ArchSketchData" in lp:
211
obj.addProperty("App::PropertyBool","ArchSketchData","Wall",QT_TRANSLATE_NOOP("App::Property","Use Base ArchSketch (if used) data (e.g. widths, aligns, offsets) instead of Wall's properties"))
212
obj.ArchSketchData = True
216
def onDocumentRestored(self,obj):
217
"""Method run when the document is restored. Re-adds the Arch component, and Arch wall properties."""
219
ArchComponent.Component.onDocumentRestored(self,obj)
220
self.setProperties(obj)
222
if hasattr(obj,"ArchSketchData") and obj.ArchSketchData and Draft.getType(obj.Base) == "ArchSketch":
223
if hasattr(obj,"Width"):
224
obj.setEditorMode("Width", ["ReadOnly"])
225
if hasattr(obj,"Align"):
226
obj.setEditorMode("Align", ["ReadOnly"])
227
if hasattr(obj,"Offset"):
228
obj.setEditorMode("Offset", ["ReadOnly"])
229
if hasattr(obj,"OverrideWidth"):
230
obj.setEditorMode("OverrideWidth", ["ReadOnly"])
231
if hasattr(obj,"OverrideAlign"):
232
obj.setEditorMode("OverrideAlign", ["ReadOnly"])
233
if hasattr(obj,"OverrideOffset"):
234
obj.setEditorMode("OverrideOffset", ["ReadOnly"])
236
if hasattr(obj,"Width"):
237
obj.setEditorMode("Width", 0)
238
if hasattr(obj,"Align"):
239
obj.setEditorMode("Align", 0)
240
if hasattr(obj,"Offset"):
241
obj.setEditorMode("Offset", 0)
242
if hasattr(obj,"OverrideWidth"):
243
obj.setEditorMode("OverrideWidth", 0)
244
if hasattr(obj,"OverrideAlign"):
245
obj.setEditorMode("OverrideAlign", 0)
246
if hasattr(obj,"OverrideOffset"):
247
obj.setEditorMode("OverrideOffset", 0)
249
def execute(self,obj):
250
"""Method run when the object is recomputed.
252
Extrude the wall from the Base shape if possible. Processe additions
253
and subtractions. Assign the resulting shape as the shape of the wall.
255
Add blocks if the MakeBlocks property is assigned. If the Base shape is
256
a mesh, just copy the mesh.
263
import DraftGeomUtils
266
extdata = self.getExtrusionData(obj)
269
extv = extdata[2].Rotation.multVec(extdata[1])
270
if isinstance(bplates,list):
272
# Test : if base is Sketch, then fuse all solid; otherwise, makeCompound
273
sketchBaseToFuse = obj.Base.getLinkedObject().isDerivedFrom("Sketcher::SketchObject")
274
# but turn this off if we have layers, otherwise layers get merged
275
if hasattr(obj,"Material") and obj.Material \
276
and hasattr(obj.Material,"Materials") and obj.Material.Materials:
277
sketchBaseToFuse = False
279
b.Placement = extdata[2].multiply(b.Placement)
282
# See getExtrusionData() - not fusing baseplates there - fuse solids here
283
# Remarks - If solids are fused, but exportIFC.py use underlying baseplates w/o fuse, the result in ifc look slightly different from in FC.
287
shps = shps.fuse(b) #shps.fuse(b)
292
# TODO - To let user to select whether to fuse (slower) or to do a compound (faster) only ?
297
base = Part.makeCompound(shps)
299
bplates.Placement = extdata[2].multiply(bplates.Placement)
300
base = bplates.extrude(extv)
302
if hasattr(obj.Base,'Shape'):
303
if obj.Base.Shape.isNull():
305
if not obj.Base.Shape.isValid():
306
if not obj.Base.Shape.Solids:
307
# let pass invalid objects if they have solids...
309
elif obj.Base.Shape.Solids:
310
base = Part.Shape(obj.Base.Shape)
312
elif hasattr(obj,"MakeBlocks") and hasattr(self,"basewires"):
313
if obj.MakeBlocks and self.basewires and extdata and obj.Width and obj.Height:
314
#print "calculating blocks"
315
if len(self.basewires) == 1:
317
n = FreeCAD.Vector(extv)
321
if obj.BlockLength.Value:
324
offset = obj.OffsetFirst.Value
326
offset = obj.OffsetSecond.Value
328
# only 1 wire (first) is supported
329
# TODO - Can support multiple wires?
331
# self.basewires was list of list of edges,
332
# no matter Base is DWire, Sketch or else
333
# See discussion - https://forum.freecad.org/viewtopic.php?t=86365
334
baseEdges = self.basewires[0]
336
for edge in baseEdges:
337
while offset < (edge.Length-obj.Joint.Value):
338
#print i," Edge ",edge," : ",edge.Length," - ",offset
340
t = edge.tangentAt(offset)
342
p.multiply(1.1*obj.Width.Value+obj.Offset.Value)
343
p1 = edge.valueAt(offset).add(p)
344
p2 = edge.valueAt(offset).add(p.negative())
345
sh = Part.LineSegment(p1,p2).toShape()
347
sh = sh.extrude(-t.multiply(obj.Joint.Value))
353
offset += (obj.BlockLength.Value + obj.Joint.Value)
354
offset -= edge.Length
356
if isinstance(bplates,list):
358
if obj.BlockHeight.Value:
359
fsize = obj.BlockHeight.Value + obj.Joint.Value
360
bh = obj.BlockHeight.Value
362
fsize = obj.Height.Value
363
bh = obj.Height.Value
364
bvec = FreeCAD.Vector(n)
366
svec = FreeCAD.Vector(n)
369
plate1 = bplates.cut(cuts1).Faces
371
plate1 = bplates.Faces
372
blocks1 = Part.makeCompound([f.extrude(bvec) for f in plate1])
374
plate2 = bplates.cut(cuts2).Faces
376
plate2 = bplates.Faces
377
blocks2 = Part.makeCompound([f.extrude(bvec) for f in plate2])
378
interval = extv.Length/(fsize)
379
entire = int(interval)
380
rest = (interval - entire)
381
for i in range(entire):
383
b = Part.Shape(blocks2)
385
b = Part.Shape(blocks1)
387
t = FreeCAD.Vector(svec)
392
rest = extv.Length - (entire * fsize)
393
rvec = FreeCAD.Vector(n)
396
b = Part.makeCompound([f.extrude(rvec) for f in plate2])
398
b = Part.makeCompound([f.extrude(rvec) for f in plate1])
399
t = FreeCAD.Vector(svec)
404
base = Part.makeCompound(blocks)
407
FreeCAD.Console.PrintWarning(translate("Arch","Cannot compute blocks for wall")+obj.Label+"\n")
409
elif obj.Base.isDerivedFrom("Mesh::Feature"):
410
if obj.Base.Mesh.isSolid():
411
if obj.Base.Mesh.countComponents() == 1:
412
sh = ArchCommands.getShapeFromMesh(obj.Base.Mesh)
413
if sh.isClosed() and sh.isValid() and sh.Solids and (not sh.isNull()):
416
FreeCAD.Console.PrintWarning(translate("Arch","This mesh is an invalid solid")+"\n")
417
obj.Base.ViewObject.show()
419
#FreeCAD.Console.PrintError(translate("Arch","Error: Invalid base object")+"\n")
421
# walls can be made of only a series of additions and have no base shape
424
base = self.processSubShapes(obj,base,pl)
426
self.applyShape(obj,base,pl)
429
if hasattr(obj,"MakeBlocks"):
431
fvol = obj.BlockLength.Value * obj.BlockHeight.Value * obj.Width.Value
433
#print("base volume:",fvol)
434
#for s in base.Solids:
435
#print(abs(s.Volume - fvol))
436
ents = [s for s in base.Solids if abs(s.Volume - fvol) < 1]
437
obj.CountEntire = len(ents)
438
obj.CountBroken = len(base.Solids) - len(ents)
443
# set the length property
445
if hasattr(obj.Base,'Shape'):
446
if obj.Base.Shape.Edges:
447
if not obj.Base.Shape.Faces:
448
if hasattr(obj.Base.Shape,"Length"):
449
l = obj.Base.Shape.Length
450
if obj.Length.Value != l:
452
self.oldLength = None # delete the stored value to prevent triggering base change below
454
# set the Area property
455
obj.Area = obj.Length.Value * obj.Height.Value
457
def onBeforeChange(self,obj,prop):
458
"""Method called before the object has a property changed.
460
Specifically, this method is called before the value changes.
462
If "Length" has changed, record the old length so that .onChanged() can
463
be sure that the base needs to be changed.
465
Also call ArchComponent.Component.onBeforeChange().
470
The name of the property that has changed.
474
self.oldLength = obj.Length.Value
475
ArchComponent.Component.onBeforeChange(self,obj,prop)
477
def onChanged(self, obj, prop):
478
"""Method called when the object has a property changed.
480
If length has changed, extend the length of the Base object, if the
481
Base object only has a single edge to extend.
483
Also hide subobjects.
485
Also call ArchComponent.Component.onChanged().
490
The name of the property that has changed.
494
if (obj.Base and obj.Length.Value
495
and hasattr(self,"oldLength") and (self.oldLength is not None)
496
and (self.oldLength != obj.Length.Value)):
498
if hasattr(obj.Base,'Shape'):
499
if len(obj.Base.Shape.Edges) == 1:
500
import DraftGeomUtils
501
e = obj.Base.Shape.Edges[0]
502
if DraftGeomUtils.geomType(e) == "Line":
503
if e.Length != obj.Length.Value:
504
v = e.Vertexes[-1].Point.sub(e.Vertexes[0].Point)
506
v.multiply(obj.Length.Value)
507
p2 = e.Vertexes[0].Point.add(v)
508
if Draft.getType(obj.Base) == "Wire":
509
#print "modifying p2"
511
elif Draft.getType(obj.Base) in ["Sketcher::SketchObject", "ArchSketch"]:
513
obj.Base.recompute() # Fix for the 'GeoId index out range' error.
514
obj.Base.movePoint(0, 2, obj.Base.Placement.inverse().multVec(p2))
515
except Exception: # This 'GeoId index out range' error should no longer occur.
516
print("Debug: The base sketch of this wall could not be changed, because the sketch has not been edited yet in this session (this is a bug in FreeCAD). Try entering and exiting edit mode in this sketch first, and then changing the wall length should work.")
518
FreeCAD.Console.PrintError(translate("Arch","Error: Unable to modify the base object of this wall")+"\n")
520
if hasattr(obj,"ArchSketchData") and obj.ArchSketchData and Draft.getType(obj.Base) == "ArchSketch":
521
if hasattr(obj,"Width"):
522
obj.setEditorMode("Width", ["ReadOnly"])
523
if hasattr(obj,"Align"):
524
obj.setEditorMode("Align", ["ReadOnly"])
525
if hasattr(obj,"Offset"):
526
obj.setEditorMode("Offset", ["ReadOnly"])
527
if hasattr(obj,"OverrideWidth"):
528
obj.setEditorMode("OverrideWidth", ["ReadOnly"])
529
if hasattr(obj,"OverrideAlign"):
530
obj.setEditorMode("OverrideAlign", ["ReadOnly"])
531
if hasattr(obj,"OverrideOffset"):
532
obj.setEditorMode("OverrideOffset", ["ReadOnly"])
534
if hasattr(obj,"Width"):
535
obj.setEditorMode("Width", 0)
536
if hasattr(obj,"Align"):
537
obj.setEditorMode("Align", 0)
538
if hasattr(obj,"Offset"):
539
obj.setEditorMode("Offset", 0)
540
if hasattr(obj,"OverrideWidth"):
541
obj.setEditorMode("OverrideWidth", 0)
542
if hasattr(obj,"OverrideAlign"):
543
obj.setEditorMode("OverrideAlign", 0)
544
if hasattr(obj,"OverrideOffset"):
545
obj.setEditorMode("OverrideOffset", 0)
547
self.hideSubobjects(obj,prop)
548
ArchComponent.Component.onChanged(self,obj,prop)
550
def getFootprint(self,obj):
551
"""Get the faces that make up the base/foot of the wall.
556
The faces that make up the foot of the wall.
561
for f in obj.Shape.Faces:
562
if f.normalAt(0,0).getAngle(FreeCAD.Vector(0,0,-1)) < 0.01:
563
if abs(abs(f.CenterOfMass.z) - abs(obj.Shape.BoundBox.ZMin)) < 0.001:
567
def getExtrusionData(self,obj):
568
"""Get data needed to extrude the wall from a base object.
570
take the Base object, and find a base face to extrude
571
out, a vector to define the extrusion direction and distance.
573
Rebase the base face to the (0,0,0) origin.
575
Return the base face, rebased, with the extrusion vector, and the
576
<Base.Placement> needed to return the face back to its original
581
tuple of (<Part.Face>, <Base.Vector>, <Base.Placement>)
582
Tuple containing the base face, the vector for extrusion, and the
583
placement needed to move the face back from the (0,0,0) origin.
587
import DraftGeomUtils
589
# If ArchComponent.Component.getExtrusionData() can successfully get
590
# extrusion data, just use that.
591
data = ArchComponent.Component.getExtrusionData(self,obj)
593
if not isinstance(data[0],list):
594
# multifuses not considered here
596
length = obj.Length.Value
598
# TODO currently layers were not supported when len(basewires) > 0 ##( or 1 ? )
601
# Get width of each edge segment from Base Objects if they store it
602
# (Adding support in SketchFeaturePython, DWire...)
603
widths = [] # [] or None are both False
604
if hasattr(obj,"ArchSketchData") and obj.ArchSketchData and Draft.getType(obj.Base) == "ArchSketch":
605
if hasattr(obj.Base, 'Proxy'):
606
if hasattr(obj.Base.Proxy, 'getWidths'):
607
# Return a list of Width corresponding to indexes of sorted
609
widths = obj.Base.Proxy.getWidths(obj.Base)
611
# Get width of each edge/wall segment from ArchWall.OverrideWidth if
612
# Base Object does not provide it
614
if obj.OverrideWidth:
615
if obj.Base and obj.Base.isDerivedFrom("Sketcher::SketchObject"):
616
# If Base Object is ordinary Sketch (or when ArchSketch.getWidth() not implemented yet):-
617
# sort the width list in OverrrideWidth to correspond to indexes of sorted edges of Sketch
619
import ArchSketchObject
621
print("ArchSketchObject add-on module is not installed yet")
623
widths = ArchSketchObject.sortSketchWidth(obj.Base, obj.OverrideWidth)
625
widths = obj.OverrideWidth
627
# If Base Object is not Sketch, but e.g. DWire, the width
628
# list in OverrrideWidth just correspond to sequential
630
widths = obj.OverrideWidth
632
widths = [obj.Width.Value]
634
# having no width is valid for walls so the user doesn't need to be warned
635
# it just disables extrusions and return none
636
#print ("Width & OverrideWidth & base.getWidths() should not be all 0 or None or [] empty list ")
639
# Set 'default' width - for filling in any item in the list == 0 or None
641
width = obj.Width.Value
643
width = 200 # 'Default' width value
645
# Get align of each edge segment from Base Objects if they store it.
646
# (Adding support in SketchFeaturePython, DWire...)
648
if hasattr(obj,"ArchSketchData") and obj.ArchSketchData and Draft.getType(obj.Base) == "ArchSketch":
649
if hasattr(obj.Base, 'Proxy'):
650
if hasattr(obj.Base.Proxy, 'getAligns'):
651
# Return a list of Align corresponds to indexes of sorted
653
aligns = obj.Base.Proxy.getAligns(obj.Base)
654
# Get align of each edge/wall segment from ArchWall.OverrideAlign if
655
# Base Object does not provide it
657
if obj.OverrideAlign:
658
if obj.Base and obj.Base.isDerivedFrom("Sketcher::SketchObject"):
659
# If Base Object is ordinary Sketch (or when
660
# ArchSketch.getAligns() not implemented yet):- sort the
661
# align list in OverrideAlign to correspond to indexes of
662
# sorted edges of Sketch
664
import ArchSketchObject
666
print("ArchSketchObject add-on module is not installed yet")
668
aligns = ArchSketchObject.sortSketchAlign(obj.Base, obj.OverrideAlign)
670
aligns = obj.OverrideAlign
672
# If Base Object is not Sketch, but e.g. DWire, the align
673
# list in OverrideAlign just correspond to sequential order
675
aligns = obj.OverrideAlign
679
# set 'default' align - for filling in any item in the list == 0 or None
680
align = obj.Align # or aligns[0]
682
# Get offset of each edge segment from Base Objects if they store it
683
# (Adding support in SketchFeaturePython, DWire...)
684
offsets = [] # [] or None are both False
685
if hasattr(obj,"ArchSketchData") and obj.ArchSketchData and Draft.getType(obj.Base) == "ArchSketch":
687
if hasattr(obj.Base, 'Proxy'):
688
if hasattr(obj.Base.Proxy, 'getOffsets'):
689
# Return a list of Offset corresponding to indexes of sorted
691
offsets = obj.Base.Proxy.getOffsets(obj.Base)
692
# Get offset of each edge/wall segment from ArchWall.OverrideOffset if
693
# Base Object does not provide it
695
if obj.OverrideOffset:
696
if obj.Base and obj.Base.isDerivedFrom("Sketcher::SketchObject"):
697
# If Base Object is ordinary Sketch (or when ArchSketch.getOffsets() not implemented yet):-
698
# sort the offset list in OverrideOffset to correspond to indexes of sorted edges of Sketch
699
if hasattr(ArchSketchObject, 'sortSketchOffset'):
700
offsets = ArchSketchObject.sortSketchOffset(obj.Base, obj.OverrideOffset)
702
offsets = obj.OverrideOffset
704
# If Base Object is not Sketch, but e.g. DWire, the width
705
# list in OverrrideWidth just correspond to sequential
707
offsets = obj.OverrideOffset
709
offsets = [obj.Offset.Value]
711
# Set 'default' offset - for filling in any item in the list == 0 or None
712
offset = obj.Offset.Value # could be 0
714
height = obj.Height.Value
716
height = self.getParentHeight(obj)
719
if obj.Normal == Vector(0,0,0):
721
normal = DraftGeomUtils.get_shape_normal(obj.Base.Shape)
723
normal = Vector(0,0,1)
725
normal = Vector(0,0,1)
727
normal = Vector(obj.Normal)
730
self.basewires = None
734
if hasattr(obj,"Material"):
736
if hasattr(obj.Material,"Materials"):
737
thicknesses = [abs(t) for t in obj.Material.Thicknesses]
740
restwidth = width - sum(thicknesses)
742
varwidth = [t for t in thicknesses if t == 0]
744
varwidth = restwidth/len(varwidth)
745
for t in obj.Material.Thicknesses:
749
layers.append(varwidth)
752
if hasattr(obj.Base,'Shape'):
754
if obj.Base.Shape.Solids:
757
# If the user has defined a specific face of the Base
758
# object to build the wall from, extrude from that face,
759
# and return the extrusion moved to (0,0,0), normal of the
760
# face, and placement to move the extrusion back to its
763
if len(obj.Base.Shape.Faces) >= obj.Face:
764
face = obj.Base.Shape.Faces[obj.Face-1]
765
if obj.Normal != Vector(0,0,0):
766
normal = face.normalAt(0,0)
767
if normal.getAngle(Vector(0,0,1)) > math.pi/4:
768
normal.multiply(width)
769
base = face.extrude(normal)
770
if obj.Align == "Center":
771
base.translate(normal.negative().multiply(0.5))
772
elif obj.Align == "Right":
773
base.translate(normal.negative())
775
normal.multiply(height)
776
base = face.extrude(normal)
777
base, placement = self.rebase(base)
778
return (base,normal,placement)
780
# If the Base has faces, but no specific one has been
781
# selected, rebase the faces and continue.
782
elif obj.Base.Shape.Faces:
783
if not DraftGeomUtils.isCoplanar(obj.Base.Shape.Faces):
786
base,placement = self.rebase(obj.Base.Shape)
788
elif hasattr(obj.Base, 'Proxy') and obj.ArchSketchData and \
789
hasattr(obj.Base.Proxy, 'getWallBaseShapeEdgesInfo'):
791
wallBaseShapeEdgesInfo = obj.Base.Proxy.getWallBaseShapeEdgesInfo(obj.Base)
792
#get wall edges (not wires); use original edges if getWallBaseShapeEdgesInfo() provided none
793
if wallBaseShapeEdgesInfo:
794
self.basewires = wallBaseShapeEdgesInfo.get('wallAxis') # 'wallEdges' # widths, aligns, offsets?
796
# Sort Sketch edges consistently with below procedures
797
# without using Sketch.Shape.Edges - found the latter order
798
# in some corner case != getSortedClusters()
799
elif obj.Base.isDerivedFrom("Sketcher::SketchObject"):
801
skGeom = obj.Base.GeometryFacadeList
803
skPlacement = obj.Base.Placement # Get Sketch's placement to restore later
805
if not i.Construction:
806
# support Line, Arc, Circle, Ellipse for Sketch as Base at the moment
807
if isinstance(i.Geometry, (Part.LineSegment, Part.Circle, Part.ArcOfCircle, Part.Ellipse)):
808
skGeomEdgesI = i.Geometry.toShape()
809
skGeomEdges.append(skGeomEdgesI)
810
for cluster in Part.getSortedClusters(skGeomEdges):
811
clusterTransformed = []
813
# TODO 2023.11.26: Multiplication order should be switched?
814
# So far 'no problem' as 'edge.placement' is always '0,0,0' ?
815
edge.Placement = edge.Placement.multiply(skPlacement) ## TODO add attribute to skip Transform...
817
clusterTransformed.append(edge)
818
# Only use cluster of edges rather than turning into wire
819
self.basewires.append(clusterTransformed)
821
# Use Sketch's Normal for all edges/wires generated
822
# from sketch for consistency. Discussion on checking
823
# normal of sketch.Placement vs
824
# sketch.getGlobalPlacement() -
825
# https://forum.freecad.org/viewtopic.php?f=22&t=39341&p=334275#p334275
826
# normal = obj.Base.Placement.Rotation.multVec(FreeCAD.Vector(0,0,1))
827
normal = obj.Base.getGlobalPlacement().Rotation.multVec(FreeCAD.Vector(0,0,1))
829
else: #For all objects except Sketch, single edge or more
830
# See discussion - https://forum.freecad.org/viewtopic.php?t=86365
831
# See discussion - https://forum.freecad.org/viewtopic.php?t=82207&start=10
832
#self.basewires = obj.Base.Shape.Wires
834
# Now, adopt approach same as for Sketch
836
clusters = Part.getSortedClusters(obj.Base.Shape.Edges)
837
self.basewires = clusters
839
# Found case that after sorting below, direction of
840
# edges sorted are not as 'expected' thus resulted in
841
# bug - e.g. a Dwire with edges/vertexes in clockwise
842
# order, 1st vertex is Forward as expected. After
843
# sorting below, edges sorted still in clockwise order
844
# - no problem, but 1st vertex of each edge become
845
# Reverse rather than Forward.
847
# See FC discussion -
848
# https://forum.freecad.org/viewtopic.php?f=23&t=48275&p=413745#p413745
851
#for cluster in Part.getSortedClusters(obj.Base.Shape.Edges):
852
# for c in Part.sortEdges(cluster):
853
# self.basewires.append(Part.Wire(c))
854
# if not sketch, e.g. Dwire, can have wire which is 3d
855
# so not on the placement's working plane - below
856
# applied to Sketch not applicable here
857
#normal = obj.Base.getGlobalPlacement().Rotation.multVec(FreeCAD.Vector(0,0,1))
858
#normal = obj.Base.Placement.Rotation.multVec(FreeCAD.Vector(0,0,1))
861
if (len(self.basewires) == 1) and layers:
862
self.basewires = [self.basewires[0] for l in layers]
866
for i,wire in enumerate(self.basewires):
868
# Check number of edges per 'wire' and get the 1st edge
869
if isinstance(wire,Part.Wire):
870
edgeNum = len(wire.Edges)
872
elif isinstance(wire[0],Part.Edge):
876
for n in range(0,edgeNum,1): # why these not work - range(edgeNum), range(0,edgeNum) ...
878
# Fill the aligns list with ArchWall's default
879
# align entry and with same number of items as
882
if aligns[n] not in ['Left', 'Right', 'Center']:
887
# Fill the widths List with ArchWall's default
888
# width entry and with same number of items as
895
# Fill the offsets List with ArchWall's default
896
# offset entry and with same number of items as
902
offsets.append(offset)
904
# Get a direction vector orthogonal to both the
905
# normal of the face/sketch and the direction the
906
# wire was drawn in. IE: along the width direction
908
if isinstance(e.Curve,(Part.Circle,Part.Ellipse)):
909
dvec = e.Vertexes[0].Point.sub(e.Curve.Center)
911
dvec = DraftGeomUtils.vec(e).cross(normal)
913
if not DraftVecUtils.isNull(dvec):
917
curAligns = aligns[0]
918
#off = obj.Offset.Value # off is no longer used
920
if curAligns == "Left":
924
for n in range(edgeNum):
925
curWidth.append(abs(layers[i]))
926
#off = off+layeroffset # off is no longer used
927
offsets = [x+layeroffset for x in offsets]
928
dvec.multiply(curWidth[0])
929
layeroffset += abs(curWidth[0])
934
# Now DraftGeomUtils.offsetWire() support
935
# similar effect as ArchWall Offset
938
# dvec2 = DraftVecUtils.scaleTo(dvec,off)
939
# wire = DraftGeomUtils.offsetWire(wire,dvec2)
941
# Get the 'offseted' wire taking into account
942
# of Width and Align of each edge, and overall
944
w2 = DraftGeomUtils.offsetWire(wire, dvec,
951
basewireOffset=offsets)
952
# Get the 'base' wire taking into account of
953
# width and align of each edge
954
w1 = DraftGeomUtils.offsetWire(wire, dvec,
958
offsetMode="BasewireMode",
961
basewireOffset=offsets)
962
face = DraftGeomUtils.bind(w1, w2, per_segment=True)
964
elif curAligns == "Right":
965
dvec = dvec.negative()
969
for n in range(edgeNum):
970
curWidth.append(abs(layers[i]))
971
#off = off+layeroffset # off is no longer used
972
offsets = [x+layeroffset for x in offsets]
973
dvec.multiply(curWidth[0])
974
layeroffset += abs(curWidth[0])
979
# Now DraftGeomUtils.offsetWire() support similar effect as ArchWall Offset
982
# dvec2 = DraftVecUtils.scaleTo(dvec,off)
983
# wire = DraftGeomUtils.offsetWire(wire,dvec2)
986
w2 = DraftGeomUtils.offsetWire(wire, dvec,
993
basewireOffset=offsets)
994
w1 = DraftGeomUtils.offsetWire(wire, dvec,
998
offsetMode="BasewireMode",
1001
basewireOffset=offsets)
1002
face = DraftGeomUtils.bind(w1, w2, per_segment=True)
1004
#elif obj.Align == "Center":
1005
elif curAligns == "Center":
1007
totalwidth=sum([abs(l) for l in layers])
1008
curWidth = abs(layers[i])
1009
off = totalwidth/2-layeroffset
1010
d1 = Vector(dvec).multiply(off)
1011
w1 = DraftGeomUtils.offsetWire(wire, d1)
1012
layeroffset += curWidth
1013
off = totalwidth/2-layeroffset
1014
d1 = Vector(dvec).multiply(off)
1015
w2 = DraftGeomUtils.offsetWire(wire, d1)
1017
dvec.multiply(width)
1019
w2 = DraftGeomUtils.offsetWire(wire, dvec,
1026
basewireOffset=offsets)
1027
w1 = DraftGeomUtils.offsetWire(wire, dvec,
1031
offsetMode="BasewireMode",
1034
basewireOffset=offsets)
1035
face = DraftGeomUtils.bind(w1, w2, per_segment=True)
1037
del widths[0:edgeNum]
1038
del aligns[0:edgeNum]
1039
del offsets[0:edgeNum]
1043
if layers and (layers[i] < 0):
1044
# layers with negative values are not drawn
1049
# To allow exportIFC.py to work properly on
1050
# sketch, which use only 1st face / wire,
1051
# do not fuse baseface here So for a sketch
1052
# with multiple wires, each returns
1053
# individual face (rather than fusing
1054
# together) for exportIFC.py to work
1056
# "ArchWall - Based on Sketch Issues" - https://forum.freecad.org/viewtopic.php?f=39&t=31235
1058
# "Bug #2408: [PartDesign] .fuse is splitting edges it should not"
1059
# - https://forum.freecad.org/viewtopic.php?f=10&t=20349&p=346237#p346237
1060
# - bugtracker - https://freecad.org/tracker/view.php?id=2408
1062
# Try Part.Shell before removeSplitter
1063
# - https://forum.freecad.org/viewtopic.php?f=10&t=20349&start=10
1064
# - 1st finding : if a rectangle + 1 line, can't removesSplitter properly...
1065
# - 2nd finding : if 2 faces do not touch, can't form a shell; then, subsequently for remaining faces even though touch each faces, can't form a shell
1067
baseface.append(face)
1068
# The above make Refine methods below (in else) useless, regardless removeSpitters yet to be improved for cases do not work well
1069
''' Whether layers or not, all baseface.append(face) '''
1074
''' Whether layers or not, all baseface = [face] '''
1077
base,placement = self.rebase(baseface)
1080
totalwidth = sum([abs(l) for l in layers])
1085
l2 = length/2 or 0.5
1086
w1 = -totalwidth/2 + offset
1088
v1 = Vector(-l2,w1,0)
1089
v2 = Vector(l2,w1,0)
1090
v3 = Vector(l2,w2,0)
1091
v4 = Vector(-l2,w2,0)
1092
base.append(Part.Face(Part.makePolygon([v1,v2,v3,v4,v1])))
1095
l2 = length/2 or 0.5
1097
v1 = Vector(-l2,-w2,0)
1098
v2 = Vector(l2,-w2,0)
1099
v3 = Vector(l2,w2,0)
1100
v4 = Vector(-l2,w2,0)
1101
base = Part.Face(Part.makePolygon([v1,v2,v3,v4,v1]))
1102
placement = FreeCAD.Placement()
1103
if base and placement:
1105
extrusion = normal.multiply(height)
1106
if placement.Rotation.Angle > 0:
1107
extrusion = placement.inverse().Rotation.multVec(extrusion)
1108
return (base,extrusion,placement)
1111
class _ViewProviderWall(ArchComponent.ViewProviderComponent):
1112
"""The view provider for the wall object.
1116
vobj: <Gui.ViewProviderDocumentObject>
1117
The view provider to turn into a wall view provider.
1120
def __init__(self,vobj):
1121
ArchComponent.ViewProviderComponent.__init__(self,vobj)
1122
vobj.ShapeColor = ArchCommands.getDefaultColor("Wall")
1125
"""Return the path to the appropriate icon.
1127
If a clone, return the cloned wall icon path. Otherwise return the
1133
Path to the appropriate icon .svg file.
1137
if hasattr(self,"Object"):
1138
if self.Object.CloneOf:
1139
return ":/icons/Arch_Wall_Clone.svg"
1140
elif (not self.Object.Base) and self.Object.Additions:
1141
return ":/icons/Arch_Wall_Tree_Assembly.svg"
1142
return ":/icons/Arch_Wall_Tree.svg"
1144
def attach(self,vobj):
1145
"""Add display modes' data to the coin scenegraph.
1147
Add each display mode as a coin node, whose parent is this view
1150
Each display mode's node includes the data needed to display the object
1151
in that mode. This might include colors of faces, or the draw style of
1152
lines. This data is stored as additional coin nodes which are children
1153
of the display mode node.
1155
Add the textures used in the Footprint display mode.
1158
self.Object = vobj.Object
1159
from pivy import coin
1160
tex = coin.SoTexture2()
1161
image = Draft.loadTexture(Draft.svgpatterns()['simple'][1], 128)
1162
if not image is None:
1164
texcoords = coin.SoTextureCoordinatePlane()
1165
s = params.get_param_arch("patternScale")
1166
texcoords.directionS.setValue(s,0,0)
1167
texcoords.directionT.setValue(0,s,0)
1168
self.fcoords = coin.SoCoordinate3()
1169
self.fset = coin.SoIndexedFaceSet()
1170
sep = coin.SoSeparator()
1172
sep.addChild(texcoords)
1173
sep.addChild(self.fcoords)
1174
sep.addChild(self.fset)
1175
vobj.RootNode.addChild(sep)
1176
ArchComponent.ViewProviderComponent.attach(self,vobj)
1178
def updateData(self,obj,prop):
1179
"""Method called when the host object has a property changed.
1181
If the host object's Placement, Shape, or Material has changed, and the
1182
host object has a Material assigned, give the shape the color and
1183
transparency of the Material.
1187
obj: <App::FeaturePython>
1188
The host object that has changed.
1190
The name of the property that has changed.
1193
if prop in ["Placement","Shape","Material"]:
1194
if obj.ViewObject.DisplayMode == "Footprint":
1195
obj.ViewObject.Proxy.setDisplayMode("Footprint")
1196
if hasattr(obj,"Material"):
1197
if obj.Material and obj.Shape:
1198
if hasattr(obj.Material,"Materials"):
1199
activematerials = [obj.Material.Materials[i] for i in range(len(obj.Material.Materials)) if obj.Material.Thicknesses[i] >= 0]
1200
if len(activematerials) == len(obj.Shape.Solids):
1202
for i,mat in enumerate(activematerials):
1203
c = obj.ViewObject.ShapeColor
1204
c = (c[0],c[1],c[2],obj.ViewObject.Transparency/100.0)
1205
if 'DiffuseColor' in mat.Material:
1206
if "(" in mat.Material['DiffuseColor']:
1207
c = tuple([float(f) for f in mat.Material['DiffuseColor'].strip("()").split(",")])
1208
if 'Transparency' in mat.Material:
1209
c = (c[0],c[1],c[2],float(mat.Material['Transparency']))
1210
cols.extend([c for j in range(len(obj.Shape.Solids[i].Faces))])
1211
obj.ViewObject.DiffuseColor = cols
1212
ArchComponent.ViewProviderComponent.updateData(self,obj,prop)
1213
if len(obj.ViewObject.DiffuseColor) > 1:
1214
# force-reset colors if changed
1215
obj.ViewObject.DiffuseColor = obj.ViewObject.DiffuseColor
1217
def getDisplayModes(self,vobj):
1218
"""Define the display modes unique to the Arch Wall.
1220
Define mode Footprint, which only displays the footprint of the wall.
1221
Also add the display modes of the Arch Component.
1226
List containing the names of the new display modes.
1229
modes = ArchComponent.ViewProviderComponent.getDisplayModes(self,vobj)+["Footprint"]
1232
def setDisplayMode(self,mode):
1233
"""Method called when the display mode changes.
1235
Called when the display mode changes, this method can be used to set
1236
data that wasn't available when .attach() was called.
1238
When Footprint is set as display mode, find the faces that make up the
1239
footprint of the wall, and give them a lined texture. Then display
1240
the wall as a wireframe.
1242
Then pass the displaymode onto Arch Component's .setDisplayMode().
1247
The name of the display mode the view provider has switched to.
1252
The name of the display mode the view provider has switched to.
1255
self.fset.coordIndex.deleteValues(0)
1256
self.fcoords.point.deleteValues(0)
1257
if mode == "Footprint":
1258
if hasattr(self,"Object"):
1259
faces = self.Object.Proxy.getFootprint(self.Object)
1265
tri = face.tessellate(1)
1267
verts.append([v.x,v.y,v.z])
1269
fdata.extend([f[0]+idx,f[1]+idx,f[2]+idx,-1])
1271
self.fcoords.point.setValues(verts)
1272
self.fset.coordIndex.setValues(0,len(fdata),fdata)
1274
return ArchComponent.ViewProviderComponent.setDisplayMode(self,mode)
1276
def setupContextMenu(self, vobj, menu):
1277
super().contextMenuAddEdit(menu)
1279
actionFlipDirection = QtGui.QAction(QtGui.QIcon(":/icons/Arch_Wall_Tree.svg"),
1280
translate("Arch", "Flip direction"),
1282
QtCore.QObject.connect(actionFlipDirection,
1283
QtCore.SIGNAL("triggered()"),
1285
menu.addAction(actionFlipDirection)
1287
super().contextMenuAddToggleSubcomponents(menu)
1289
def flipDirection(self):
1291
if hasattr(self,"Object") and self.Object:
1293
if obj.Align == "Left":
1295
FreeCAD.ActiveDocument.recompute()
1296
elif obj.Align == "Right":
1298
FreeCAD.ActiveDocument.recompute()