* BIM: Implement smart base removal for Walls Previously, removing the Base object from an Arch Wall would cause the wall to reset its position to the document origin and could lead to unintended geometric changes for complex walls. This commit introduces a "smart debasing" mechanism integrated into the Component Task Panel's "Remove" button: - For walls based on a single straight line, the operation now preserves the wall's global position and parametric `Length`, making it an independent object. - For walls with complex bases (multi-segment, curved), a warning dialog is now presented to the user, explaining the consequences (shape alteration and position reset) before allowing the operation to proceed. This is supported by new API functions `Arch.is_debasable()` and `Arch.debaseWall()`, which contain the core logic for the feature. Fixes: https://github.com/FreeCAD/FreeCAD/issues/24453 * BIM: Move wall debasing logic into ArchWall proxy The logic for handling the removal of a wall's base object was previously implemented directly within the generic `ComponentTaskPanel` in `ArchComponent.py`. This created a tight coupling, forcing the generic component UI to have specific knowledge about the `ArchWall` type. This commit refactors the implementation to follow a more object-oriented and polymorphic design: 1. A new overridable method, `handleComponentRemoval(subobject)`, has been added to the base `ArchComponent` proxy class. Its default implementation maintains the standard removal behavior. 2. The `_Wall` proxy class in `ArchWall.py` now overrides this method. All wall-specific debasing logic, including the eligibility check and the user-facing warning dialog, now resides entirely within this override. 3. The `ComponentTaskPanel.removeElement` method has been simplified. It is now a generic dispatcher that calls `handleComponentRemoval` on the proxy of the object being edited, with no specific knowledge of object types. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * BIM: Correct user warning The operation can indeed be undone. Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roy-043 <70520633+Roy-043@users.noreply.github.com>
1730 lines
69 KiB
Python
1730 lines
69 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2011 Yorik van Havre <yorik@uncreated.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/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
__title__ = "FreeCAD Arch Commands"
|
|
__author__ = "Yorik van Havre"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
## @package ArchCommands
|
|
# \ingroup ARCH
|
|
# \brief Utility functions for the Arch Workbench
|
|
#
|
|
# This module provides general functions used by Arch tools
|
|
# and utility commands
|
|
|
|
import FreeCAD
|
|
import ArchComponent
|
|
import Draft
|
|
import DraftVecUtils
|
|
|
|
from FreeCAD import Vector
|
|
from draftutils import params
|
|
from draftutils.groups import is_group
|
|
|
|
if FreeCAD.GuiUp:
|
|
from PySide import QtGui, QtCore
|
|
import FreeCADGui
|
|
from draftutils.translate import translate
|
|
else:
|
|
# \cond
|
|
def translate(ctxt, txt):
|
|
return txt
|
|
|
|
# \endcond
|
|
|
|
|
|
# module functions ###############################################
|
|
|
|
|
|
def getStringList(objects):
|
|
"""getStringList(objects): returns a string defining a list
|
|
of objects"""
|
|
result = "["
|
|
for o in objects:
|
|
if len(result) > 1:
|
|
result += ","
|
|
result += "FreeCAD.ActiveDocument." + o.Name
|
|
result += "]"
|
|
return result
|
|
|
|
|
|
def getDefaultColor(objectType):
|
|
"""getDefaultColor(string): returns a color value for the given object
|
|
type (Wall, Structure, Window, WindowGlass)"""
|
|
alpha = 1.0
|
|
if objectType == "Wall":
|
|
c = params.get_param_arch("WallColor")
|
|
elif objectType == "Structure":
|
|
c = params.get_param_arch("StructureColor")
|
|
elif objectType == "WindowGlass":
|
|
c = params.get_param_arch("WindowGlassColor")
|
|
alpha = 1.0 - params.get_param_arch("WindowTransparency") / 100.0
|
|
elif objectType == "Rebar":
|
|
c = params.get_param_arch("RebarColor")
|
|
elif objectType == "Panel":
|
|
c = params.get_param_arch("PanelColor")
|
|
elif objectType == "Space":
|
|
c = params.get_param_arch("defaultSpaceColor")
|
|
elif objectType == "Helpers":
|
|
c = params.get_param_arch("ColorHelpers")
|
|
elif objectType == "Construction":
|
|
c = params.get_param("constructioncolor")
|
|
alpha = 0.2
|
|
else:
|
|
c = params.get_param_view("DefaultShapeColor")
|
|
r, g, b, _ = Draft.get_rgba_tuple(c)
|
|
return (r, g, b, alpha)
|
|
|
|
|
|
def _usedForAttachment(host, obj):
|
|
if not getattr(obj, "AttachmentSupport", []):
|
|
return False
|
|
for sub in obj.AttachmentSupport:
|
|
if sub[0] == host:
|
|
return True
|
|
return False
|
|
|
|
|
|
def addComponents(objectsList, host):
|
|
"""addComponents(objectsList,hostObject): adds the given object or the objects
|
|
from the given list as components to the given host Object. Use this for
|
|
example to add windows to a wall, or to add walls to a cell or floor."""
|
|
if not isinstance(objectsList, list):
|
|
objectsList = [objectsList]
|
|
hostType = Draft.getType(host)
|
|
if hostType in ["Floor", "Building", "Site", "Project", "BuildingPart"]:
|
|
for o in objectsList:
|
|
host.addObject(o)
|
|
elif hostType in [
|
|
"Wall",
|
|
"CurtainWall",
|
|
"Structure",
|
|
"Precast",
|
|
"Window",
|
|
"Roof",
|
|
"Stairs",
|
|
"StructuralSystem",
|
|
"Panel",
|
|
"Component",
|
|
"Pipe",
|
|
]:
|
|
import DraftGeomUtils
|
|
|
|
outList = host.OutListRecursive
|
|
a = host.Additions
|
|
x = getattr(host, "Axes", [])
|
|
for o in objectsList:
|
|
if hasattr(o, "Shape"):
|
|
if Draft.getType(o) == "Window":
|
|
if hasattr(o, "Hosts"):
|
|
if not host in o.Hosts:
|
|
g = o.Hosts
|
|
g.append(host)
|
|
o.Hosts = g
|
|
elif o in outList:
|
|
FreeCAD.Console.PrintWarning(
|
|
translate(
|
|
"Arch", "Cannot add {0} as it is already referenced by {1}."
|
|
).format(o.Label, host.Label)
|
|
+ "\n"
|
|
)
|
|
elif Draft.getType(o) == "Axis":
|
|
if not o in x:
|
|
x.append(o)
|
|
elif DraftGeomUtils.isValidPath(o.Shape) and (hostType in ["Structure", "Precast"]):
|
|
if _usedForAttachment(host, o):
|
|
o.AttachmentSupport = None
|
|
o.MapMode = "Deactivated"
|
|
host.Tool = o
|
|
elif not o in a:
|
|
if hasattr(o, "Shape"):
|
|
a.append(o)
|
|
host.Additions = a
|
|
if hasattr(host, "Axes"):
|
|
host.Axes = x
|
|
elif hostType in ["SectionPlane"]:
|
|
a = host.Objects
|
|
for o in objectsList:
|
|
if not o in a:
|
|
a.append(o)
|
|
host.Objects = a
|
|
elif host.isDerivedFrom("App::DocumentObjectGroup"):
|
|
for o in objectsList:
|
|
host.addObject(o)
|
|
|
|
|
|
def removeComponents(objectsList, host=None):
|
|
"""removeComponents(objectsList,[hostObject]): removes the given component or
|
|
the components from the given list from their parents. If a host object is
|
|
specified, this function will try adding the components as holes to the host
|
|
object instead."""
|
|
if not isinstance(objectsList, list):
|
|
objectsList = [objectsList]
|
|
if host:
|
|
if Draft.getType(host) in [
|
|
"Wall",
|
|
"CurtainWall",
|
|
"Structure",
|
|
"Precast",
|
|
"Window",
|
|
"Roof",
|
|
"Stairs",
|
|
"StructuralSystem",
|
|
"Panel",
|
|
"Component",
|
|
"Pipe",
|
|
]:
|
|
if getattr(host, "Tool", None) in objectsList:
|
|
host.Tool = None
|
|
if hasattr(host, "Axes"):
|
|
a = host.Axes
|
|
for o in objectsList[:]:
|
|
if o in a:
|
|
a.remove(o)
|
|
objectsList.remove(o)
|
|
host.Axes = a
|
|
s = host.Subtractions
|
|
for o in objectsList:
|
|
if Draft.getType(o) == "Window":
|
|
if hasattr(o, "Hosts"):
|
|
if not host in o.Hosts:
|
|
g = o.Hosts
|
|
g.append(host)
|
|
o.Hosts = g
|
|
elif not o in s:
|
|
s.append(o)
|
|
if FreeCAD.GuiUp:
|
|
if Draft.getType(o) != "Roof":
|
|
setAsSubcomponent(o)
|
|
# Avoid cyclic dependency via Attachment Support:
|
|
if hasattr(o, "Base") and o.Base:
|
|
objList = [o, o.Base]
|
|
else:
|
|
objList = [o]
|
|
for i in objList:
|
|
if _usedForAttachment(host, i):
|
|
FreeCAD.Console.PrintMessage(
|
|
translate(
|
|
"Arch",
|
|
"{0} is mapped to {1}, removing the former's "
|
|
+ "Attachment Support to avoid cyclic dependency.",
|
|
).format(o.Label, host.Label)
|
|
+ "\n"
|
|
)
|
|
i.AttachmentSupport = None
|
|
i.MapMode = "Deactivated"
|
|
host.Subtractions = s
|
|
elif Draft.getType(host) in ["SectionPlane"]:
|
|
a = host.Objects
|
|
for o in objectsList:
|
|
if o in a:
|
|
a.remove(o)
|
|
host.Objects = a
|
|
else:
|
|
for o in objectsList:
|
|
if o.InList:
|
|
h = o.InList[0]
|
|
|
|
is_base_removal = hasattr(h, "Base") and h.Base == o
|
|
has_handler = hasattr(h, "Proxy") and hasattr(h.Proxy, "handleComponentRemoval")
|
|
|
|
if is_base_removal and has_handler:
|
|
# Dispatch to the object's own smart removal logic and skip the old code path.
|
|
h.Proxy.handleComponentRemoval(h, o)
|
|
continue
|
|
|
|
tp = Draft.getType(h)
|
|
if tp in ["Floor", "Building", "Site", "BuildingPart"]:
|
|
c = h.Group
|
|
if o in c:
|
|
c.remove(o)
|
|
h.Group = c
|
|
o.ViewObject.show()
|
|
elif tp in ["Wall", "Structure", "Precast"]:
|
|
a = h.Additions
|
|
s = h.Subtractions
|
|
if o in a:
|
|
a.remove(o)
|
|
h.Additions = a
|
|
o.ViewObject.show()
|
|
elif o in s:
|
|
s.remove(o)
|
|
h.Subtractions = s
|
|
o.ViewObject.show()
|
|
elif o == h.Base:
|
|
h.Base = None
|
|
if FreeCAD.GuiUp:
|
|
o.ViewObject.show()
|
|
elif tp in ["SectionPlane"]:
|
|
a = h.Objects
|
|
if o in a:
|
|
a.remove(o)
|
|
h.Objects = a
|
|
if hasattr(o, "Hosts") and Draft.getType(o) == "Window":
|
|
o.Hosts = []
|
|
|
|
|
|
def makeComponent(baseobj=None, name=None, delete=False):
|
|
"""makeComponent([baseobj],[name],[delete]): creates an undefined, non-parametric BIM
|
|
component from the given base object"""
|
|
if not FreeCAD.ActiveDocument:
|
|
FreeCAD.Console.PrintError("No active document. Aborting\n")
|
|
return
|
|
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Component")
|
|
obj.Label = name if name else translate("Arch", "Component")
|
|
ArchComponent.Component(obj)
|
|
if FreeCAD.GuiUp:
|
|
ArchComponent.ViewProviderComponent(obj.ViewObject)
|
|
if baseobj:
|
|
import Part
|
|
|
|
if hasattr(baseobj, "Shape"):
|
|
obj.Shape = baseobj.Shape
|
|
obj.Placement = baseobj.Placement
|
|
if delete:
|
|
FreeCAD.ActiveDocument.removeObject(baseobj.Name)
|
|
else:
|
|
obj.Base = baseobj
|
|
if FreeCAD.GuiUp:
|
|
baseobj.ViewObject.hide()
|
|
elif isinstance(baseobj, Part.Shape):
|
|
obj.Shape = baseobj
|
|
Draft.select(obj)
|
|
return obj
|
|
|
|
|
|
def cloneComponent(obj):
|
|
"""cloneComponent(obj): Creates a clone of an object as an undefined component"""
|
|
c = makeComponent()
|
|
c.CloneOf = obj
|
|
c.Placement = obj.Placement
|
|
c.Label = obj.Label
|
|
if hasattr(obj, "Material"):
|
|
if obj.Material:
|
|
c.Material = obj.Material
|
|
if hasattr(obj, "IfcAttributes"):
|
|
if obj.IfcAttributes:
|
|
c.IfcAttributes = obj.IfcAttributes
|
|
Draft.select(c)
|
|
return c
|
|
|
|
|
|
def setAsSubcomponent(obj):
|
|
"""Sets the given object properly to become a subcomponent (addition, subtraction)
|
|
of an Arch component"""
|
|
Draft.ungroup(obj)
|
|
if params.get_param_arch("applyConstructionStyle"):
|
|
if FreeCAD.GuiUp:
|
|
color = getDefaultColor("Construction")
|
|
if hasattr(obj.ViewObject, "LineColor"):
|
|
obj.ViewObject.LineColor = color
|
|
if hasattr(obj.ViewObject, "PointColor"):
|
|
obj.ViewObject.PointColor = color
|
|
if hasattr(obj.ViewObject, "ShapeColor"):
|
|
obj.ViewObject.ShapeColor = color
|
|
if hasattr(obj.ViewObject, "Transparency"):
|
|
obj.ViewObject.Transparency = int(color[3] * 100)
|
|
obj.ViewObject.hide()
|
|
|
|
|
|
def copyProperties(obj1, obj2):
|
|
"""copyProperties(obj1,obj2): Copies properties values from obj1 to obj2,
|
|
when that property exists in both objects"""
|
|
for prop in obj1.PropertiesList:
|
|
if prop in obj2.PropertiesList:
|
|
if not prop in ["Proxy", "Shape"]:
|
|
setattr(obj2, prop, getattr(obj1, prop))
|
|
if obj1.ViewObject and obj2.ViewObject:
|
|
for prop in obj1.ViewObject.PropertiesList:
|
|
if prop in obj2.ViewObject.PropertiesList:
|
|
if not prop in ["Proxy", "Shape"]:
|
|
setattr(obj2.ViewObject, prop, getattr(obj1.ViewObject, prop))
|
|
|
|
|
|
def splitMesh(obj, mark=True):
|
|
"""splitMesh(object,[mark]): splits the given mesh object into separated components.
|
|
If mark is False, nothing else is done. If True (default), non-manifold components
|
|
will be painted in red."""
|
|
if not obj.isDerivedFrom("Mesh::Feature"):
|
|
return []
|
|
basemesh = obj.Mesh
|
|
comps = basemesh.getSeparateComponents()
|
|
nlist = []
|
|
if comps:
|
|
basename = obj.Name
|
|
FreeCAD.ActiveDocument.removeObject(basename)
|
|
for c in comps:
|
|
newobj = FreeCAD.ActiveDocument.addObject("Mesh::Feature", basename)
|
|
newobj.Mesh = c
|
|
if mark and (not (c.isSolid()) or c.hasNonManifolds()):
|
|
newobj.ViewObject.ShapeColor = (1.0, 0.0, 0.0, 1.0)
|
|
nlist.append(newobj)
|
|
return nlist
|
|
return [obj]
|
|
|
|
|
|
def makeFace(wires, method=2, cleanup=False):
|
|
"""makeFace(wires): makes a face from a list of wires, finding which ones are holes"""
|
|
# print("makeFace: start:", wires)
|
|
import Part
|
|
import DraftGeomUtils
|
|
|
|
if not isinstance(wires, list):
|
|
if len(wires.Vertexes) < 3:
|
|
raise
|
|
return Part.Face(wires)
|
|
elif len(wires) == 1:
|
|
# import Draft;Draft.printShape(wires[0])
|
|
if len(wires[0].Vertexes) < 3:
|
|
raise
|
|
return Part.Face(wires[0])
|
|
|
|
wires = wires[:]
|
|
|
|
# print("makeFace: inner wires found")
|
|
ext = None
|
|
max_length = 0
|
|
# cleaning up rubbish in wires
|
|
if cleanup:
|
|
for i in range(len(wires)):
|
|
wires[i] = DraftGeomUtils.removeInterVertices(wires[i])
|
|
# print("makeFace: garbage removed")
|
|
for w in wires:
|
|
# we assume that the exterior boundary is that one with
|
|
# the biggest bounding box
|
|
if w.BoundBox.DiagonalLength > max_length:
|
|
max_length = w.BoundBox.DiagonalLength
|
|
ext = w
|
|
# print("makeFace: exterior wire", ext)
|
|
wires.remove(ext)
|
|
|
|
if method == 1:
|
|
# method 1: reverse inner wires
|
|
# all interior wires mark a hole and must reverse
|
|
# their orientation, otherwise Part.Face fails
|
|
for w in wires:
|
|
# print("makeFace: reversing", w)
|
|
w.reverse()
|
|
# make sure that the exterior wires comes as first in the list
|
|
wires.insert(0, ext)
|
|
# print("makeFace: done sorting", wires)
|
|
if wires:
|
|
return Part.Face(wires)
|
|
else:
|
|
# method 2: use the cut method
|
|
mf = Part.Face(ext)
|
|
# print("makeFace: external face:", mf)
|
|
for w in wires:
|
|
f = Part.Face(w)
|
|
# print("makeFace: internal face:", f)
|
|
mf = mf.cut(f)
|
|
# print("makeFace: final face:", mf.Faces)
|
|
return mf.Faces[0]
|
|
|
|
|
|
def closeHole(shape):
|
|
"""closeHole(shape): closes a hole in an open shape"""
|
|
import DraftGeomUtils
|
|
import Part
|
|
|
|
# creating an edges lookup table
|
|
lut = {}
|
|
for face in shape.Faces:
|
|
for edge in face.Edges:
|
|
hc = edge.hashCode()
|
|
if hc in lut:
|
|
lut[hc] = lut[hc] + 1
|
|
else:
|
|
lut[hc] = 1
|
|
# filter out the edges shared by more than one face
|
|
bound = []
|
|
for e in shape.Edges:
|
|
if lut[e.hashCode()] == 1:
|
|
bound.append(e)
|
|
bound = Part.__sortEdges__(bound)
|
|
try:
|
|
nface = Part.Face(Part.Wire(bound))
|
|
shell = Part.makeShell(shape.Faces + [nface])
|
|
solid = Part.Solid(shell)
|
|
except Part.OCCError:
|
|
raise
|
|
else:
|
|
return solid
|
|
|
|
|
|
def getCutVolume(cutplane, shapes, clip=False, depth=None):
|
|
"""getCutVolume(cutplane,shapes,[clip,depth]): returns a cut face and a cut volume
|
|
from the given shapes and the given cutting plane. If clip is True, the cutvolume will
|
|
also cut off everything outside the cutplane projection. If depth is non-zero, geometry
|
|
further than this distance will be clipped off"""
|
|
if not shapes:
|
|
return None, None, None
|
|
if not cutplane.Faces:
|
|
return None, None, None
|
|
import Part
|
|
|
|
if not isinstance(shapes, list):
|
|
shapes = [shapes]
|
|
# building boundbox
|
|
bb = shapes[0].BoundBox
|
|
for sh in shapes[1:]:
|
|
bb.add(sh.BoundBox)
|
|
bb.enlarge(1)
|
|
# building cutplane space
|
|
um = vm = wm = 0
|
|
try:
|
|
if hasattr(cutplane, "Shape"):
|
|
p = cutplane.Shape.copy().Faces[0]
|
|
else:
|
|
p = cutplane.copy().Faces[0]
|
|
except Part.OCCError:
|
|
FreeCAD.Console.PrintMessage(translate("Arch", "Invalid cut plane") + "\n")
|
|
return None, None, None
|
|
ce = p.CenterOfMass
|
|
ax = p.normalAt(0, 0)
|
|
prm_range = p.ParameterRange # (uMin, uMax, vMin, vMax)
|
|
u = p.valueAt(prm_range[0], 0).sub(p.valueAt(prm_range[1], 0)).normalize()
|
|
v = u.cross(ax)
|
|
if not bb.isCutPlane(ce, ax):
|
|
# FreeCAD.Console.PrintMessage(translate("Arch","No objects are cut by the plane)+"\n")
|
|
return None, None, None
|
|
else:
|
|
corners = [
|
|
FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin),
|
|
FreeCAD.Vector(bb.XMin, bb.YMax, bb.ZMin),
|
|
FreeCAD.Vector(bb.XMax, bb.YMin, bb.ZMin),
|
|
FreeCAD.Vector(bb.XMax, bb.YMax, bb.ZMin),
|
|
FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMax),
|
|
FreeCAD.Vector(bb.XMin, bb.YMax, bb.ZMax),
|
|
FreeCAD.Vector(bb.XMax, bb.YMin, bb.ZMax),
|
|
FreeCAD.Vector(bb.XMax, bb.YMax, bb.ZMax),
|
|
]
|
|
for c in corners:
|
|
dv = c.sub(ce)
|
|
um1 = DraftVecUtils.project(dv, u).Length
|
|
um = max(um, um1)
|
|
vm1 = DraftVecUtils.project(dv, v).Length
|
|
vm = max(vm, vm1)
|
|
wm1 = DraftVecUtils.project(dv, ax).Length
|
|
wm = max(wm, wm1)
|
|
vu = DraftVecUtils.scaleTo(u, um)
|
|
vui = vu.negative()
|
|
vv = DraftVecUtils.scaleTo(v, vm)
|
|
vvi = vv.negative()
|
|
p1 = ce.add(vu.add(vvi))
|
|
p2 = ce.add(vu.add(vv))
|
|
p3 = ce.add(vui.add(vv))
|
|
p4 = ce.add(vui.add(vvi))
|
|
cutface = Part.makePolygon([p1, p2, p3, p4, p1])
|
|
cutface = Part.Face(cutface)
|
|
cutnormal = DraftVecUtils.scaleTo(ax, wm)
|
|
cutvolume = cutface.extrude(cutnormal)
|
|
cutnormal = cutnormal.negative()
|
|
invcutvolume = cutface.extrude(cutnormal)
|
|
if clip:
|
|
extrudedplane = p.extrude(cutnormal)
|
|
bordervolume = invcutvolume.cut(extrudedplane)
|
|
cutvolume = cutvolume.fuse(bordervolume)
|
|
cutvolume = cutvolume.removeSplitter()
|
|
invcutvolume = extrudedplane
|
|
cutface = p
|
|
if depth:
|
|
depthnormal = DraftVecUtils.scaleTo(cutnormal, depth)
|
|
depthvolume = cutface.extrude(depthnormal)
|
|
depthclipvolume = invcutvolume.cut(depthvolume)
|
|
cutvolume = cutvolume.fuse(depthclipvolume)
|
|
cutvolume = cutvolume.removeSplitter()
|
|
return cutface, cutvolume, invcutvolume
|
|
|
|
|
|
def getShapeFromMesh(mesh, fast=True, tolerance=0.001, flat=False, cut=True):
|
|
import Part
|
|
import MeshPart
|
|
import DraftGeomUtils
|
|
|
|
if mesh.isSolid() and (mesh.countComponents() == 1) and fast:
|
|
# use the best method
|
|
faces = []
|
|
for f in mesh.Facets:
|
|
p = f.Points + [f.Points[0]]
|
|
pts = []
|
|
for pp in p:
|
|
pts.append(FreeCAD.Vector(pp[0], pp[1], pp[2]))
|
|
try:
|
|
f = Part.Face(Part.makePolygon(pts))
|
|
except Exception:
|
|
print("getShapeFromMesh: error building face from polygon")
|
|
# pass
|
|
else:
|
|
faces.append(f)
|
|
shell = Part.makeShell(faces)
|
|
try:
|
|
solid = Part.Solid(shell)
|
|
except Part.OCCError:
|
|
print("getShapeFromMesh: error creating solid")
|
|
else:
|
|
try:
|
|
solid = solid.removeSplitter()
|
|
except Part.OCCError:
|
|
print("getShapeFromMesh: error removing splitter")
|
|
# pass
|
|
return solid
|
|
|
|
# if not mesh.isSolid():
|
|
# print "getShapeFromMesh: non-solid mesh, using slow method"
|
|
faces = []
|
|
segments = mesh.getPlanarSegments(tolerance)
|
|
# print(len(segments))
|
|
for i in segments:
|
|
if len(i) > 0:
|
|
wires = MeshPart.wireFromSegment(mesh, i)
|
|
if wires:
|
|
if flat:
|
|
nwires = []
|
|
for w in wires:
|
|
nwires.append(DraftGeomUtils.flattenWire(w))
|
|
wires = nwires
|
|
try:
|
|
faces.append(makeFace(wires, method=int(cut) + 1))
|
|
except Exception:
|
|
return None
|
|
try:
|
|
se = Part.makeShell(faces)
|
|
se = se.removeSplitter()
|
|
if flat:
|
|
return se
|
|
except Part.OCCError:
|
|
print("getShapeFromMesh: error removing splitter")
|
|
try:
|
|
cp = Part.makeCompound(faces)
|
|
except Part.OCCError:
|
|
print("getShapeFromMesh: error creating compound")
|
|
return None
|
|
else:
|
|
return cp
|
|
else:
|
|
try:
|
|
solid = Part.Solid(se)
|
|
except Part.OCCError:
|
|
print("getShapeFromMesh: error creating solid")
|
|
return se
|
|
else:
|
|
if solid.isClosed():
|
|
return solid
|
|
else:
|
|
return se
|
|
|
|
|
|
def projectToVector(shape, vector):
|
|
"""projectToVector(shape,vector): projects the given shape on the given
|
|
vector"""
|
|
projpoints = []
|
|
minl = 10000000000
|
|
maxl = -10000000000
|
|
for v in shape.Vertexes:
|
|
p = DraftVecUtils.project(v.Point, vector)
|
|
projpoints.append(p)
|
|
l = p.Length
|
|
if p.getAngle(vector) > 1:
|
|
l = -l
|
|
if l > maxl:
|
|
maxl = l
|
|
if l < minl:
|
|
minl = l
|
|
return DraftVecUtils.scaleTo(vector, maxl - minl)
|
|
|
|
|
|
def meshToShape(obj, mark=True, fast=True, tol=0.001, flat=False, cut=True):
|
|
"""meshToShape(object,[mark,fast,tol,flat,cut]): turns a mesh into a shape, joining coplanar facets. If
|
|
mark is True (default), non-solid objects will be marked in red. Fast uses a faster algorithm by
|
|
building a shell from the facets then removing splitter, tol is the tolerance used when converting
|
|
mesh segments to wires, flat will force the wires to be perfectly planar, to be sure they can be
|
|
turned into faces, but this might leave gaps in the final shell. If cut is true, holes in faces are
|
|
made by subtraction (default)"""
|
|
|
|
name = obj.Name
|
|
if "Mesh" in obj.PropertiesList:
|
|
mesh = obj.Mesh
|
|
# plac = obj.Placement
|
|
solid = getShapeFromMesh(mesh, fast, tol, flat, cut)
|
|
if solid:
|
|
if solid.isClosed() and solid.isValid():
|
|
FreeCAD.ActiveDocument.removeObject(name)
|
|
newobj = FreeCAD.ActiveDocument.addObject("Part::Feature", name)
|
|
newobj.Shape = solid
|
|
# newobj.Placement = plac #the placement is already computed in the mesh
|
|
if (not solid.isClosed()) or (not solid.isValid()):
|
|
if mark:
|
|
newobj.ViewObject.ShapeColor = (1.0, 0.0, 0.0, 1.0)
|
|
return newobj
|
|
return None
|
|
|
|
|
|
def removeCurves(shape, dae=False, tolerance=5):
|
|
"""removeCurves(shape,dae,tolerance=5): replaces curved faces in a shape
|
|
with faceted segments. If dae is True, DAE triangulation options are used"""
|
|
import Mesh
|
|
|
|
if dae:
|
|
from importers import importDAE
|
|
|
|
t = importDAE.triangulate(shape.cleaned())
|
|
else:
|
|
t = shape.cleaned().tessellate(tolerance)
|
|
m = Mesh.Mesh(t)
|
|
return getShapeFromMesh(m)
|
|
|
|
|
|
def removeShape(objs, mark=True):
|
|
"""removeShape(objs,mark=True): takes an arch object (wall or structure) built on a cubic shape, and removes
|
|
the inner shape, keeping its length, width and height as parameters. If mark is True, objects that cannot
|
|
be processed by this function will become red."""
|
|
import DraftGeomUtils
|
|
|
|
if not isinstance(objs, list):
|
|
objs = [objs]
|
|
for obj in objs:
|
|
if DraftGeomUtils.isCubic(obj.Shape):
|
|
dims = DraftGeomUtils.getCubicDimensions(obj.Shape)
|
|
if dims:
|
|
name = obj.Name
|
|
tp = Draft.getType(obj)
|
|
print(tp)
|
|
if tp == "Structure":
|
|
FreeCAD.ActiveDocument.removeObject(name)
|
|
import ArchStructure
|
|
|
|
str = ArchStructure.makeStructure(
|
|
length=dims[1], width=dims[2], height=dims[3], name=name
|
|
)
|
|
str.Placement = dims[0]
|
|
elif tp == "Wall":
|
|
FreeCAD.ActiveDocument.removeObject(name)
|
|
import Arch
|
|
|
|
length = dims[1]
|
|
width = dims[2]
|
|
v1 = Vector(length / 2, 0, 0)
|
|
v2 = v1.negative()
|
|
v1 = dims[0].multVec(v1)
|
|
v2 = dims[0].multVec(v2)
|
|
line = Draft.makeLine(v1, v2)
|
|
Arch.makeWall(line, width=width, height=dims[3], name=name)
|
|
else:
|
|
if mark:
|
|
obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0, 1.0)
|
|
|
|
|
|
def mergeCells(objectslist):
|
|
"""mergeCells(objectslist): merges the objects in the given list
|
|
into one. All objects must be of the same type and based on the Cell
|
|
object (cells, floors, buildings, or sites)."""
|
|
if not objectslist:
|
|
return None
|
|
if not isinstance(objectslist, list):
|
|
return None
|
|
if len(objectslist) < 2:
|
|
return None
|
|
typ = Draft.getType(objectslist[0])
|
|
if not (typ in ["Cell", "Floor", "Building", "Site"]):
|
|
return None
|
|
for o in objectslist:
|
|
if Draft.getType(o) != typ:
|
|
return None
|
|
base = objectslist.pop(0)
|
|
for o in objectslist:
|
|
l = base.Components
|
|
for c in o.Components:
|
|
if not c in l:
|
|
l.append(c)
|
|
base.Components = l
|
|
FreeCAD.ActiveDocument.removeObject(o.Name)
|
|
FreeCAD.ActiveDocument.recompute()
|
|
return base
|
|
|
|
|
|
def download(url, force=False):
|
|
"""download(url,force=False): downloads a file from the given URL and saves it in the
|
|
macro path. Returns the path to the saved file. If force is True, the file will be
|
|
downloaded again evn if it already exists."""
|
|
from urllib.request import urlopen
|
|
import os
|
|
|
|
name = url.split("/")[-1]
|
|
macropath = FreeCAD.getUserMacroDir(True)
|
|
filepath = os.path.join(macropath, name)
|
|
if os.path.exists(filepath) and not (force):
|
|
return filepath
|
|
try:
|
|
FreeCAD.Console.PrintMessage("Downloading " + url + " …\n")
|
|
response = urlopen(url)
|
|
s = response.read()
|
|
f = open(filepath, "wb")
|
|
f.write(s)
|
|
f.close()
|
|
except Exception:
|
|
return None
|
|
else:
|
|
return filepath
|
|
|
|
|
|
def check(objectslist, includehidden=False):
|
|
"""check(objectslist,includehidden=False): checks if the given objects contain only solids"""
|
|
objs = Draft.get_group_contents(objectslist)
|
|
if not includehidden:
|
|
objs = Draft.removeHidden(objs)
|
|
bad = []
|
|
for o in objs:
|
|
if not hasattr(o, "Shape"):
|
|
bad.append([o, "is not a Part-based object"])
|
|
else:
|
|
s = o.Shape
|
|
if (not s.isClosed()) and (not (Draft.getType(o) == "Axis")):
|
|
bad.append([o, translate("Arch", "is not closed")])
|
|
elif not s.isValid():
|
|
bad.append([o, translate("Arch", "is not valid")])
|
|
elif (not s.Solids) and (not (Draft.getType(o) == "Axis")):
|
|
bad.append([o, translate("Arch", "does not contain any solid")])
|
|
else:
|
|
f = 0
|
|
for sol in s.Solids:
|
|
f += len(sol.Faces)
|
|
if not sol.isClosed():
|
|
bad.append([o, translate("Arch", "contains a non-closed solid")])
|
|
if len(s.Faces) != f:
|
|
bad.append(
|
|
[o, translate("Arch", "contains faces that are not part of any solid")]
|
|
)
|
|
return bad
|
|
|
|
|
|
def getHost(obj, strict=True):
|
|
"""getHost(obj,[strict]): returns the host of the current object. If strict is true (default),
|
|
the host can only be an object of a higher level than the given one, or in other words, if a wall
|
|
is contained in another wall which is part of a floor, the floor is returned instead of the parent wall
|
|
"""
|
|
import Draft
|
|
|
|
t = Draft.getType(obj)
|
|
for par in obj.InList:
|
|
if par.isDerivedFrom("Part::Feature") or par.isDerivedFrom("App::DocumentObjectGroup"):
|
|
if strict:
|
|
if Draft.getType(par) != t:
|
|
return par
|
|
else:
|
|
return getHost(par, strict)
|
|
else:
|
|
return par
|
|
return None
|
|
|
|
|
|
def pruneIncluded(objectslist, strict=False, silent=False):
|
|
"""pruneIncluded(objectslist,[strict]): removes from a list of Arch objects, those that are subcomponents of
|
|
another shape-based object, leaving only the top-level shapes. If strict is True, the object
|
|
is removed only if the parent is also part of the selection."""
|
|
import Draft
|
|
|
|
newlist = []
|
|
for obj in objectslist:
|
|
toplevel = True
|
|
if obj.isDerivedFrom("Part::Feature"):
|
|
if Draft.getType(obj) not in ["Window", "Clone", "Pipe", "Rebar", "Roof"]:
|
|
for parent in obj.InList:
|
|
if not parent.isDerivedFrom("Part::Feature"):
|
|
pass
|
|
elif parent.isDerivedFrom("Part::Part2DObject"):
|
|
# don't consider 2D objects based on arch elements
|
|
pass
|
|
elif Draft.getType(parent) in [
|
|
"BezCurve",
|
|
"BSpline",
|
|
"Clone",
|
|
"Facebinder",
|
|
"Wire",
|
|
"Project",
|
|
"Roof",
|
|
"Site",
|
|
"Space",
|
|
"Window",
|
|
]:
|
|
pass
|
|
elif parent.isDerivedFrom("PartDesign::FeatureBase"):
|
|
# don't consider a PartDesign_Clone that references obj
|
|
pass
|
|
elif parent.isDerivedFrom("PartDesign::Body") and obj == parent.BaseFeature:
|
|
# don't consider a PartDesign_Body with a PartDesign_Clone that references obj
|
|
pass
|
|
elif parent.isDerivedFrom("PartDesign::SubShapeBinder") or (
|
|
hasattr(parent, "TypeId") and parent.TypeId == "PartDesign::ShapeBinder"
|
|
):
|
|
# don't consider a PartDesign_SubShapeBinder or PartDesign_ShapeBinder referencing this object from another object
|
|
pass
|
|
elif hasattr(parent, "Host") and parent.Host == obj:
|
|
pass
|
|
elif hasattr(parent, "Hosts") and obj in parent.Hosts:
|
|
pass
|
|
elif hasattr(parent, "TypeId") and parent.TypeId == "Part::Mirroring":
|
|
pass
|
|
elif hasattr(parent, "CloneOf"):
|
|
if parent.CloneOf:
|
|
if parent.CloneOf.Name != obj.Name:
|
|
toplevel = False
|
|
else:
|
|
toplevel = False
|
|
else:
|
|
toplevel = False
|
|
|
|
if toplevel == False and strict:
|
|
if parent not in objectslist and parent not in newlist:
|
|
toplevel = True
|
|
if toplevel:
|
|
newlist.append(obj)
|
|
elif not silent:
|
|
FreeCAD.Console.PrintWarning("pruning " + obj.Label + "\n")
|
|
return newlist
|
|
|
|
|
|
def getAllChildren(objectlist):
|
|
"getAllChildren(objectlist): returns all the children of all the object sin the list"
|
|
obs = []
|
|
for o in objectlist:
|
|
if not o in obs:
|
|
obs.append(o)
|
|
if o.OutList:
|
|
l = getAllChildren(o.OutList)
|
|
for c in l:
|
|
if not c in obs:
|
|
obs.append(c)
|
|
return obs
|
|
|
|
|
|
def get_architectural_contents(
|
|
initial_objects,
|
|
recursive=True,
|
|
discover_hosted_elements=True,
|
|
include_components_from_additions=False,
|
|
include_initial_objects_in_result=True,
|
|
):
|
|
"""
|
|
Retrieves a flat list of unique architectural objects that are considered "contents" of or are
|
|
related to the given initial_objects.
|
|
|
|
This includes:
|
|
- Children from .Group properties (hierarchical traversal if recursive=True).
|
|
- Architecturally hosted elements (e.g., windows in walls, rebars in structures)
|
|
if discover_hosted_elements=True.
|
|
- Optionally, components from .Additions properties.
|
|
|
|
The traversal uses a queue and ensures objects are processed only once.
|
|
|
|
Parameters:
|
|
-----------
|
|
initial_objects : App::DocumentObject or list of App::DocumentObject
|
|
The starting object(s) from which to discover contents.
|
|
recursive : bool, optional
|
|
If True (default), recursively find contents of discovered group-like
|
|
objects (those with a .Group property and identified by draftutils.groups.is_group()).
|
|
discover_hosted_elements : bool, optional
|
|
If True (default), try to find elements like windows, doors, rebars
|
|
that are architecturally hosted by other elements encountered during traversal.
|
|
This relies on Draft.getType() and common Arch properties like .Hosts or .Host.
|
|
include_components_from_additions : bool, optional
|
|
If False (default), objects found in .Additions lists are not added.
|
|
Set to True if these components should be part of the discovered contents.
|
|
include_initial_objects_in_result : bool, optional
|
|
If True (default), the objects in initial_objects themselves will be
|
|
included in the output list (if not already discovered as a child of another).
|
|
If False, only their "descendant" contents are returned.
|
|
|
|
Returns:
|
|
--------
|
|
list of App::DocumentObject
|
|
A flat list of unique architectural document objects.
|
|
"""
|
|
|
|
final_contents_list = []
|
|
queue = []
|
|
|
|
if not isinstance(initial_objects, list):
|
|
initial_objects_list = [initial_objects]
|
|
else:
|
|
initial_objects_list = list(initial_objects) # Make a copy
|
|
|
|
queue.extend(initial_objects_list)
|
|
|
|
# Set to keep track of object names already added to the queue or fully processed
|
|
# This prevents duplicates in the queue and reprocessing.
|
|
processed_or_queued_names = set()
|
|
for (
|
|
item
|
|
) in initial_objects_list: # Pre-populate for initial items if they are to be added later
|
|
processed_or_queued_names.add(item.Name)
|
|
|
|
idx = 0 # Use an index for iterating the queue, as it can grow
|
|
while idx < len(queue):
|
|
obj = queue[idx]
|
|
idx += 1
|
|
|
|
# Add the current object to the final list if it's not already there.
|
|
# The decision to include initial_objects is handled by how they are first added to queue
|
|
# and this check.
|
|
if obj not in final_contents_list:
|
|
is_initial = obj in initial_objects_list
|
|
if (is_initial and include_initial_objects_in_result) or not is_initial:
|
|
final_contents_list.append(obj)
|
|
|
|
children_to_add_to_queue_next = []
|
|
|
|
# 1. Hierarchical children from .Group (if recursive)
|
|
if recursive and is_group(obj) and hasattr(obj, "Group") and obj.Group:
|
|
for child in obj.Group:
|
|
if child.Name not in processed_or_queued_names:
|
|
children_to_add_to_queue_next.append(child)
|
|
processed_or_queued_names.add(child.Name) # Mark as queued
|
|
|
|
# 2. Architecturally-hosted elements (if discover_hosted_elements)
|
|
if discover_hosted_elements:
|
|
host_types = ["Wall", "Structure", "CurtainWall", "Precast", "Panel", "Roof"]
|
|
if Draft.getType(obj) in host_types:
|
|
# Hosted elements are typically in the host's InList
|
|
for item_in_inlist in obj.InList:
|
|
element_to_check = item_in_inlist
|
|
if hasattr(item_in_inlist, "getLinkedObject"): # Resolve App::Link
|
|
linked = item_in_inlist.getLinkedObject()
|
|
if linked:
|
|
element_to_check = linked
|
|
|
|
if element_to_check.Name in processed_or_queued_names:
|
|
continue
|
|
|
|
is_confirmed_hosted = False
|
|
element_type = Draft.getType(element_to_check)
|
|
|
|
if element_type == "Window": # This covers Arch Windows and Arch Doors
|
|
if hasattr(element_to_check, "Hosts") and obj in element_to_check.Hosts:
|
|
is_confirmed_hosted = True
|
|
elif element_type == "Rebar":
|
|
if hasattr(element_to_check, "Host") and obj == element_to_check.Host:
|
|
is_confirmed_hosted = True
|
|
|
|
if is_confirmed_hosted:
|
|
children_to_add_to_queue_next.append(element_to_check)
|
|
processed_or_queued_names.add(element_to_check.Name) # Mark as queued
|
|
|
|
# 3. Components from .Additions list (e.g., walls added to a main wall)
|
|
if include_components_from_additions and hasattr(obj, "Additions") and obj.Additions:
|
|
for addition_comp in obj.Additions:
|
|
actual_addition = addition_comp
|
|
if hasattr(addition_comp, "getLinkedObject"): # Resolve if Addition is an App::Link
|
|
linked_add = addition_comp.getLinkedObject()
|
|
if linked_add:
|
|
actual_addition = linked_add
|
|
|
|
if actual_addition.Name not in processed_or_queued_names:
|
|
children_to_add_to_queue_next.append(actual_addition)
|
|
processed_or_queued_names.add(actual_addition.Name) # Mark as queued
|
|
|
|
if children_to_add_to_queue_next:
|
|
# Add newly-discovered children to the end of the queue. This function uses an index
|
|
# (idx) to iterate through the queue, and queue.extend() adds new items to the end. This
|
|
# results in a breadth-first-like traversal for objects discovered at the same "depth"
|
|
# from different parent branches. Children of the current 'obj' will be processed after
|
|
# 'obj's current siblings in the queue.
|
|
queue.extend(children_to_add_to_queue_next)
|
|
|
|
return final_contents_list
|
|
|
|
|
|
def survey(callback=False):
|
|
"""survey(): starts survey mode, where you can click edges and faces to get their lengths or area.
|
|
Clicking on no object (on an empty area) resets the count."""
|
|
if not callback:
|
|
if hasattr(FreeCAD, "SurveyObserver"):
|
|
for label in FreeCAD.SurveyObserver.labels:
|
|
FreeCAD.ActiveDocument.removeObject(label)
|
|
FreeCADGui.Selection.removeObserver(FreeCAD.SurveyObserver)
|
|
del FreeCAD.SurveyObserver
|
|
FreeCADGui.Control.closeDialog()
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
del FreeCAD.SurveyDialog
|
|
else:
|
|
FreeCAD.SurveyObserver = _SurveyObserver(callback=survey)
|
|
FreeCADGui.Selection.addObserver(FreeCAD.SurveyObserver)
|
|
FreeCAD.SurveyDialog = SurveyTaskPanel()
|
|
FreeCADGui.Control.showDialog(FreeCAD.SurveyDialog)
|
|
else:
|
|
sel = FreeCADGui.Selection.getSelectionEx()
|
|
if hasattr(FreeCAD, "SurveyObserver"):
|
|
if not sel:
|
|
if FreeCAD.SurveyObserver.labels:
|
|
for label in FreeCAD.SurveyObserver.labels:
|
|
FreeCAD.ActiveDocument.removeObject(label)
|
|
tl = FreeCAD.SurveyObserver.totalLength
|
|
ta = FreeCAD.SurveyObserver.totalArea
|
|
FreeCAD.SurveyObserver.labels = []
|
|
FreeCAD.SurveyObserver.selection = []
|
|
FreeCAD.SurveyObserver.totalLength = 0
|
|
FreeCAD.SurveyObserver.totalArea = 0
|
|
FreeCAD.SurveyObserver.totalVolume = 0
|
|
if not FreeCAD.SurveyObserver.cancellable:
|
|
FreeCAD.Console.PrintMessage("\n---- Reset ----\n\n")
|
|
FreeCAD.SurveyObserver.cancellable = True
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
FreeCAD.SurveyDialog.newline(tl, ta)
|
|
else:
|
|
FreeCADGui.Selection.removeObserver(FreeCAD.SurveyObserver)
|
|
del FreeCAD.SurveyObserver
|
|
FreeCADGui.Control.closeDialog()
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
del FreeCAD.SurveyDialog
|
|
else:
|
|
FreeCAD.SurveyObserver.cancellable = False
|
|
basesel = FreeCAD.SurveyObserver.selection
|
|
newsels = []
|
|
for o in sel:
|
|
found = False
|
|
for eo in basesel:
|
|
if o.ObjectName == eo.ObjectName:
|
|
if o.SubElementNames == eo.SubElementNames:
|
|
found = True
|
|
if not found:
|
|
newsels.append(o)
|
|
if newsels:
|
|
for o in newsels:
|
|
if hasattr(o.Object, "Shape"):
|
|
n = o.Object.Label
|
|
showUnit = params.get_param_arch("surveyUnits")
|
|
t = ""
|
|
u = FreeCAD.Units.Quantity()
|
|
if not o.HasSubObjects:
|
|
# entire object
|
|
anno = FreeCAD.ActiveDocument.addObject(
|
|
"App::AnnotationLabel", "surveyLabel"
|
|
)
|
|
if hasattr(o.Object.Shape, "CenterOfMass"):
|
|
anno.BasePosition = o.Object.Shape.CenterOfMass
|
|
else:
|
|
anno.BasePosition = o.Object.Shape.BoundBox.Center
|
|
FreeCAD.SurveyObserver.labels.append(anno.Name)
|
|
if o.Object.Shape.Solids:
|
|
u = FreeCAD.Units.Quantity(
|
|
o.Object.Shape.Volume, FreeCAD.Units.Volume
|
|
)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^3", "³")
|
|
anno.LabelText = "v " + t
|
|
FreeCAD.Console.PrintMessage(
|
|
"Object: " + n + ", Element: Whole, Volume: " + t + "\n"
|
|
)
|
|
FreeCAD.SurveyObserver.totalVolume += u.Value
|
|
elif o.Object.Shape.Faces:
|
|
u = FreeCAD.Units.Quantity(
|
|
o.Object.Shape.Area, FreeCAD.Units.Area
|
|
)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^2", "²")
|
|
anno.LabelText = "a " + t
|
|
FreeCAD.Console.PrintMessage(
|
|
"Object: " + n + ", Element: Whole, Area: " + t + "\n"
|
|
)
|
|
FreeCAD.SurveyObserver.totalArea += u.Value
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
FreeCAD.SurveyDialog.update(2, t)
|
|
else:
|
|
u = FreeCAD.Units.Quantity(
|
|
o.Object.Shape.Length, FreeCAD.Units.Length
|
|
)
|
|
t = u.getUserPreferred()[0]
|
|
anno.LabelText = "l " + t
|
|
FreeCAD.Console.PrintMessage(
|
|
"Object: " + n + ", Element: Whole, Length: " + t + "\n"
|
|
)
|
|
FreeCAD.SurveyObserver.totalLength += u.Value
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
FreeCAD.SurveyDialog.update(1, t)
|
|
if FreeCAD.GuiUp and t:
|
|
if showUnit:
|
|
QtGui.QApplication.clipboard().setText(t)
|
|
else:
|
|
QtGui.QApplication.clipboard().setText(str(u.Value))
|
|
else:
|
|
# single element(s)
|
|
for el in o.SubElementNames:
|
|
e = getattr(o.Object.Shape, el)
|
|
anno = FreeCAD.ActiveDocument.addObject(
|
|
"App::AnnotationLabel", "surveyLabel"
|
|
)
|
|
if "Vertex" in el:
|
|
anno.BasePosition = e.Point
|
|
else:
|
|
if hasattr(e, "CenterOfMass"):
|
|
anno.BasePosition = e.CenterOfMass
|
|
else:
|
|
anno.BasePosition = e.BoundBox.Center
|
|
FreeCAD.SurveyObserver.labels.append(anno.Name)
|
|
if "Face" in el:
|
|
u = FreeCAD.Units.Quantity(e.Area, FreeCAD.Units.Area)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^2", "²")
|
|
anno.LabelText = "a " + t
|
|
FreeCAD.Console.PrintMessage(
|
|
"Object: "
|
|
+ n
|
|
+ ", Element: "
|
|
+ el
|
|
+ ", Area: "
|
|
+ t
|
|
+ "\n"
|
|
)
|
|
FreeCAD.SurveyObserver.totalArea += u.Value
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
FreeCAD.SurveyDialog.update(2, t)
|
|
elif "Edge" in el:
|
|
u = FreeCAD.Units.Quantity(e.Length, FreeCAD.Units.Length)
|
|
t = u.getUserPreferred()[0]
|
|
anno.LabelText = "l " + t
|
|
FreeCAD.Console.PrintMessage(
|
|
"Object: "
|
|
+ n
|
|
+ ", Element: "
|
|
+ el
|
|
+ ", Length: "
|
|
+ t
|
|
+ "\n"
|
|
)
|
|
FreeCAD.SurveyObserver.totalLength += u.Value
|
|
if hasattr(FreeCAD, "SurveyDialog"):
|
|
FreeCAD.SurveyDialog.update(1, t)
|
|
elif "Vertex" in el:
|
|
u = FreeCAD.Units.Quantity(e.Z, FreeCAD.Units.Length)
|
|
t = u.getUserPreferred()[0]
|
|
anno.LabelText = "z " + t
|
|
FreeCAD.Console.PrintMessage(
|
|
"Object: "
|
|
+ n
|
|
+ ", Element: "
|
|
+ el
|
|
+ ", Zcoord: "
|
|
+ t
|
|
+ "\n"
|
|
)
|
|
if FreeCAD.GuiUp and t:
|
|
if showUnit:
|
|
QtGui.QApplication.clipboard().setText(t)
|
|
else:
|
|
QtGui.QApplication.clipboard().setText(str(u.Value))
|
|
|
|
FreeCAD.SurveyObserver.selection.extend(newsels)
|
|
if hasattr(FreeCAD, "SurveyObserver"):
|
|
if (
|
|
FreeCAD.SurveyObserver.totalLength
|
|
or FreeCAD.SurveyObserver.totalArea
|
|
or FreeCAD.SurveyObserver.totalVolume
|
|
):
|
|
msg = " Total:"
|
|
if FreeCAD.SurveyObserver.totalLength:
|
|
u = FreeCAD.Units.Quantity(
|
|
FreeCAD.SurveyObserver.totalLength, FreeCAD.Units.Length
|
|
)
|
|
t = u.getUserPreferred()[0]
|
|
msg += " Length: " + t
|
|
if FreeCAD.SurveyObserver.totalArea:
|
|
u = FreeCAD.Units.Quantity(
|
|
FreeCAD.SurveyObserver.totalArea, FreeCAD.Units.Area
|
|
)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^2", "²")
|
|
msg += " Area: " + t
|
|
if FreeCAD.SurveyObserver.totalVolume:
|
|
u = FreeCAD.Units.Quantity(
|
|
FreeCAD.SurveyObserver.totalVolume, FreeCAD.Units.Volume
|
|
)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^3", "³")
|
|
msg += " Volume: " + t
|
|
FreeCAD.Console.PrintMessage(msg + "\n")
|
|
|
|
|
|
class _SurveyObserver:
|
|
"an observer for the survey() function"
|
|
|
|
def __init__(self, callback):
|
|
self.callback = callback
|
|
self.selection = []
|
|
self.labels = []
|
|
self.totalLength = 0
|
|
self.totalArea = 0
|
|
self.totalVolume = 0
|
|
self.cancellable = False
|
|
self.doubleclear = False
|
|
|
|
def addSelection(self, document, object, element, position):
|
|
self.doubleclear = False
|
|
self.callback(True)
|
|
|
|
def clearSelection(self, document):
|
|
if not self.doubleclear:
|
|
self.doubleclear = True
|
|
else:
|
|
self.callback(True)
|
|
|
|
|
|
class SurveyTaskPanel:
|
|
"A task panel for the survey tool"
|
|
|
|
def __init__(self):
|
|
self.form = QtGui.QWidget()
|
|
self.form.setWindowIcon(QtGui.QIcon(":/icons/Arch_Survey.svg"))
|
|
layout = QtGui.QVBoxLayout(self.form)
|
|
llayout = QtGui.QHBoxLayout()
|
|
self.descr = QtGui.QLineEdit()
|
|
llayout.addWidget(self.descr)
|
|
self.addButton = QtGui.QPushButton()
|
|
llayout.addWidget(self.addButton)
|
|
layout.addLayout(llayout)
|
|
self.tree = QtGui.QTreeWidget()
|
|
self.tree.setColumnCount(3)
|
|
layout.addWidget(self.tree)
|
|
blayout = QtGui.QHBoxLayout()
|
|
self.clearButton = QtGui.QPushButton()
|
|
blayout.addWidget(self.clearButton)
|
|
self.copyLength = QtGui.QPushButton()
|
|
blayout.addWidget(self.copyLength)
|
|
self.copyArea = QtGui.QPushButton()
|
|
blayout.addWidget(self.copyArea)
|
|
layout.addLayout(blayout)
|
|
self.export = QtGui.QPushButton()
|
|
layout.addWidget(self.export)
|
|
QtCore.QObject.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.setText)
|
|
QtCore.QObject.connect(self.clearButton, QtCore.SIGNAL("clicked()"), self.clear)
|
|
QtCore.QObject.connect(self.copyLength, QtCore.SIGNAL("clicked()"), self.clipLength)
|
|
QtCore.QObject.connect(self.copyArea, QtCore.SIGNAL("clicked()"), self.clipArea)
|
|
QtCore.QObject.connect(self.export, QtCore.SIGNAL("clicked()"), self.exportCSV)
|
|
QtCore.QObject.connect(
|
|
self.tree, QtCore.SIGNAL("itemClicked(QTreeWidgetItem*,int)"), self.setDescr
|
|
)
|
|
self.retranslateUi(self)
|
|
item = QtGui.QTreeWidgetItem(self.tree)
|
|
self.tree.setCurrentItem(item)
|
|
|
|
def retranslateUi(self, dlg):
|
|
self.form.setWindowTitle(QtGui.QApplication.translate("Arch", "Survey", None))
|
|
self.addButton.setText(QtGui.QApplication.translate("Arch", "Set Description", None))
|
|
self.clearButton.setText(QtGui.QApplication.translate("Arch", "Clear", None))
|
|
self.copyLength.setText(QtGui.QApplication.translate("Arch", "Copy Total Length", None))
|
|
self.copyArea.setText(QtGui.QApplication.translate("Arch", "Copy Total Area", None))
|
|
self.export.setText(QtGui.QApplication.translate("Arch", "Export CSV", None))
|
|
self.tree.setHeaderLabels(
|
|
[
|
|
QtGui.QApplication.translate("Arch", "Description", None),
|
|
QtGui.QApplication.translate("Arch", "Length", None),
|
|
QtGui.QApplication.translate("Arch", "Area", None),
|
|
]
|
|
)
|
|
|
|
def isAllowedAlterSelection(self):
|
|
return True
|
|
|
|
def isAllowedAlterView(self):
|
|
return True
|
|
|
|
def getStandardButtons(self):
|
|
return QtGui.QDialogButtonBox.Close
|
|
|
|
def reject(self):
|
|
if hasattr(FreeCAD, "SurveyObserver"):
|
|
for label in FreeCAD.SurveyObserver.labels:
|
|
FreeCAD.ActiveDocument.removeObject(label)
|
|
FreeCADGui.Selection.removeObserver(FreeCAD.SurveyObserver)
|
|
del FreeCAD.SurveyObserver
|
|
return True
|
|
|
|
def clear(self):
|
|
FreeCADGui.Selection.clearSelection()
|
|
|
|
def clipLength(self):
|
|
if hasattr(FreeCAD, "SurveyObserver"):
|
|
u = FreeCAD.Units.Quantity(FreeCAD.SurveyObserver.totalLength, FreeCAD.Units.Length)
|
|
t = u.getUserPreferred()[0]
|
|
if params.get_param_arch("surveyUnits"):
|
|
QtGui.QApplication.clipboard().setText(t)
|
|
else:
|
|
QtGui.QApplication.clipboard().setText(str(u.Value / u.getUserPreferred()[1]))
|
|
|
|
def clipArea(self):
|
|
if hasattr(FreeCAD, "SurveyObserver"):
|
|
u = FreeCAD.Units.Quantity(FreeCAD.SurveyObserver.totalArea, FreeCAD.Units.Area)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^2", "²")
|
|
if params.get_param_arch("surveyUnits"):
|
|
QtGui.QApplication.clipboard().setText(t)
|
|
else:
|
|
QtGui.QApplication.clipboard().setText(str(u.Value / u.getUserPreferred()[1]))
|
|
|
|
def newline(self, length=0, area=0):
|
|
FreeCADGui.Selection.clearSelection()
|
|
item = QtGui.QTreeWidgetItem(self.tree)
|
|
if length or area:
|
|
item.setText(0, QtGui.QApplication.translate("Arch", "Total", None))
|
|
item.setToolTip(0, "total")
|
|
f = QtGui.QFont()
|
|
f.setBold(True)
|
|
item.setFont(0, f)
|
|
item.setFont(1, f)
|
|
item.setFont(2, f)
|
|
else:
|
|
item.setText(0, self.descr.text())
|
|
self.descr.setText("")
|
|
self.tree.setCurrentItem(item)
|
|
if length:
|
|
u = FreeCAD.Units.Quantity(length, FreeCAD.Units.Length)
|
|
t = u.getUserPreferred()[0]
|
|
item.setText(1, t)
|
|
if area:
|
|
u = FreeCAD.Units.Quantity(area, FreeCAD.Units.Area)
|
|
t = u.getUserPreferred()[0]
|
|
t = t.replace("^2", "²")
|
|
item.setText(2, t)
|
|
if length or area:
|
|
item = QtGui.QTreeWidgetItem(self.tree)
|
|
self.tree.setCurrentItem(item)
|
|
|
|
def update(self, column, txt):
|
|
item = QtGui.QTreeWidgetItem(self.tree)
|
|
self.tree.setCurrentItem(item)
|
|
item.setText(column, txt)
|
|
|
|
def setDescr(self, item, col):
|
|
self.descr.setText(item.text(0))
|
|
|
|
def setText(self):
|
|
item = self.tree.currentItem()
|
|
if item:
|
|
item.setText(0, self.descr.text())
|
|
self.descr.setText("")
|
|
|
|
def exportCSV(self):
|
|
import csv
|
|
|
|
rows = self.tree.topLevelItemCount()
|
|
if rows:
|
|
filename = QtGui.QFileDialog.getSaveFileName(
|
|
QtGui.QApplication.activeWindow(),
|
|
translate("Arch", "Export CSV File"),
|
|
None,
|
|
"CSV file (*.csv)",
|
|
)
|
|
if filename:
|
|
with open(filename[0].encode("utf8"), "w") as csvfile:
|
|
csvfile = csv.writer(csvfile, delimiter="\t")
|
|
suml = 0
|
|
for i in range(rows):
|
|
item = self.tree.topLevelItem(i)
|
|
row = []
|
|
row.append(item.text(0))
|
|
if item.text(1):
|
|
u = FreeCAD.Units.Quantity(item.text(1))
|
|
if item.toolTip(0) == "total":
|
|
row.append("=SUM(B" + str(suml + 1) + ":B" + str(i) + ")")
|
|
else:
|
|
row.append(u.Value / u.getUserPreferred()[1])
|
|
row.append(u.getUserPreferred()[2])
|
|
else:
|
|
row.extend(["", ""])
|
|
if item.text(2):
|
|
t = item.text(2).replace("²", "^2")
|
|
u = FreeCAD.Units.Quantity(t)
|
|
if item.toolTip(0) == "total":
|
|
row.append("=SUM(D" + str(suml + 1) + ":D" + str(i) + ")")
|
|
else:
|
|
row.append(u.Value / u.getUserPreferred()[1])
|
|
row.append(u.getUserPreferred()[2])
|
|
else:
|
|
row.extend(["", ""])
|
|
csvfile.writerow(row)
|
|
if item.toolTip(0) == "total":
|
|
suml = i + 1
|
|
print("successfully exported ", filename[0])
|
|
|
|
|
|
def toggleIfcBrepFlag(obj):
|
|
"""toggleIfcBrepFlag(obj): toggles the IFC brep flag of the given object, forcing it
|
|
to be exported as brep geometry or not."""
|
|
if not hasattr(obj, "IfcData"):
|
|
FreeCAD.Console.PrintMessage(
|
|
translate("Arch", "Object does not have settable IFC attributes")
|
|
)
|
|
else:
|
|
d = obj.IfcData
|
|
if "FlagForceBrep" in d:
|
|
if d["FlagForceBrep"] == "True":
|
|
d["FlagForceBrep"] = "False"
|
|
FreeCAD.Console.PrintMessage(
|
|
translate("Arch", "Disabling B-rep force flag of object")
|
|
+ " "
|
|
+ obj.Label
|
|
+ "\n"
|
|
)
|
|
else:
|
|
d["FlagForceBrep"] = "True"
|
|
FreeCAD.Console.PrintMessage(
|
|
translate("Arch", "Enabling B-rep force flag of object")
|
|
+ " "
|
|
+ obj.Label
|
|
+ "\n"
|
|
)
|
|
else:
|
|
d["FlagForceBrep"] = "True"
|
|
FreeCAD.Console.PrintMessage(
|
|
translate("Arch", "Enabling B-rep force flag of object") + " " + obj.Label + "\n"
|
|
)
|
|
obj.IfcData = d
|
|
|
|
|
|
def makeCompoundFromSelected(objects=None):
|
|
"""makeCompoundFromSelected([objects]): Creates a new compound object from the given
|
|
subobjects (faces, edges) or from the selection if objects is None"""
|
|
import FreeCADGui
|
|
import Part
|
|
|
|
so = []
|
|
if not objects:
|
|
objects = FreeCADGui.Selection.getSelectionEx()
|
|
if not isinstance(objects, list):
|
|
objects = [objects]
|
|
for o in objects:
|
|
so.extend(o.SubObjects)
|
|
if so:
|
|
c = Part.makeCompound(so)
|
|
Part.show(c)
|
|
|
|
|
|
def cleanArchSplitter(objects=None):
|
|
"""cleanArchSplitter([objects]): removes the splitters from the base shapes
|
|
of the given Arch objects or selected Arch objects if objects is None"""
|
|
import FreeCAD
|
|
import FreeCADGui
|
|
|
|
if not objects:
|
|
objects = FreeCADGui.Selection.getSelection()
|
|
if not isinstance(objects, list):
|
|
objects = [objects]
|
|
for obj in objects:
|
|
if hasattr(obj, "Shape"):
|
|
if hasattr(obj, "Base"):
|
|
if obj.Base:
|
|
print("Attempting to clean splitters from ", obj.Label)
|
|
base = obj.Base.getLinkedObject()
|
|
if base.isDerivedFrom("Part::Feature"):
|
|
if not base.Shape.isNull():
|
|
base.Shape = base.Shape.removeSplitter()
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
|
|
def rebuildArchShape(objects=None):
|
|
"""rebuildArchShape([objects]): takes the faces from the base shape of the given (or selected
|
|
if objects is None) Arch objects, and tries to rebuild a valid solid from them."""
|
|
import FreeCAD
|
|
import Part
|
|
|
|
if not objects and FreeCAD.GuiUp:
|
|
objects = FreeCADGui.Selection.getSelection()
|
|
if not isinstance(objects, list):
|
|
objects = [objects]
|
|
for obj in objects:
|
|
success = False
|
|
if hasattr(obj, "Shape"):
|
|
if hasattr(obj, "Base"):
|
|
if obj.Base:
|
|
try:
|
|
print("Attempting to rebuild ", obj.Label)
|
|
base = obj.Base.getLinkedObject()
|
|
if base.isDerivedFrom("Part::Feature"):
|
|
if not base.Shape.isNull():
|
|
faces = []
|
|
for f in base.Shape.Faces:
|
|
f2 = Part.Face(f.Wires)
|
|
# print("rebuilt face: isValid is ", f2.isValid())
|
|
faces.append(f2)
|
|
if faces:
|
|
shell = Part.Shell(faces)
|
|
if shell:
|
|
# print("rebuilt shell: isValid is ", shell.isValid())
|
|
solid = Part.Solid(shell)
|
|
if solid:
|
|
if not solid.isValid():
|
|
solid.sewShape()
|
|
solid = Part.Solid(solid)
|
|
# print("rebuilt solid: isValid is ",solid.isValid())
|
|
if solid.isValid():
|
|
base.Shape = solid
|
|
success = True
|
|
except Exception:
|
|
pass
|
|
if not success:
|
|
print("Failed to rebuild a valid solid for object ", obj.Name)
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
|
|
def getExtrusionData(shape, sortmethod="area"):
|
|
"""If a shape has been extruded, returns the base face, and extrusion vector.
|
|
|
|
Determines if a shape appears to have been extruded from some base face, and
|
|
extruded at the normal from that base face. IE: it looks like a cuboid.
|
|
https://en.wikipedia.org/wiki/Cuboid#Rectangular_cuboid
|
|
|
|
If this is the case, returns what appears to be the base face, and the vector
|
|
used to make that extrusion.
|
|
|
|
The base face is determined based on the sortmethod parameter, which can either
|
|
be:
|
|
|
|
"area" = Of the faces with the smallest area, the one with the lowest z coordinate.
|
|
"z" = The face with the lowest z coordinate.
|
|
a 3D vector = the face which center is closest to the given 3D point
|
|
|
|
Parameters
|
|
----------
|
|
shape: <Part.Shape>
|
|
Shape to examine.
|
|
sortmethod: {"area", "z"}
|
|
Which sorting algorithm to use to determine the base face.
|
|
|
|
Returns
|
|
-------
|
|
Extrusion data: list
|
|
Two item list containing the base face, and the vector used to create the
|
|
extrusion. In that order.
|
|
Failure: None
|
|
Returns None if the object does not appear to be an extrusion.
|
|
"""
|
|
|
|
if shape.isNull():
|
|
return None
|
|
if not shape.Solids:
|
|
return None
|
|
if len(shape.Faces) < 3:
|
|
return None
|
|
# build faces list with normals
|
|
faces = []
|
|
import Part
|
|
|
|
for f in shape.Faces:
|
|
try:
|
|
faces.append([f, f.normalAt(0, 0)])
|
|
except Part.OCCError:
|
|
return None
|
|
# find opposite normals pairs
|
|
pairs = []
|
|
for i1, f1 in enumerate(faces):
|
|
for i2, f2 in enumerate(faces):
|
|
if f1[0].hashCode() != f2[0].hashCode():
|
|
if round(f1[1].getAngle(f2[1]), 4) == 3.1416:
|
|
pairs.append([i1, i2])
|
|
if not pairs:
|
|
return None
|
|
valids = []
|
|
for pair in pairs:
|
|
hc = [faces[pair[0]][0].hashCode(), faces[pair[1]][0].hashCode()]
|
|
# check if other normals are all at 90 degrees
|
|
ok = True
|
|
for f in faces:
|
|
if f[0].hashCode() not in hc:
|
|
if round(f[1].getAngle(faces[pair[0]][1]), 4) != 1.5708:
|
|
ok = False
|
|
if ok:
|
|
# prefer the face with the lowest z
|
|
if faces[pair[0]][0].CenterOfMass.z < faces[pair[1]][0].CenterOfMass.z:
|
|
valids.append(
|
|
[
|
|
faces[pair[0]][0],
|
|
faces[pair[1]][0].CenterOfMass.sub(faces[pair[0]][0].CenterOfMass),
|
|
]
|
|
)
|
|
else:
|
|
valids.append(
|
|
[
|
|
faces[pair[1]][0],
|
|
faces[pair[0]][0].CenterOfMass.sub(faces[pair[1]][0].CenterOfMass),
|
|
]
|
|
)
|
|
if valids:
|
|
if sortmethod == "z":
|
|
valids.sort(key=lambda v: v[0].CenterOfMass.z)
|
|
elif sortmethod == "area":
|
|
# sort by smallest area
|
|
valids.sort(key=lambda v: v[0].Area)
|
|
else:
|
|
valids.sort(key=lambda v: (v[0].CenterOfMass.sub(sortmethod)).Length)
|
|
return valids[0]
|
|
return None
|
|
|
|
|
|
def printMessage(message):
|
|
FreeCAD.Console.PrintMessage(message)
|
|
if FreeCAD.GuiUp:
|
|
QtGui.QMessageBox.information(None, "", message)
|
|
|
|
|
|
def printWarning(message):
|
|
FreeCAD.Console.PrintMessage(message)
|
|
if FreeCAD.GuiUp:
|
|
QtGui.QMessageBox.warning(None, "", message)
|
|
|
|
|
|
def makeIfcSpreadsheet(archobj=None):
|
|
ifc_container = None
|
|
for obj in FreeCAD.ActiveDocument.Objects:
|
|
if obj.Name == "IfcPropertiesContainer":
|
|
ifc_container = obj
|
|
if not ifc_container:
|
|
ifc_container = FreeCAD.ActiveDocument.addObject(
|
|
"App::DocumentObjectGroup", "IfcPropertiesContainer"
|
|
)
|
|
import Spreadsheet
|
|
|
|
ifc_spreadsheet = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet", "IfcProperties")
|
|
ifc_spreadsheet.set("A1", translate("Arch", "Category"))
|
|
ifc_spreadsheet.set("B1", translate("Arch", "Key"))
|
|
ifc_spreadsheet.set("C1", translate("Arch", "Type"))
|
|
ifc_spreadsheet.set("D1", translate("Arch", "Value"))
|
|
ifc_spreadsheet.set("E1", translate("Arch", "Unit"))
|
|
ifc_container.addObject(ifc_spreadsheet)
|
|
if archobj:
|
|
if hasattr(obj, "IfcProperties"):
|
|
archobj.IfcProperties = ifc_spreadsheet
|
|
return ifc_spreadsheet
|
|
else:
|
|
FreeCAD.Console.PrintWarning(
|
|
translate(
|
|
"Arch",
|
|
"The object does not have an IfcProperties attribute. Cancel spreadsheet creation for object:",
|
|
)
|
|
+ " "
|
|
+ archobj.Label
|
|
)
|
|
FreeCAD.ActiveDocument.removeObject(ifc_spreadsheet)
|
|
else:
|
|
return ifc_spreadsheet
|