| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | __title__ = "FreeCAD Arch Component" |
| | __author__ = "Yorik van Havre" |
| | __url__ = "https://www.freecad.org" |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | """This module provides the base Arch component class, that is shared |
| | by all of the Arch BIM objects. |
| | |
| | Examples |
| | -------- |
| | TODO put examples here. |
| | """ |
| |
|
| | import math |
| |
|
| | import FreeCAD |
| | import ArchCommands |
| | import ArchIFC |
| | import Draft |
| |
|
| | from draftutils import params |
| |
|
| | if FreeCAD.GuiUp: |
| | from PySide import QtGui, QtCore |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | import FreeCADGui |
| | from draftutils.translate import translate |
| | else: |
| | |
| | def translate(ctxt, txt): |
| | return txt |
| |
|
| | def QT_TRANSLATE_NOOP(ctxt, txt): |
| | return txt |
| |
|
| | |
| |
|
| |
|
| | def addToComponent(compobject, addobject, mod=None): |
| | """Add an object to a component's properties. |
| | |
| | Does not run if the addobject already exists in the component's properties. |
| | Adds the object to the first property found of Base, Group, or Hosts. |
| | |
| | If mod is provided, adds the object to that property instead. |
| | |
| | Parameters |
| | ---------- |
| | compobject: <ArchComponent.Component> |
| | The component object to add the object to. |
| | addobject: <App::DocumentObject> |
| | The object to add to the component. |
| | mod: str, optional |
| | The property to add the object to. |
| | """ |
| |
|
| | import Draft |
| |
|
| | if compobject == addobject: |
| | return |
| | |
| | found = False |
| | attribs = ["Additions", "Objects", "Components", "Subtractions", "Base", "Group", "Hosts"] |
| | for a in attribs: |
| | if hasattr(compobject, a): |
| | if a == "Base": |
| | if addobject == getattr(compobject, a): |
| | found = True |
| | else: |
| | if addobject in getattr(compobject, a): |
| | found = True |
| | if not found: |
| | if mod: |
| | if hasattr(compobject, mod): |
| | if mod == "Base": |
| | setattr(compobject, mod, addobject) |
| | addobject.ViewObject.hide() |
| | elif mod == "Axes": |
| | if Draft.getType(addobject) == "Axis": |
| | l = getattr(compobject, mod) |
| | l.append(addobject) |
| | setattr(compobject, mod, l) |
| | else: |
| | l = getattr(compobject, mod) |
| | l.append(addobject) |
| | setattr(compobject, mod, l) |
| | if mod != "Objects": |
| | addobject.ViewObject.hide() |
| | if Draft.getType(compobject) == "PanelSheet": |
| | addobject.Placement.move(compobject.Placement.Base.negative()) |
| | else: |
| | for a in attribs[:3]: |
| | if hasattr(compobject, a): |
| | l = getattr(compobject, a) |
| | l.append(addobject) |
| | setattr(compobject, a, l) |
| | addobject.ViewObject.hide() |
| | break |
| |
|
| |
|
| | def removeFromComponent(compobject, subobject): |
| | """Remove the object from the given component. |
| | |
| | Try to find the object in the component's properties. If found, remove the |
| | object. |
| | |
| | If the object is not found, add the object in the component's Subtractions |
| | property. |
| | |
| | Parameters |
| | ---------- |
| | compobject: <ArchComponent.Component> |
| | The component to remove the object from. |
| | subobject: <App::DocumentObject> |
| | The object to remove from the component. |
| | """ |
| |
|
| | if compobject == subobject: |
| | return |
| | found = False |
| | attribs = [ |
| | "Additions", |
| | "Subtractions", |
| | "Objects", |
| | "Components", |
| | "Base", |
| | "Axes", |
| | "Fixtures", |
| | "Group", |
| | "Hosts", |
| | ] |
| | for a in attribs: |
| | if hasattr(compobject, a): |
| | if a == "Base": |
| | if subobject == getattr(compobject, a): |
| | setattr(compobject, a, None) |
| | subobject.ViewObject.show() |
| | found = True |
| | else: |
| | if subobject in getattr(compobject, a): |
| | l = getattr(compobject, a) |
| | l.remove(subobject) |
| | setattr(compobject, a, l) |
| | subobject.ViewObject.show() |
| | if Draft.getType(compobject) == "PanelSheet": |
| | subobject.Placement.move(compobject.Placement.Base) |
| | found = True |
| | if not found: |
| | if hasattr(compobject, "Subtractions"): |
| | l = compobject.Subtractions |
| | l.append(subobject) |
| | compobject.Subtractions = l |
| | if (Draft.getType(subobject) != "Window") and ( |
| | not Draft.isClone(subobject, "Window", True) |
| | ): |
| | ArchCommands.setAsSubcomponent(subobject) |
| |
|
| |
|
| | class Component(ArchIFC.IfcProduct): |
| | """The Arch Component object. |
| | |
| | Acts as a base for all other Arch objects, such as Arch walls and Arch |
| | structures. Its properties and behaviours are common to all Arch objects. |
| | |
| | You can learn more about Arch Components, and the purpose of Arch |
| | Components here: https://wiki.freecad.org/Arch_Component |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The object to turn into an Arch Component |
| | """ |
| |
|
| | def __init__(self, obj): |
| | obj.Proxy = self |
| | self.Type = "Component" |
| | Component.setProperties(self, obj) |
| |
|
| | def setProperties(self, obj): |
| | """Give the component its component specific properties, such as material. |
| | |
| | You can learn more about properties here: |
| | https://wiki.freecad.org/property |
| | """ |
| |
|
| | ArchIFC.IfcProduct.setProperties(self, obj) |
| |
|
| | pl = obj.PropertiesList |
| | if not "Base" in pl: |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "Base", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "The base object this component is built upon"), |
| | locked=True, |
| | ) |
| | if not "CloneOf" in pl: |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "CloneOf", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "The object this component is cloning"), |
| | locked=True, |
| | ) |
| | if not "Additions" in pl: |
| | obj.addProperty( |
| | "App::PropertyLinkList", |
| | "Additions", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "Other shapes that are appended to this object"), |
| | locked=True, |
| | ) |
| | if not "Subtractions" in pl: |
| | obj.addProperty( |
| | "App::PropertyLinkList", |
| | "Subtractions", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "Other shapes that are subtracted from this object" |
| | ), |
| | locked=True, |
| | ) |
| | if not "Description" in pl: |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "Description", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "An optional description for this component"), |
| | locked=True, |
| | ) |
| | if not "Tag" in pl: |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "Tag", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "An optional tag for this component"), |
| | locked=True, |
| | ) |
| | if not "StandardCode" in pl: |
| | obj.addProperty( |
| | "App::PropertyString", |
| | "StandardCode", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "An optional standard (OmniClass, etc.) code for this component", |
| | ), |
| | locked=True, |
| | ) |
| | if not "Material" in pl: |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "Material", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "A material for this object"), |
| | locked=True, |
| | ) |
| | if "BaseMaterial" in pl: |
| | obj.Material = obj.BaseMaterial |
| | obj.removeProperty("BaseMaterial") |
| | FreeCAD.Console.PrintMessage( |
| | "Upgrading " + obj.Label + " BaseMaterial property to Material\n" |
| | ) |
| | if not "MoveBase" in pl: |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "MoveBase", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "Specifies if moving this object moves its base instead" |
| | ), |
| | locked=True, |
| | ) |
| | obj.MoveBase = params.get_param_arch("MoveBase") |
| | if not "MoveWithHost" in pl: |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "MoveWithHost", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Specifies if this object must move together when its host is moved", |
| | ), |
| | locked=True, |
| | ) |
| | obj.MoveWithHost = params.get_param_arch("MoveWithHost") |
| | if not "VerticalArea" in pl: |
| | obj.addProperty( |
| | "App::PropertyArea", |
| | "VerticalArea", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "The area of all vertical faces of this object"), |
| | locked=True, |
| | ) |
| | obj.setEditorMode("VerticalArea", 1) |
| | if not "HorizontalArea" in pl: |
| | obj.addProperty( |
| | "App::PropertyArea", |
| | "HorizontalArea", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "The area of the projection of this object onto the XY plane" |
| | ), |
| | locked=True, |
| | ) |
| | obj.setEditorMode("HorizontalArea", 1) |
| | if not "PerimeterLength" in pl: |
| | obj.addProperty( |
| | "App::PropertyLength", |
| | "PerimeterLength", |
| | "Component", |
| | QT_TRANSLATE_NOOP("App::Property", "The perimeter length of the horizontal area"), |
| | locked=True, |
| | ) |
| | obj.setEditorMode("PerimeterLength", 1) |
| | if not "HiRes" in pl: |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "HiRes", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "An optional higher-resolution mesh or shape for this object" |
| | ), |
| | locked=True, |
| | ) |
| | if not "Axis" in pl: |
| | obj.addProperty( |
| | "App::PropertyLink", |
| | "Axis", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "An optional axis or axis system on which this object should be duplicated", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | self.Subvolume = None |
| | |
| |
|
| | def onDocumentRestored(self, obj): |
| | """Method run when the document is restored. Re-add the Arch component properties. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | """ |
| | Component.setProperties(self, obj) |
| |
|
| | def execute(self, obj): |
| | """Method run when the object is recomputed. |
| | |
| | If the object is a clone, just copy the shape it's cloned from. |
| | |
| | Process subshapes of the object to add additions, and subtract |
| | subtractions from the object's shape. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | """ |
| |
|
| | if self.clone(obj): |
| | return |
| | if not self.ensureBase(obj): |
| | return |
| | if obj.Base: |
| | shape = self.spread(obj, obj.Base.Shape) |
| | if obj.Additions or obj.Subtractions: |
| | shape = self.processSubShapes(obj, shape) |
| | obj.Shape = shape |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | self.Type = "Component" |
| |
|
| | def onBeforeChange(self, obj, prop): |
| | """Method called before the object has a property changed. |
| | |
| | Specifically, this method is called before the value changes. |
| | |
| | If "Placement" has changed, record the old placement, so that |
| | .onChanged() can compare between the old and new placement, and move |
| | its children accordingly. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | prop: string |
| | The name of the property that has changed. |
| | """ |
| | if prop == "Placement": |
| | self.oldPlacement = FreeCAD.Placement(obj.Placement) |
| |
|
| | def onChanged(self, obj, prop): |
| | """Method called when the object has a property changed. |
| | |
| | If "Placement" has changed, move any children components that have been |
| | set to move with their host, such that they stay in the same location |
| | to this component. |
| | |
| | Also call ArchIFC.IfcProduct.onChanged(). |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | prop: string |
| | The name of the property that has changed. |
| | """ |
| |
|
| | ArchIFC.IfcProduct.onChanged(self, obj, prop) |
| |
|
| | if prop == "Placement": |
| | if hasattr(self, "oldPlacement") and self.oldPlacement != obj.Placement: |
| | deltap = obj.Placement.Base.sub(self.oldPlacement.Base) |
| | if deltap.Length == 0: |
| | deltap = None |
| | deltar = obj.Placement.Rotation * self.oldPlacement.Rotation.inverted() |
| | if deltar.Angle < 0.0001: |
| | deltar = None |
| | for child in self.getMovableChildren(obj): |
| | if deltar: |
| | child.Placement.rotate( |
| | self.oldPlacement.Base, |
| | deltar.Axis, |
| | math.degrees(deltar.Angle), |
| | comp=True, |
| | ) |
| | if deltap: |
| | child.Placement.move(deltap) |
| |
|
| | def getMovableChildren(self, obj): |
| | """Find the component's children set to move with their host. |
| | |
| | In this case, children refer to Additions, Subtractions, and objects |
| | linked to this object that refer to it as a host in the "Host" or |
| | "Hosts" properties. Objects are set to move with their host via the |
| | MoveWithHost property. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | |
| | Returns |
| | ------- |
| | list of <App::FeaturePython> |
| | List of child objects set to move with their host. |
| | """ |
| |
|
| | ilist = obj.Additions + obj.Subtractions |
| | for o in obj.InList: |
| | if hasattr(o, "Hosts"): |
| | if obj in o.Hosts: |
| | ilist.append(o) |
| | elif hasattr(o, "Host"): |
| | if obj == o.Host: |
| | ilist.append(o) |
| |
|
| | |
| | |
| | if hasattr(obj, "RailingLeft") and obj.RailingLeft: |
| | ilist.append(obj.RailingLeft) |
| | if hasattr(obj, "RailingRight") and obj.RailingRight: |
| | ilist.append(obj.RailingRight) |
| |
|
| | ilist2 = [] |
| | for o in ilist: |
| | if hasattr(o, "MoveWithHost"): |
| | if o.MoveWithHost: |
| | ilist2.append(o) |
| | else: |
| | ilist2.append(o) |
| | return ilist2 |
| |
|
| | def getParentHeight(self, obj): |
| | """Get a height value from hosts. |
| | |
| | Recursively crawl hosts until a Floor or BuildingPart is found, then |
| | return the value of its Height property. |
| | |
| | Parameters |
| | --------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | |
| | Returns |
| | ------- |
| | <App::PropertyLength> |
| | The Height value of the found Floor or BuildingPart. |
| | """ |
| |
|
| | for parent in obj.InList: |
| | if Draft.getType(parent) in ["Floor", "BuildingPart"]: |
| | if obj in parent.Group: |
| | if parent.HeightPropagate: |
| | if parent.Height.Value: |
| | return parent.Height.Value |
| | |
| | for parent in obj.InList: |
| | if hasattr(parent, "Group"): |
| | if obj in parent.Group: |
| | return self.getParentHeight(parent) |
| | |
| | for parent in obj.InList: |
| | if hasattr(parent, "Additions"): |
| | if obj in parent.Additions: |
| | return self.getParentHeight(parent) |
| | return 0 |
| |
|
| | def clone(self, obj): |
| | """If the object is a clone, copy the shape. |
| | |
| | If the object is a clone according to the "CloneOf" property, copy the |
| | object's shape and several properties relating to shape, such as |
| | "Length" and "Thickness". |
| | |
| | Only perform the copy if this object and the object it's a clone of are |
| | of the same type, or if the object has the type "Component" or |
| | "BuildingPart". |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | |
| | Returns |
| | ------- |
| | bool |
| | True if the copy occurs, False if otherwise. |
| | """ |
| |
|
| | if hasattr(obj, "CloneOf"): |
| | if obj.CloneOf: |
| | if (Draft.getType(obj.CloneOf) == Draft.getType(obj)) or ( |
| | Draft.getType(obj) in ["Component", "BuildingPart"] |
| | ): |
| | pl = obj.Placement |
| | |
| | obj.Shape = obj.CloneOf.Shape.copy() |
| | obj.Placement = pl |
| | for prop in [ |
| | "Length", |
| | "Width", |
| | "Height", |
| | "Thickness", |
| | "Area", |
| | "PerimeterLength", |
| | "HorizontalArea", |
| | "VerticalArea", |
| | ]: |
| | if hasattr(obj, prop) and hasattr(obj.CloneOf, prop): |
| | setattr(obj, prop, getattr(obj.CloneOf, prop)) |
| | return True |
| | return False |
| |
|
| | def getSiblings(self, obj): |
| | """Find objects that have the same Base object, and type. |
| | |
| | Look to base object, and find other objects that are based off this |
| | base object. If these objects are the same type, return them. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | |
| | Returns |
| | ------- |
| | list of <App::FeaturePython> |
| | List of objects that have the same Base and type as this component. |
| | """ |
| |
|
| | if not hasattr(obj, "Base"): |
| | return [] |
| | if not obj.Base: |
| | return [] |
| | siblings = [] |
| | for o in obj.Base.InList: |
| | if hasattr(o, "Base"): |
| | if o.Base: |
| | if o.Base.Name == obj.Base.Name: |
| | if o.Name != obj.Name: |
| | if Draft.getType(o) == Draft.getType(obj): |
| | siblings.append(o) |
| | return siblings |
| |
|
| | def getExtrusionData(self, obj): |
| | """Get the object's extrusion data. |
| | |
| | Recursively scrape the Bases of the object, until a Base that is |
| | derived from a <Part::Extrusion> is found. From there, copy the |
| | extrusion to the (0,0,0) origin. |
| | |
| | With this copy, get the <Part.Face> the shape was originally |
| | extruded from, the <Base.Vector> of the extrusion, and the |
| | <Base.Placement> needed to move the copy back to its original |
| | location/orientation. Return this data as a tuple. |
| | |
| | If an object derived from a <Part::Multifuse> is encountered, return |
| | this data as a tuple containing lists. The lists will contain the same |
| | data as above, from each of the objects within the multifuse. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | |
| | Returns |
| | ------- |
| | tuple |
| | Tuple containing: |
| | |
| | 1) The <Part.Face> the object was extruded from. |
| | 2) The <Base.Vector> of the extrusion. |
| | 3) The <Base.Placement> of the extrusion. |
| | """ |
| |
|
| | if hasattr(obj, "CloneOf"): |
| | if obj.CloneOf: |
| | if hasattr(obj.CloneOf, "Proxy"): |
| | if hasattr(obj.CloneOf.Proxy, "getExtrusionData"): |
| | data = obj.CloneOf.Proxy.getExtrusionData(obj.CloneOf) |
| | if data: |
| | return data |
| |
|
| | if obj.Base: |
| | |
| | if ( |
| | hasattr(obj.Base, "Proxy") |
| | and hasattr(obj.Base.Proxy, "getExtrusionData") |
| | and (not obj.Additions) |
| | and (not obj.Subtractions) |
| | ): |
| | if obj.Base.Base: |
| | if obj.Placement.Rotation.Angle < 0.0001: |
| | |
| | data = obj.Base.Proxy.getExtrusionData(obj.Base) |
| | if data: |
| | return data |
| | |
| | |
| | disp = obj.Shape.CenterOfMass.sub(obj.Base.Shape.CenterOfMass) |
| | if isinstance(data[2], (list, tuple)): |
| | ndata2 = [] |
| | for p in data[2]: |
| | p.move(disp) |
| | ndata2.append(p) |
| | return (data[0], data[1], ndata2) |
| | else: |
| | ndata2 = data[2] |
| | ndata2.move(disp) |
| | return (data[0], data[1], ndata2) |
| |
|
| | |
| | elif obj.Base.isDerivedFrom("Part::Extrusion"): |
| | if obj.Base.Base and len(obj.Base.Base.Shape.Wires) == 1: |
| | base, placement = self.rebase(obj.Base.Base.Shape) |
| | extrusion = FreeCAD.Vector(obj.Base.Dir).normalize() |
| | if extrusion.Length == 0: |
| | extrusion = FreeCAD.Vector(0, 0, 1) |
| | else: |
| | extrusion = placement.inverse().Rotation.multVec(extrusion) |
| | if hasattr(obj.Base, "LengthFwd"): |
| | if obj.Base.LengthFwd.Value: |
| | extrusion = extrusion.multiply(obj.Base.LengthFwd.Value) |
| | if not self.isIdentity(obj.Base.Placement): |
| | placement = placement.multiply(obj.Base.Placement) |
| | return (base, extrusion, placement) |
| |
|
| | elif obj.Base.isDerivedFrom("Part::MultiFuse"): |
| | rshapes = [] |
| | revs = [] |
| | rpls = [] |
| | for sub in obj.Base.Shapes: |
| | if sub.isDerivedFrom("Part::Extrusion"): |
| | if sub.Base: |
| | base, placement = self.rebase(sub.Base.Shape) |
| | extrusion = FreeCAD.Vector(sub.Dir).normalize() |
| | if extrusion.Length == 0: |
| | extrusion = FreeCAD.Vector(0, 0, 1) |
| | else: |
| | extrusion = placement.inverse().Rotation.multVec(extrusion) |
| | if hasattr(sub, "LengthFwd"): |
| | if sub.LengthFwd.Value: |
| | extrusion = extrusion.multiply(sub.LengthFwd.Value) |
| | placement = obj.Placement.multiply(placement) |
| | rshapes.append(base) |
| | revs.append(extrusion) |
| | rpls.append(placement) |
| | else: |
| | exdata = ArchCommands.getExtrusionData(sub.Shape) |
| | if exdata: |
| | base, placement = self.rebase(exdata[0]) |
| | extrusion = placement.inverse().Rotation.multVec(exdata[1]) |
| | placement = obj.Placement.multiply(placement) |
| | rshapes.append(base) |
| | revs.append(extrusion) |
| | rpls.append(placement) |
| | if rshapes and revs and rpls: |
| | return (rshapes, revs, rpls) |
| | return None |
| |
|
| | def rebase(self, shape, hint=None): |
| | """Copy a shape to the (0,0,0) origin. |
| | |
| | Create a copy of a shape, such that its center of mass is in the |
| | (0,0,0) origin. |
| | |
| | TODO Determine the way the shape is rotated by this method. |
| | |
| | Return the copy of the shape, and the <Base.Placement> needed to move |
| | the copy back to its original location/orientation. |
| | |
| | Parameters |
| | ---------- |
| | shape: <Part.Shape> |
| | The shape to copy. |
| | hint: <Base.Vector>, optional |
| | If the angle between the normal vector of the shape, and the hint |
| | vector is greater than 90 degrees, the normal will be reversed |
| | before being rotated. |
| | """ |
| |
|
| | import DraftGeomUtils |
| |
|
| | |
| | if not isinstance(shape, list): |
| | shape = [shape] |
| | if hasattr(shape[0], "CenterOfMass"): |
| | v = shape[0].CenterOfMass |
| | else: |
| | v = shape[0].BoundBox.Center |
| |
|
| | |
| | n = DraftGeomUtils.getNormal(shape[0]) |
| | if (not n) or (not n.Length): |
| | n = FreeCAD.Vector(0, 0, 1) |
| |
|
| | |
| | |
| | if hint and hint.getAngle(n) > 1.58: |
| | n = n.negative() |
| |
|
| | r = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), n) |
| | if round(abs(r.Angle), 8) == round(math.pi, 8): |
| | r = FreeCAD.Rotation() |
| |
|
| | shapes = [] |
| | for s in shape: |
| | |
| | s = s.copy() |
| | s.translate(v.negative()) |
| | s.rotate(FreeCAD.Vector(0, 0, 0), r.Axis, math.degrees(-r.Angle)) |
| | shapes.append(s) |
| | p = FreeCAD.Placement() |
| | p.Base = v |
| | p.Rotation = r |
| | if len(shapes) == 1: |
| | return (shapes[0], p) |
| | else: |
| | return (shapes, p) |
| |
|
| | def hideSubobjects(self, obj, prop): |
| | """Hides Additions and Subtractions of this Component when that list changes. |
| | |
| | Intended to be used in conjunction with the .onChanged() method, to |
| | access the property that has changed. |
| | |
| | When an object loses or gains an Addition, this method hides all |
| | Additions. When it gains or loses a Subtraction, this method hides all |
| | Subtractions. |
| | |
| | Does not effect objects of type Window, or clones of Windows. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | prop: string |
| | The name of the property that has changed. |
| | """ |
| |
|
| | if FreeCAD.GuiUp: |
| | if prop in ["Additions", "Subtractions"]: |
| | if hasattr(obj, prop): |
| | for o in getattr(obj, prop): |
| | if (Draft.getType(o) != "Window") and ( |
| | not Draft.isClone(o, "Window", True) |
| | ): |
| | if Draft.getType(obj) == "Wall": |
| | if Draft.getType(o) == "Roof": |
| | continue |
| | o.ViewObject.hide() |
| | elif prop in ["Mesh"]: |
| | if hasattr(obj, prop): |
| | o = getattr(obj, prop) |
| | if o: |
| | o.ViewObject.hide() |
| |
|
| | def handleComponentRemoval(self, obj, subobject): |
| | """ |
| | Default handler for when a component is removed via the Task Panel. |
| | Subclasses can override this to provide special behavior. |
| | """ |
| | removeFromComponent(obj, subobject) |
| |
|
| | def processSubShapes(self, obj, base, placement=None): |
| | """Add Additions and Subtractions to a base shape. |
| | |
| | If Additions exist, fuse them to the base shape. If no base is |
| | provided, just fuse other additions to the first addition. |
| | |
| | If Subtractions exist, cut them from the base shape. Roofs and Windows |
| | are treated uniquely, as they define their own Shape to subtract from |
| | parent shapes using their .getSubVolume() methods. |
| | |
| | TODO determine what the purpose of the placement argument is. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | base: <Part.Shape>, optional |
| | The base shape to add Additions and Subtractions to. |
| | placement: <Base.Placement>, optional |
| | Prior to adding or subtracting subshapes, the <Base.Placement> of |
| | the subshapes are multiplied by the inverse of this parameter. |
| | |
| | Returns |
| | ------- |
| | <Part.Shape> |
| | The base shape, with the additions and subtractions performed. |
| | """ |
| |
|
| | import Draft |
| | import Part |
| |
|
| | |
| |
|
| | if placement: |
| | if self.isIdentity(placement): |
| | placement = None |
| | else: |
| | placement = FreeCAD.Placement(placement) |
| | placement = placement.inverse() |
| |
|
| | |
| | for o in obj.Additions: |
| |
|
| | |
| | |
| | |
| | |
| | if not base or base.isNull(): |
| | if hasattr(o, "Shape"): |
| | base = Part.Shape(o.Shape) |
| | |
| | if placement: |
| | |
| | base.Placement = placement.multiply(base.Placement) |
| | else: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | import ArchWall |
| |
|
| | js = ArchWall.mergeShapes(o, obj) |
| | if js: |
| | add = js.cut(base) |
| | if placement: |
| | |
| | add.Placement = placement.multiply(add.Placement) |
| | base = base.fuse(add) |
| | elif hasattr(o, "Shape"): |
| | if o.Shape and not o.Shape.isNull() and o.Shape.Solids: |
| | |
| | s = o.Shape.copy() |
| | if placement: |
| | |
| | s.Placement = placement.multiply(s.Placement) |
| | if base: |
| | if base.Solids: |
| | try: |
| | base = base.fuse(s) |
| | except Part.OCCError: |
| | print( |
| | "Arch: unable to fuse object ", obj.Name, " with ", o.Name |
| | ) |
| | else: |
| | base = s |
| |
|
| | |
| | subs = obj.Subtractions |
| | for link in obj.InListRecursive: |
| | if hasattr(link, "Host"): |
| | if ( |
| | Draft.getType(link) != "Rebar" |
| | and link.Host == obj |
| | and not self._objectInInternalLinkgroup(link) |
| | ): |
| | subs.append(link) |
| | elif hasattr(link, "Hosts"): |
| | if obj in link.Hosts and not self._objectInInternalLinkgroup(link): |
| | subs.append(link) |
| | for o in subs: |
| | if base: |
| | if base.isNull(): |
| | base = None |
| |
|
| | if base: |
| | subvolume = None |
| |
|
| | if (Draft.getType(o.getLinkedObject()) == "Window") or ( |
| | Draft.isClone(o, "Window", True) |
| | ): |
| | |
| | subvolume = o.getLinkedObject().Proxy.getSubVolume( |
| | o, host=obj |
| | ) |
| | elif (Draft.getType(o) == "Roof") or (Draft.isClone(o, "Roof")): |
| | |
| | subvolume = o.Proxy.getSubVolume(o).copy() |
| | elif hasattr(o, "Subvolume") and hasattr(o.Subvolume, "Shape"): |
| | |
| | |
| | subvolume = o.Subvolume.Shape.copy() |
| | if hasattr(o, "Placement"): |
| | |
| | subvolume.Placement = o.Placement.multiply(subvolume.Placement) |
| |
|
| | if subvolume: |
| | if base.Solids and subvolume.Solids: |
| | if placement: |
| | |
| | subvolume.Placement = placement.multiply(subvolume.Placement) |
| | if len(base.Solids) > 1: |
| | base = Part.makeCompound([sol.cut(subvolume) for sol in base.Solids]) |
| | else: |
| | base = base.cut(subvolume) |
| | elif hasattr(o, "Shape"): |
| | |
| | if o.Shape: |
| | if not o.Shape.isNull(): |
| | if o.Shape.Solids and base.Solids: |
| | |
| | s = o.Shape.copy() |
| | if placement: |
| | |
| | s.Placement = placement.multiply(s.Placement) |
| | try: |
| | if len(base.Solids) > 1: |
| | base = Part.makeCompound( |
| | [sol.cut(s) for sol in base.Solids] |
| | ) |
| | else: |
| | base = base.cut(s) |
| | except Part.OCCError: |
| | print("Arch: unable to cut object ", o.Name, " from ", obj.Name) |
| | return base |
| |
|
| | def spread(self, obj, shape, placement=None): |
| | """Copy the object to its Axis's points. |
| | |
| | If the object has the "Axis" property assigned, create a copy of the |
| | shape for each point on the object assigned as the "Axis". Translate |
| | each of these copies equal to the displacement of the points from the |
| | (0,0,0) origin. |
| | |
| | If the object's "Axis" is unassigned, return the original shape |
| | unchanged. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | shape: <Part.Shape> |
| | The shape to copy. |
| | placement: |
| | Does nothing. |
| | |
| | Returns |
| | ------- |
| | <Part.Shape> |
| | The shape, either spread to the axis points, or unchanged. |
| | """ |
| |
|
| | points = None |
| | if hasattr(obj, "Axis"): |
| | if obj.Axis: |
| | if hasattr(obj.Axis, "Proxy"): |
| | if hasattr(obj.Axis.Proxy, "getPoints"): |
| | points = obj.Axis.Proxy.getPoints(obj.Axis) |
| | if not points: |
| | if hasattr(obj.Axis, "Shape"): |
| | points = [v.Point for v in obj.Axis.Shape.Vertexes] |
| | if points: |
| | shps = [] |
| | for p in points: |
| | |
| | sh = shape.copy() |
| | sh.translate(p) |
| | shps.append(sh) |
| | import Part |
| |
|
| | shape = Part.makeCompound(shps) |
| | return shape |
| |
|
| | def isIdentity(self, placement): |
| | """Check if a placement is *almost* zero. |
| | |
| | Check if a <Base.Placement>'s displacement from (0,0,0) is almost zero, |
| | and if the angle of its rotation about its axis is almost zero. |
| | |
| | Parameters |
| | ---------- |
| | placement: <Base.Placement> |
| | The placement to examine. |
| | |
| | Returns |
| | ------- |
| | bool |
| | Returns true if angle and displacement are almost zero, false it |
| | otherwise. |
| | """ |
| |
|
| | if (placement.Base.Length < 0.000001) and (placement.Rotation.Angle < 0.000001): |
| | return True |
| | return False |
| |
|
| | def applyShape(self, obj, shape, placement, allowinvalid=False, allownosolid=False): |
| | """Check the given shape, then assign it to the object. |
| | |
| | Check if the shape is valid, isn't null, and if it has volume. Remove |
| | redundant edges from the shape. Spread the shape to the "Axis" with |
| | method .spread(). |
| | |
| | Set the object's Shape and Placement to the values given, if |
| | successful. |
| | |
| | Finally, run .computeAreas() method, to calculate the horizontal and |
| | vertical area of the shape. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | shape: <Part.Shape> |
| | The shape to check and apply to the object. |
| | placement: <Base.Placement> |
| | The placement to apply to the object. |
| | allowinvalid: bool, optional |
| | Whether to allow invalid shapes, or to throw an error. |
| | allownosolid: bool, optional |
| | Whether to allow non-solid shapes, or to throw an error. |
| | """ |
| |
|
| | if shape: |
| | if not shape.isNull(): |
| | if shape.isValid(): |
| | if shape.Solids: |
| | if shape.Volume < 0: |
| | shape.reverse() |
| | if shape.Volume < 0: |
| | FreeCAD.Console.PrintError( |
| | translate("Arch", "Error computing the shape of this object") + "\n" |
| | ) |
| | return |
| | import Part |
| |
|
| | try: |
| | r = shape.removeSplitter() |
| | except Part.OCCError: |
| | pass |
| | else: |
| | shape = r |
| | p = self.spread( |
| | obj, shape, placement |
| | ).Placement.copy() |
| | obj.Shape = self.spread(obj, shape, placement) |
| | if not self.isIdentity(placement): |
| | obj.Placement = placement |
| | else: |
| | obj.Placement = p |
| | else: |
| | if allownosolid: |
| | obj.Shape = self.spread(obj, shape, placement) |
| | if not self.isIdentity(placement): |
| | obj.Placement = placement |
| | else: |
| | FreeCAD.Console.PrintWarning( |
| | obj.Label + " " + translate("Arch", "has no solid") + "\n" |
| | ) |
| | else: |
| | if allowinvalid: |
| | obj.Shape = self.spread(obj, shape, placement) |
| | if not self.isIdentity(placement): |
| | obj.Placement = placement |
| | else: |
| | FreeCAD.Console.PrintWarning( |
| | obj.Label + " " + translate("Arch", "has an invalid shape") + "\n" |
| | ) |
| | else: |
| | FreeCAD.Console.PrintWarning( |
| | obj.Label + " " + translate("Arch", "has a null shape") + "\n" |
| | ) |
| | self.computeAreas(obj) |
| |
|
| | def computeAreas(self, obj): |
| | """Compute the area properties of the object's shape. |
| | |
| | This function calculates and assigns the following properties to the object: |
| | - **VerticalArea**: The total area of all vertical faces of the object. |
| | - **HorizontalArea**: The area of the object's projection onto the XY plane. |
| | - **PerimeterLength**: The perimeter of the horizontal area. |
| | |
| | The function uses the `AreaCalculator` helper class to perform these calculations. |
| | Refer to that class for more details on the calculation. |
| | |
| | Parameters |
| | ---------- |
| | obj : App::FeaturePython |
| | The component object whose area properties are to be computed. |
| | """ |
| | calculator = AreaCalculator(obj) |
| | calculator.compute() |
| |
|
| | def isStandardCase(self, obj): |
| | """Determine if the component is a standard case of its IFC type. |
| | |
| | Not all IFC types have a standard case. |
| | |
| | If an object is a standard case or not varies between the different |
| | types. Each type has its own rules to define what is a standard case. |
| | |
| | Rotated objects, or objects with Additions or Subtractions are not |
| | standard cases. |
| | |
| | All objects whose IfcType is suffixed with the string " Sandard Case" |
| | are automatically a standard case. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The component object. |
| | |
| | Returns |
| | ------- |
| | bool |
| | Whether the object is a standard case or not. |
| | """ |
| |
|
| | |
| | if obj.IfcType.endswith("Standard Case"): |
| | return True |
| | |
| | if obj.IfcType + " Standard Case" in ArchIFC.IfcTypes: |
| | |
| | if obj.Additions or obj.Subtractions: |
| | return False |
| | if obj.Placement.Rotation.Axis.getAngle(FreeCAD.Vector(0, 0, 1)) > 0.01: |
| | |
| | return False |
| | if obj.CloneOf: |
| | return obj.CloneOf.Proxy.isStandardCase(obj.CloneOf) |
| | if obj.IfcType == "Wall": |
| | |
| | |
| | |
| | if (not obj.Base) or (len(obj.Base.Shape.Edges) == 1): |
| | if hasattr(obj, "Normal"): |
| | if obj.Normal in [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, 1)]: |
| | return True |
| | elif obj.IfcType in ["Beam", "Column", "Slab"]: |
| | |
| | |
| | |
| | if obj.Base and (len(obj.Base.Shape.Wires) != 1): |
| | return False |
| | if not hasattr(obj, "Normal"): |
| | return False |
| | if hasattr(obj, "Tool") and obj.Tool: |
| | return False |
| | if obj.Normal == FreeCAD.Vector(0, 0, 0): |
| | return True |
| | elif len(obj.Base.Shape.Wires) == 1: |
| | import DraftGeomUtils |
| |
|
| | n = DraftGeomUtils.getNormal(obj.Base.Shape) |
| | if n: |
| | if (n.getAngle(obj.Normal) < 0.01) or ( |
| | abs(n.getAngle(obj.Normal) - 3.14159) < 0.01 |
| | ): |
| | return True |
| | |
| | |
| | |
| | |
| | |
| | |
| | return False |
| |
|
| | def getHosts(self, obj): |
| | """Return the objects that have this one as host, |
| | that is, objects with a "Host" property pointing |
| | at this object, or a "Hosts" property containing |
| | this one. |
| | |
| | Returns |
| | ------- |
| | list of <Arch._Structure> |
| | The BIM Structures hosting this component. |
| | """ |
| |
|
| | hosts = [] |
| |
|
| | for link in obj.InListRecursive: |
| | if hasattr(link, "Host"): |
| | if link.Host == obj and not self._objectInInternalLinkgroup(link): |
| | hosts.append(link) |
| | elif hasattr(link, "Hosts"): |
| | if obj in link.Hosts and not self._objectInInternalLinkgroup(link): |
| | hosts.append(link) |
| | return hosts |
| |
|
| | def ensureBase(self, obj): |
| | """Returns False if the object has a Base but of the wrong type. |
| | Either returns True""" |
| |
|
| | if getattr(obj, "Base", None): |
| | if obj.Base.isDerivedFrom("Part::Feature"): |
| | return True |
| | elif obj.Base.isDerivedFrom("Mesh::Feature"): |
| | return True |
| | else: |
| | import Part |
| |
|
| | if isinstance(getattr(obj.Base, "Shape", None), Part.Shape): |
| | return True |
| | else: |
| | t = translate("Arch", "Wrong base type") |
| | FreeCAD.Console.PrintError(obj.Label + ": " + t + "\n") |
| | return False |
| |
|
| | def _isInternalLinkgroup(self, obj): |
| | """Returns True if obj is an internal LinkGroup. Such a group is used to |
| | store hidden objects used for variant Links that should not be hosted.""" |
| |
|
| | |
| | |
| | if obj.TypeId != "App::LinkGroup": |
| | return False |
| | for inObj in obj.InList: |
| | if getattr(inObj, "LinkCopyOnChangeGroup", None) is obj: |
| | return True |
| | return False |
| |
|
| | def _objectInInternalLinkgroup(self, obj): |
| | """Returns True if obj is a hidden object in an internal LinkGroup.""" |
| |
|
| | for inObj in obj.InList: |
| | if self._isInternalLinkgroup(inObj): |
| | return True |
| | return False |
| |
|
| |
|
| | class AreaCalculator: |
| | """Helper class to compute vertical area, horizontal area, and perimeter length. |
| | |
| | This class encapsulates the logic for calculating the following properties: |
| | - **VerticalArea**: The total area of all vertical faces of the object. See the |
| | `isFaceVertical` method for the criteria used to determine vertical faces. |
| | - **HorizontalArea**: The area of the object's projection onto the XY plane. |
| | - **PerimeterLength**: The perimeter of the horizontal area. |
| | |
| | The class provides methods to validate the object's shape, identify vertical and |
| | horizontal faces, and compute the required properties. |
| | """ |
| |
|
| | def __init__(self, obj): |
| | self.obj = obj |
| |
|
| | def isShapeInvalid(self): |
| | """Check if the object's shape is invalid.""" |
| | return ( |
| | not self.obj.Shape |
| | or self.obj.Shape.isNull() |
| | or not self.obj.Shape.isValid() |
| | or not self.obj.Shape.Faces |
| | ) |
| |
|
| | def tooManyFaces(self): |
| | """Check if the object's shape has too many faces to process.""" |
| | return len(self.obj.Shape.Faces) > params.get_param_arch("MaxComputeAreas") |
| |
|
| | def resetAreas(self): |
| | """Reset the area properties of the object to zero. Generally called when |
| | there is an error. |
| | """ |
| | for prop in ["VerticalArea", "HorizontalArea", "PerimeterLength"]: |
| | setattr(self.obj, prop, 0) |
| |
|
| | def isFaceVertical(self, face, face_index=None): |
| | """Determine if a face is vertical. |
| | |
| | A face is considered vertical if: |
| | - Its normal vector forms an angle close to 90 degrees with the Z-axis. |
| | - The projected face has an area of zero. |
| | |
| | Parameters |
| | ---------- |
| | face : Part.Face |
| | The face object to be checked. |
| | face_index : str, optional |
| | The face's 1-based index identifier, used for debugging error messages. |
| | Defaults to None. |
| | |
| | Notes |
| | ----- |
| | The check whether the projected face has an area of zero means that roof-like |
| | (sloped) and domed faces alike will not be counted as vertical faces. |
| | Vertically-extruded curved edges (for instance from a slab) will be classified |
| | as vertical and be counted. This is an improvement over the fix for |
| | https://github.com/FreeCAD/FreeCAD/issues/14687. |
| | """ |
| | import Part |
| | import DraftGeomUtils |
| | import TechDraw |
| |
|
| | face_name = f" Face{face_index}" if face_index is not None else "" |
| |
|
| | if face.Surface.TypeId == "Part::GeomCylinder": |
| | angle = face.Surface.Axis.getAngle(FreeCAD.Vector(0, 0, 1)) |
| | return self.isZeroAngle(angle) |
| | elif face.Surface.TypeId == "Part::GeomSurfaceOfExtrusion": |
| | angle = face.Surface.Direction.getAngle(FreeCAD.Vector(0, 0, 1)) |
| | return self.isZeroAngle(angle) |
| | elif face.Surface.TypeId == "Part::GeomPlane": |
| | projectedArea = 0 |
| | elif face.findPlane() is not None: |
| | projectedArea = 0 |
| | else: |
| | try: |
| | edges = TechDraw.project(face, FreeCAD.Vector(0, 0, 1))[0].Edges |
| | wires = DraftGeomUtils.findWires(edges) |
| | if len(wires) == 1 and not wires[0].isClosed(): |
| | projectedArea = 0 |
| | else: |
| | projectedArea = Part.Face(wires).Area |
| | except Part.OCCError: |
| | FreeCAD.Console.PrintWarning( |
| | translate("Arch", f"Could not project face{face_name} from {self.obj.Label}\n") |
| | ) |
| | return False |
| |
|
| | try: |
| | angle = face.normalAt(0, 0).getAngle(FreeCAD.Vector(0, 0, 1)) |
| | return self.isRightAngle(angle) and projectedArea < 0.0001 |
| | except Part.OCCError: |
| | FreeCAD.Console.PrintWarning( |
| | translate( |
| | "Arch", |
| | f"Could not determine if face{face_name} from {self.obj.Label}" |
| | " is vertical: normalAt() failed\n", |
| | ) |
| | ) |
| | return False |
| |
|
| | def isRightAngle(self, angle): |
| | """Check if the angle is close to 90 degrees.""" |
| | return math.isclose(angle, math.pi / 2, abs_tol=0.0005) |
| |
|
| | def isZeroAngle(self, angle): |
| | """Check if the angle is close to 0 or 180 degrees.""" |
| | if math.isclose(angle, 0, abs_tol=0.0005): |
| | return True |
| | return math.isclose(angle, math.pi, abs_tol=0.0005) |
| |
|
| | def compute(self): |
| | """Compute the vertical area, horizontal area, and perimeter length. |
| | |
| | This method performs the following steps: |
| | 1. Identifies the object's vertical and horizontal faces. |
| | 2. Computes the total vertical area by adding areas of all vertical faces. |
| | 3. Projects horizontal faces onto the XY plane and computes their total horizontal area. |
| | 4. Computes the perimeter length of the horizontal area. |
| | |
| | The computed values are assigned to the object's properties: |
| | - VerticalArea |
| | - HorizontalArea |
| | - PerimeterLength |
| | """ |
| | if self.isShapeInvalid() or self.tooManyFaces(): |
| | self.resetAreas() |
| | return |
| |
|
| | verticalArea = 0 |
| | horizontalAreaFaces = [] |
| |
|
| | |
| | for i, face in enumerate(self.obj.Shape.Faces, start=1): |
| | if self.isFaceVertical(face, face_index=i): |
| | verticalArea += face.Area |
| | else: |
| | horizontalAreaFaces.append(face) |
| |
|
| | |
| | if hasattr(self.obj, "VerticalArea") and self.obj.VerticalArea.Value != verticalArea: |
| | self.obj.VerticalArea = verticalArea |
| |
|
| | |
| | if horizontalAreaFaces and hasattr(self.obj, "HorizontalArea"): |
| | self._computeHorizontalAreaAndPerimeter(horizontalAreaFaces) |
| |
|
| | def _computeHorizontalAreaAndPerimeter(self, horizontalAreaFaces): |
| | """Compute the horizontal area and perimeter length. |
| | |
| | Projects the given faces onto the XY plane, fuses them, and calculates: |
| | - The total horizontal area. |
| | - The perimeter length of the fused horizontal area. |
| | |
| | Parameters |
| | ---------- |
| | horizontalAreaFaces: list of Part.Face |
| | The faces to process. |
| | """ |
| |
|
| | import Part |
| | import TechDraw |
| | import DraftGeomUtils |
| |
|
| | |
| | |
| | param_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/debug") |
| | if "allowCrazyEdge" not in param_grp.GetBools(): |
| | old_allow_crazy_edge = None |
| | else: |
| | old_allow_crazy_edge = param_grp.GetBool("allowCrazyEdge") |
| | param_grp.SetBool("allowCrazyEdge", True) |
| |
|
| | direction = FreeCAD.Vector(0, 0, 1) |
| | projectedFaces = [] |
| | for face in horizontalAreaFaces: |
| | try: |
| | if face.findPlane() is None: |
| | if len(face.Wires) > 1: |
| | |
| | FreeCAD.Console.PrintWarning( |
| | translate( |
| | "Arch", |
| | f"Error computing areas for {self.obj.Label}: unable to project " |
| | "non-planar faces with holes. Area values will be reset to 0.\n", |
| | ) |
| | ) |
| | self.resetAreas() |
| | return |
| | wire = TechDraw.findShapeOutline(face, 1, direction) |
| | projectedFace = Part.makeFace([wire], "Part::FaceMakerSimple") |
| | else: |
| | edges = TechDraw.project(face, direction)[0].Edges |
| | wires = DraftGeomUtils.findWires(edges) |
| | |
| | projectedFace = Part.makeFace(wires, "Part::FaceMakerCheese") |
| | |
| | projectedFaces.append(projectedFace) |
| | except Part.OCCError: |
| | FreeCAD.Console.PrintWarning( |
| | translate( |
| | "Arch", |
| | f"Error computing areas for {self.obj.Label}: unable to project or " |
| | f"make face with normal {face.normalAt(0, 0)}. " |
| | "Area values will be reset to 0.\n", |
| | ) |
| | ) |
| | self.resetAreas() |
| | return |
| |
|
| | if old_allow_crazy_edge is None: |
| | param_grp.RemBool("allowCrazyEdge") |
| | else: |
| | param_grp.SetBool("allowCrazyEdge", old_allow_crazy_edge) |
| |
|
| | if projectedFaces: |
| | fusedFace = projectedFaces.pop() |
| | for face in projectedFaces: |
| | fusedFace = fusedFace.fuse(face) |
| | fusedFace = fusedFace.removeSplitter() |
| | |
| |
|
| | if self.obj.HorizontalArea.Value != fusedFace.Area: |
| | self.obj.HorizontalArea = fusedFace.Area |
| |
|
| | if hasattr(self.obj, "PerimeterLength") and len(fusedFace.Faces) == 1: |
| | perimeterLength = fusedFace.Faces[0].OuterWire.Length |
| | if self.obj.PerimeterLength.Value != perimeterLength: |
| | self.obj.PerimeterLength = perimeterLength |
| |
|
| |
|
| | class ViewProviderComponent: |
| | """A default View Provider for Component objects. |
| | |
| | Acts as a base for all other Arch view providers. It's properties and |
| | behaviours are common to all Arch view providers. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The view provider to turn into a component view provider. |
| | """ |
| |
|
| | def __init__(self, vobj): |
| | vobj.Proxy = self |
| | self.Object = vobj.Object |
| | self.setProperties(vobj) |
| |
|
| | def setProperties(self, vobj): |
| | """Give the component view provider its component view provider specific properties. |
| | |
| | You can learn more about properties here: |
| | https://wiki.freecad.org/property |
| | """ |
| |
|
| | if not "UseMaterialColor" in vobj.PropertiesList: |
| | vobj.addProperty( |
| | "App::PropertyBool", |
| | "UseMaterialColor", |
| | "Component", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Use the material color as this object's shape color, if available", |
| | ), |
| | locked=True, |
| | ) |
| | vobj.UseMaterialColor = params.get_param_arch("UseMaterialColor") |
| |
|
| | def updateData(self, obj, prop): |
| | """Method called when the host object has a property changed. |
| | |
| | If the object has a Material associated with it, match the view |
| | object's ShapeColor and Transparency to match the Material. |
| | |
| | If the object is now cloned, or is part of a compound, have the view |
| | object inherit the DiffuseColor. |
| | |
| | Parameters |
| | ---------- |
| | obj: <App::FeaturePython> |
| | The host object that has changed. |
| | prop: string |
| | The name of the property that has changed. |
| | """ |
| |
|
| | |
| | if prop == "Material": |
| | if obj.Material and getattr(obj.ViewObject, "UseMaterialColor", True): |
| | if hasattr(obj.Material, "Material"): |
| | if "DiffuseColor" in obj.Material.Material: |
| | c = tuple( |
| | [ |
| | float(f) |
| | for f in obj.Material.Material["DiffuseColor"] |
| | .strip("()") |
| | .strip("[]") |
| | .split(",") |
| | ] |
| | ) |
| | if obj.ViewObject.ShapeColor != c: |
| | obj.ViewObject.ShapeColor = c |
| | |
| | if obj.ViewObject.DiffuseColor != [c]: |
| | obj.ViewObject.DiffuseColor = [c] |
| | if "Transparency" in obj.Material.Material: |
| | t = int(obj.Material.Material["Transparency"]) |
| | if obj.ViewObject.Transparency != t: |
| | obj.ViewObject.Transparency = t |
| | elif prop == "Shape": |
| | if obj.Base: |
| | if obj.Base.isDerivedFrom("Part::Compound"): |
| | if obj.ViewObject.DiffuseColor != obj.Base.ViewObject.DiffuseColor: |
| | if len(obj.Base.ViewObject.DiffuseColor) > 1: |
| | obj.ViewObject.DiffuseColor = obj.Base.ViewObject.DiffuseColor |
| | obj.ViewObject.update() |
| | elif prop == "CloneOf": |
| | if obj.CloneOf: |
| | if (not getattr(obj, "Material", None)) and hasattr( |
| | obj.CloneOf.ViewObject, "DiffuseColor" |
| | ): |
| | if obj.ViewObject.DiffuseColor != obj.CloneOf.ViewObject.DiffuseColor: |
| | if len(obj.CloneOf.ViewObject.DiffuseColor) > 1: |
| | obj.ViewObject.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor |
| | obj.ViewObject.update() |
| | return |
| |
|
| | def getIcon(self): |
| | """Return the path to the appropriate icon. |
| | |
| | If a clone, return the cloned component icon path. Otherwise return the |
| | Arch Component icon. |
| | |
| | Returns |
| | ------- |
| | str |
| | Path to the appropriate icon .svg file. |
| | """ |
| |
|
| | import Arch_rc |
| |
|
| | if hasattr(self, "Object"): |
| | if hasattr(self.Object, "CloneOf"): |
| | if self.Object.CloneOf: |
| | return ":/icons/Arch_Component_Clone.svg" |
| | return ":/icons/Arch_Component_Tree.svg" |
| |
|
| | def onChanged(self, vobj, prop): |
| | """Method called when the view provider has a property changed. |
| | |
| | If DiffuseColor changes, change DiffuseColor to copy the host object's |
| | clone, if it exists. |
| | |
| | If ShapeColor changes, overwrite it with DiffuseColor. |
| | |
| | If Visibility changes, propagate the change to all view objects that |
| | are also hosted by this view object's host. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The component's view provider object. |
| | prop: string |
| | The name of the property that has changed. |
| | """ |
| |
|
| | obj = vobj.Object |
| | if prop == "DiffuseColor": |
| | if hasattr(obj, "CloneOf"): |
| | if obj.CloneOf and hasattr(obj.CloneOf, "DiffuseColor"): |
| | if len(obj.CloneOf.ViewObject.DiffuseColor) > 1: |
| | if vobj.DiffuseColor != obj.CloneOf.ViewObject.DiffuseColor: |
| | vobj.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor |
| | vobj.update() |
| | elif prop == "ShapeColor": |
| | |
| | if hasattr(vobj, "DiffuseColor"): |
| | if len(vobj.DiffuseColor) > 1: |
| | d = vobj.DiffuseColor |
| | vobj.DiffuseColor = d |
| | elif prop == "Visibility": |
| | |
| | if not [parent for parent in obj.InList if obj in getattr(parent, "Additions", [])]: |
| | hostedObjs = obj.Proxy.getHosts(obj) |
| | |
| | for addition in getattr(obj, "Additions", []): |
| | if hasattr(addition, "Proxy") and hasattr(addition.Proxy, "getHosts"): |
| | hostedObjs.extend(addition.Proxy.getHosts(addition)) |
| | for hostedObj in hostedObjs: |
| | if hasattr(hostedObj, "ViewObject"): |
| | hostedObj.ViewObject.Visibility = vobj.Visibility |
| | return |
| |
|
| | def attach(self, vobj): |
| | """Add display modes' data to the coin scenegraph. |
| | |
| | Add each display mode as a coin node, whose parent is this view |
| | provider. |
| | |
| | Each display mode's node includes the data needed to display the object |
| | in that mode. This might include colors of faces, or the draw style of |
| | lines. This data is stored as additional coin nodes which are children |
| | of the display mode node. |
| | |
| | Add the HiRes display mode. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The component's view provider object. |
| | """ |
| |
|
| | from pivy import coin |
| |
|
| | self.Object = vobj.Object |
| | self.hiresgroup = coin.SoSeparator() |
| | self.meshcolor = coin.SoBaseColor() |
| | self.hiresgroup.addChild(self.meshcolor) |
| | self.hiresgroup.setName("HiRes") |
| | vobj.addDisplayMode(self.hiresgroup, "HiRes") |
| | return |
| |
|
| | def getDisplayModes(self, vobj): |
| | """Define the display modes unique to the Arch Component. |
| | |
| | Define mode HiRes, which displays the component as a mesh, intended as |
| | a more visually appealing version of the component. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The component's view provider object. |
| | |
| | Returns |
| | ------- |
| | list of str |
| | List containing the names of the new display modes. |
| | """ |
| |
|
| | modes = ["HiRes"] |
| | return modes |
| |
|
| | def setDisplayMode(self, mode): |
| | """Method called when the display mode changes. |
| | |
| | Called when the display mode changes, this method can be used to set |
| | data that wasn't available when .attach() was called. |
| | |
| | When HiRes is set as display mode, display the component as a copy of |
| | the mesh associated as the HiRes property of the host object. See |
| | ArchComponent.Component's properties. |
| | |
| | If no shape is set in the HiRes property, just display the object as |
| | the Flat Lines display mode. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The component's view provider object. |
| | mode: str |
| | The name of the display mode the view provider has switched to. |
| | |
| | Returns |
| | ------- |
| | str: |
| | The name of the display mode the view provider has switched to. |
| | """ |
| |
|
| | if hasattr(self, "meshnode"): |
| | if self.meshnode: |
| | self.hiresgroup.removeChild(self.meshnode) |
| | del self.meshnode |
| | if mode == "HiRes": |
| | from pivy import coin |
| |
|
| | m = None |
| | if hasattr(self, "Object"): |
| | if hasattr(self.Object, "HiRes"): |
| | if self.Object.HiRes: |
| | |
| | self.Object.HiRes.ViewObject.show() |
| | self.Object.HiRes.ViewObject.hide() |
| | m = self.Object.HiRes.ViewObject.RootNode |
| | if not m: |
| | if hasattr(self.Object, "CloneOf"): |
| | if self.Object.CloneOf: |
| | if hasattr(self.Object.CloneOf, "HiRes"): |
| | if self.Object.CloneOf.HiRes: |
| | |
| | self.Object.CloneOf.HiRes.ViewObject.show() |
| | self.Object.CloneOf.HiRes.ViewObject.hide() |
| | m = self.Object.CloneOf.HiRes.ViewObject.RootNode |
| | if m: |
| | self.meshnode = m.copy() |
| | for c in self.meshnode.getChildren(): |
| | |
| | if isinstance(c, coin.SoSwitch): |
| | num = 0 |
| | if c.getNumChildren() > 0: |
| | if c.getChild(0).getName() == "HiRes": |
| | num = 1 |
| | |
| | c.whichChild = num |
| | break |
| | self.hiresgroup.addChild(self.meshnode) |
| | else: |
| | return "Flat Lines" |
| | return mode |
| |
|
| | def dumps(self): |
| |
|
| | return None |
| |
|
| | def loads(self, state): |
| |
|
| | return None |
| |
|
| | def claimChildren(self): |
| | """Define which objects will appear as children in the tree view. |
| | |
| | Set the host object's Base object as a child, and set any additions or |
| | subtractions as children. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The component's view provider object. |
| | |
| | Returns |
| | ------- |
| | list of <App::DocumentObject>s: |
| | The objects claimed as children. |
| | """ |
| |
|
| | if hasattr(self, "Object"): |
| | c = [] |
| | if hasattr(self.Object, "Base"): |
| | if not ( |
| | Draft.getType(self.Object) == "Wall" |
| | and Draft.getType(self.Object.Base) == "Space" |
| | ): |
| | c = [self.Object.Base] |
| | if hasattr(self.Object, "Additions"): |
| | c.extend(self.Object.Additions) |
| | if hasattr(self.Object, "Subtractions"): |
| | for s in self.Object.Subtractions: |
| | if Draft.getType(self.Object) == "Wall": |
| | if Draft.getType(s) == "Roof": |
| | continue |
| | c.append(s) |
| |
|
| | for link in ["Armatures", "Group"]: |
| | if hasattr(self.Object, link): |
| | objlink = getattr(self.Object, link) |
| | c.extend(objlink) |
| | for link in ["Tool", "Subvolume", "Mesh", "HiRes"]: |
| | if hasattr(self.Object, link): |
| | objlink = getattr(self.Object, link) |
| | if objlink: |
| | c.append(objlink) |
| | if params.get_param_arch("ClaimHosted"): |
| | for link in self.Object.Proxy.getHosts(self.Object): |
| | c.append(link) |
| |
|
| | return c |
| | return [] |
| |
|
| | def setEdit(self, vobj, mode): |
| | if mode != 0: |
| | return None |
| |
|
| | taskd = ComponentTaskPanel() |
| | taskd.obj = self.Object |
| | taskd.update() |
| | FreeCADGui.Control.showDialog(taskd) |
| | return True |
| |
|
| | def unsetEdit(self, vobj, mode): |
| | if mode != 0: |
| | return None |
| |
|
| | FreeCADGui.Control.closeDialog() |
| | return True |
| |
|
| | def setupContextMenu(self, vobj, menu): |
| | """Add the component specific options to the context menu. |
| | |
| | The context menu is the drop down menu that opens when the user right |
| | clicks on the component in the tree view. |
| | |
| | Parameters |
| | ---------- |
| | vobj: <Gui.ViewProviderDocumentObject> |
| | The component's view provider object. |
| | menu: <PySide2.QtWidgets.QMenu> |
| | The context menu already assembled prior to this method being |
| | called. |
| | """ |
| | if FreeCADGui.activeWorkbench().name() != "BIMWorkbench": |
| | return |
| | self.contextMenuAddEdit(menu) |
| | self.contextMenuAddToggleSubcomponents(menu) |
| |
|
| | def contextMenuAddEdit(self, menu): |
| | actionEdit = QtGui.QAction(translate("Arch", "Edit"), menu) |
| | QtCore.QObject.connect(actionEdit, QtCore.SIGNAL("triggered()"), self.edit) |
| | menu.addAction(actionEdit) |
| |
|
| | def contextMenuAddToggleSubcomponents(self, menu): |
| | actionToggleSubcomponents = QtGui.QAction( |
| | QtGui.QIcon(":/icons/Arch_ToggleSubs.svg"), |
| | translate("Arch", "Toggle Subcomponents"), |
| | menu, |
| | ) |
| | QtCore.QObject.connect( |
| | actionToggleSubcomponents, QtCore.SIGNAL("triggered()"), self.toggleSubcomponents |
| | ) |
| | menu.addAction(actionToggleSubcomponents) |
| |
|
| | def edit(self): |
| | FreeCADGui.ActiveDocument.setEdit(self.Object, 0) |
| |
|
| | def toggleSubcomponents(self): |
| | FreeCADGui.runCommand("Arch_ToggleSubs") |
| |
|
| | def areDifferentColors(self, a, b): |
| | """Check if two diffuse colors are almost the same. |
| | |
| | Parameters |
| | ---------- |
| | a: tuple |
| | The first DiffuseColor value to compare. |
| | a: tuple |
| | The second DiffuseColor value to compare. |
| | |
| | Returns |
| | ------- |
| | bool: |
| | True if colors are different, false if they are similar. |
| | """ |
| |
|
| | if len(a) != len(b): |
| | return True |
| | for i in range(len(a)): |
| | if abs(sum(a[i]) - sum(b[i])) > 0.00001: |
| | return True |
| | return False |
| |
|
| | def colorize(self, obj, force=False): |
| | """If an object is a clone, set it to copy the color of its parent. |
| | |
| | Only change the color of the clone if the clone and its parent have |
| | colors that are distinguishably different from each other. |
| | |
| | Parameters |
| | ---------- |
| | obj: <Part::Feature> |
| | The object to change the color of. |
| | force: bool |
| | If true, forces the colourisation even if the two objects have very |
| | similar colors. |
| | """ |
| |
|
| | if obj.CloneOf: |
| | if ( |
| | self.areDifferentColors( |
| | obj.ViewObject.DiffuseColor, obj.CloneOf.ViewObject.DiffuseColor |
| | ) |
| | or force |
| | ): |
| |
|
| | obj.ViewObject.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor |
| |
|
| |
|
| | class ArchSelectionObserver: |
| | """Selection observer used throughout the Arch module. |
| | |
| | When a nextCommand is specified, the observer fires a Gui command when |
| | anything is selected. |
| | |
| | When a watched object is specified, the observer will only fire when this |
| | watched object is selected. |
| | |
| | TODO: This could probably use a rework. Most of the functionality isn't |
| | used. It does not work correctly to reset the appearance of parent object |
| | in ComponentTaskPanel.editObject(), for example. |
| | |
| | Parameters |
| | ---------- |
| | watched: <App::DocumentObject>, optional |
| | If no watched value is provided, functionality relating to origin |
| | and hide parameters will not occur. Only the nextCommand will fire. |
| | |
| | When a watched value is provided, the selection observer will only |
| | fire when the watched object has been selected. |
| | hide: bool |
| | Sets if the watched object should be hidden. |
| | origin: <App::DocumentObject, optional |
| | If provided, and hide is True, will make the origin object |
| | selectable, and opaque (set transparency to 0). |
| | nextCommand: str |
| | Name of Gui command to run when the watched object is selected, (if |
| | one is specified), or when anything is selected (if no watched |
| | object is specified). |
| | """ |
| |
|
| | def __init__(self, origin=None, watched=None, hide=True, nextCommand=None): |
| | self.origin = origin |
| | self.watched = watched |
| | self.hide = hide |
| | self.nextCommand = nextCommand |
| |
|
| | def addSelection(self, document, object, element, position): |
| | """Method called when a selection is made on the Gui. |
| | |
| | When a nextCommand is specified, fire a Gui command when anything is |
| | selected. |
| | |
| | When a watched object is specified, only fire when this watched object |
| | is selected. |
| | |
| | Parameters |
| | ---------- |
| | document: str |
| | The document's Name. |
| | object: str |
| | The selected object's Name. |
| | element: str |
| | The element on the object that was selected, such as an edge or |
| | face. |
| | position: |
| | The location in XYZ space the selection was made. |
| | """ |
| |
|
| | if not self.watched: |
| | FreeCADGui.Selection.removeObserver(FreeCAD.ArchObserver) |
| | if self.nextCommand: |
| | FreeCADGui.runCommand(self.nextCommand) |
| | del FreeCAD.ArchObserver |
| | elif object == self.watched.Name: |
| | if not element: |
| | FreeCAD.Console.PrintMessage(translate("Arch", "Closing Sketch edit")) |
| | if self.hide: |
| | if self.origin: |
| | self.origin.ViewObject.Transparency = 0 |
| | self.origin.ViewObject.Selectable = True |
| | self.watched.ViewObject.hide() |
| | FreeCADGui.activateWorkbench("BIMWorkbench") |
| | if hasattr(FreeCAD, "ArchObserver"): |
| | FreeCADGui.Selection.removeObserver(FreeCAD.ArchObserver) |
| | del FreeCAD.ArchObserver |
| | if self.nextCommand: |
| | FreeCADGui.Selection.clearSelection() |
| | FreeCADGui.Selection.addSelection(self.watched) |
| | FreeCADGui.runCommand(self.nextCommand) |
| |
|
| |
|
| | class SelectionTaskPanel: |
| | """A simple TaskPanel to wait for a selection. |
| | |
| | Typically used in conjunction with ArchComponent.ArchSelectionObserver. |
| | """ |
| |
|
| | def __init__(self): |
| | self.baseform = QtGui.QLabel() |
| | self.baseform.setText(QtGui.QApplication.translate("Arch", "Select a base object", None)) |
| |
|
| | def getStandardButtons(self): |
| | """Adds the cancel button.""" |
| | return QtGui.QDialogButtonBox.Cancel |
| |
|
| | def reject(self): |
| | """The method run when the user selects the cancel button.""" |
| |
|
| | if hasattr(FreeCAD, "ArchObserver"): |
| | FreeCADGui.Selection.removeObserver(FreeCAD.ArchObserver) |
| | del FreeCAD.ArchObserver |
| | return True |
| |
|
| |
|
| | class ComponentTaskPanel: |
| | """The default TaskPanel for all Arch components. |
| | |
| | TODO: outline the purpose of this taskpanel. |
| | """ |
| |
|
| | def __init__(self): |
| | |
| | |
| | |
| |
|
| | self.obj = None |
| | self.attribs = [ |
| | "Base", |
| | "Additions", |
| | "Subtractions", |
| | "Objects", |
| | "Components", |
| | "Axes", |
| | "Fixtures", |
| | "Group", |
| | "Hosts", |
| | ] |
| | self.baseform = QtGui.QWidget() |
| | self.baseform.setObjectName("TaskPanel") |
| | self.grid = QtGui.QGridLayout(self.baseform) |
| | self.grid.setObjectName("grid") |
| | self.title = QtGui.QLabel(self.baseform) |
| | self.grid.addWidget(self.title, 0, 0, 1, 2) |
| | self.form = self.baseform |
| |
|
| | |
| | self.tree = QtGui.QTreeWidget(self.baseform) |
| | self.grid.addWidget(self.tree, 1, 0, 1, 2) |
| | self.tree.setColumnCount(1) |
| | self.tree.header().hide() |
| |
|
| | |
| | self.addButton = QtGui.QPushButton(self.baseform) |
| | self.addButton.setObjectName("addButton") |
| | self.addButton.setIcon(QtGui.QIcon(":/icons/Arch_Add.svg")) |
| | self.grid.addWidget(self.addButton, 3, 0, 1, 1) |
| | self.addButton.setEnabled(False) |
| |
|
| | self.delButton = QtGui.QPushButton(self.baseform) |
| | self.delButton.setObjectName("delButton") |
| | self.delButton.setIcon(QtGui.QIcon(":/icons/Arch_Remove.svg")) |
| | self.grid.addWidget(self.delButton, 3, 1, 1, 1) |
| | self.delButton.setEnabled(False) |
| |
|
| | self.ifcButton = QtGui.QPushButton(self.baseform) |
| | self.ifcButton.setObjectName("ifcButton") |
| | self.ifcButton.setIcon(QtGui.QIcon(":/icons/IFC.svg")) |
| | self.grid.addWidget(self.ifcButton, 4, 0, 1, 2) |
| | self.ifcButton.hide() |
| |
|
| | self.classButton = QtGui.QPushButton(self.baseform) |
| | self.classButton.setObjectName("classButton") |
| | self.grid.addWidget(self.classButton, 5, 0, 1, 2) |
| | try: |
| | import BimClassification |
| | except Exception: |
| | self.classButton.hide() |
| | else: |
| | import os |
| |
|
| | |
| | if not "BIM_Classification" in FreeCADGui.listCommands(): |
| | FreeCADGui.activateWorkbench("BIMWorkbench") |
| | self.classButton.setIcon( |
| | QtGui.QIcon( |
| | os.path.join( |
| | os.path.dirname(BimClassification.__file__), |
| | "icons", |
| | "BIM_Classification.svg", |
| | ) |
| | ) |
| | ) |
| |
|
| | QtCore.QObject.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.addElement) |
| | QtCore.QObject.connect(self.delButton, QtCore.SIGNAL("clicked()"), self.removeElement) |
| | QtCore.QObject.connect(self.ifcButton, QtCore.SIGNAL("clicked()"), self.editIfcProperties) |
| | QtCore.QObject.connect(self.classButton, QtCore.SIGNAL("clicked()"), self.editClass) |
| | QtCore.QObject.connect( |
| | self.tree, QtCore.SIGNAL("itemClicked(QTreeWidgetItem*,int)"), self.check |
| | ) |
| | QtCore.QObject.connect( |
| | self.tree, QtCore.SIGNAL("itemDoubleClicked(QTreeWidgetItem *,int)"), self.editObject |
| | ) |
| | self.update() |
| |
|
| | def isAllowedAlterSelection(self): |
| | """Indicate whether this task dialog allows other commands to modify |
| | the selection while it is open. |
| | |
| | Returns |
| | ------- |
| | bool |
| | If alteration of the selection should be allowed. |
| | """ |
| |
|
| | return True |
| |
|
| | def isAllowedAlterView(self): |
| | """Indicate whether this task dialog allows other commands to modify |
| | the 3D view while it is open. |
| | |
| | Returns |
| | ------- |
| | bool |
| | If alteration of the 3D view should be allowed. |
| | """ |
| |
|
| | return True |
| |
|
| | def getStandardButtons(self): |
| | """Add the standard ok button.""" |
| |
|
| | return QtGui.QDialogButtonBox.Ok |
| |
|
| | def check(self, wid, col): |
| | """This method is run as the callback when the user selects an item in the tree. |
| | |
| | Enable and disable the add and remove buttons depending on what the |
| | user has selected. |
| | |
| | If they have selected one of the root attribute folders, disable the |
| | remove button. If they have separately selected an object in the 3D |
| | view, enable the add button, allowing the user to add that object to |
| | the root attribute folder. |
| | |
| | If they have selected one of the items inside a root attribute folder, |
| | enable the remove button, allowing the user to remove the object from |
| | that attribute. |
| | |
| | Parameters |
| | ---------- |
| | wid: <PySide2.QtWidgets.QTreeWidgetItem> |
| | Qt object the user has selected in the tree widget. |
| | """ |
| |
|
| | if not wid.parent(): |
| | self.delButton.setEnabled(False) |
| | if self.obj: |
| | sel = FreeCADGui.Selection.getSelection() |
| | if sel: |
| | if not (self.obj in sel): |
| | self.addButton.setEnabled(True) |
| | else: |
| | self.delButton.setEnabled(True) |
| | self.addButton.setEnabled(False) |
| |
|
| | def getIcon(self, obj): |
| | """Get the path to the icons, of the items that fill the tree widget. |
| | |
| | Parameters |
| | --------- |
| | obj: <App::DocumentObject> |
| | The object being edited. |
| | """ |
| |
|
| | if hasattr(obj.ViewObject, "Proxy"): |
| | if hasattr(obj.ViewObject.Proxy, "getIcon"): |
| | return QtGui.QIcon(obj.ViewObject.Proxy.getIcon()) |
| | elif obj.isDerivedFrom("Sketcher::SketchObject"): |
| | return QtGui.QIcon(":/icons/Sketcher_Sketch.svg") |
| | elif obj.isDerivedFrom("App::DocumentObjectGroup"): |
| | return QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_DirIcon) |
| | elif hasattr(obj.ViewObject, "Icon"): |
| | return QtGui.QIcon(obj.ViewObject.Icon) |
| | return QtGui.QIcon(":/icons/Part_3D_object.svg") |
| |
|
| | def update(self): |
| | """Populate the treewidget with its various items. |
| | |
| | Check if the object being edited has attributes relevant to subobjects. |
| | IE: Additions, Subtractions, etc. |
| | |
| | Populate the tree with these subobjects, under folders named after the |
| | attributes they are listed in. |
| | |
| | Finally, run method .retranslateUi(). |
| | """ |
| |
|
| | self.tree.clear() |
| | dirIcon = QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_DirIcon) |
| | for a in self.attribs: |
| | setattr(self, "tree" + a, QtGui.QTreeWidgetItem(self.tree)) |
| | c = getattr(self, "tree" + a) |
| | c.setIcon(0, dirIcon) |
| | c.ChildIndicatorPolicy = 2 |
| | if self.obj: |
| | if not hasattr(self.obj, a): |
| | c.setHidden(True) |
| | else: |
| | c.setHidden(True) |
| | if self.obj: |
| | for attrib in self.attribs: |
| | if hasattr(self.obj, attrib): |
| | Oattrib = getattr(self.obj, attrib) |
| | Tattrib = getattr(self, "tree" + attrib) |
| | if Oattrib: |
| | if attrib == "Base": |
| | Oattrib = [Oattrib] |
| | for o in Oattrib: |
| | item = QtGui.QTreeWidgetItem() |
| | item.setText(0, o.Label) |
| | item.setToolTip(0, o.Name) |
| | item.setIcon(0, self.getIcon(o)) |
| | Tattrib.addChild(item) |
| | self.tree.expandItem(Tattrib) |
| | if hasattr(self.obj, "IfcProperties"): |
| | if isinstance(self.obj.IfcProperties, dict): |
| | self.ifcButton.show() |
| | self.retranslateUi(self.baseform) |
| |
|
| | def addElement(self): |
| | """This method is run as a callback when the user selects the add button. |
| | |
| | Get the object selected in the 3D view, and get the attribute folder |
| | selected in the tree widget. |
| | |
| | Add the object selected in the 3D view to the attribute associated with |
| | the selected folder, by using function addToComponent(). |
| | """ |
| |
|
| | it = self.tree.currentItem() |
| | if it: |
| | mod = None |
| | for a in self.attribs: |
| | if it.text(0) == getattr(self, "tree" + a).text(0): |
| | mod = a |
| | for o in FreeCADGui.Selection.getSelection(): |
| | addToComponent(self.obj, o, mod) |
| | self.update() |
| |
|
| | def removeElement(self): |
| | """ |
| | This method is run as a callback when the user selects the remove button. |
| | It calls a handler on the object's proxy to perform the removal. |
| | """ |
| | element_selected = self.tree.currentItem() |
| | if not element_selected: |
| | return |
| |
|
| | element_to_remove = FreeCAD.ActiveDocument.getObject(str(element_selected.toolTip(0))) |
| |
|
| | |
| | |
| | if hasattr(self.obj.Proxy, "handleComponentRemoval"): |
| | self.obj.Proxy.handleComponentRemoval(self.obj, element_to_remove) |
| | else: |
| | |
| | removeFromComponent(self.obj, element_to_remove) |
| |
|
| | self.update() |
| |
|
| | def accept(self): |
| | """This method runs as a callback when the user selects the ok button. |
| | |
| | Recomputes the document, and leave edit mode. |
| | """ |
| |
|
| | FreeCAD.ActiveDocument.recompute() |
| | FreeCADGui.ActiveDocument.resetEdit() |
| | return True |
| |
|
| | def editObject(self, wid, col): |
| | """This method is run when the user double clicks on an item in the tree widget. |
| | |
| | If the item in the tree has a corresponding object in the document, |
| | enter edit mode for that associated object. |
| | |
| | At the same time, make the object this task panel was opened for |
| | transparent and unselectable. |
| | |
| | Parameters |
| | ---------- |
| | wid: <PySide2.QtWidgets.QTreeWidgetItem> |
| | Qt object the user has selected in the tree widget. |
| | """ |
| |
|
| | if wid.parent(): |
| | obj = FreeCAD.ActiveDocument.getObject(str(wid.toolTip(0))) |
| | if obj: |
| | self.obj.ViewObject.Transparency = 80 |
| | self.obj.ViewObject.Selectable = False |
| | obj.ViewObject.show() |
| | self.accept() |
| | if obj.isDerivedFrom("Sketcher::SketchObject"): |
| | FreeCADGui.activateWorkbench("SketcherWorkbench") |
| | FreeCAD.ArchObserver = ArchSelectionObserver(self.obj, obj) |
| | FreeCADGui.Selection.addObserver(FreeCAD.ArchObserver) |
| | FreeCADGui.ActiveDocument.setEdit(obj.Name, 0) |
| |
|
| | def retranslateUi(self, TaskPanel): |
| | """Add the text of the task panel, in translated form.""" |
| |
|
| | self.baseform.setWindowTitle(QtGui.QApplication.translate("Arch", "Component", None)) |
| | self.delButton.setText(QtGui.QApplication.translate("Arch", "Remove", None)) |
| | self.addButton.setText(QtGui.QApplication.translate("Arch", "Add", None)) |
| | self.title.setText(QtGui.QApplication.translate("Arch", "Components of This Object", None)) |
| | self.treeBase.setText(0, QtGui.QApplication.translate("Arch", "Base component", None)) |
| | self.treeAdditions.setText(0, QtGui.QApplication.translate("Arch", "Additions", None)) |
| | self.treeSubtractions.setText(0, QtGui.QApplication.translate("Arch", "Subtractions", None)) |
| | self.treeObjects.setText(0, QtGui.QApplication.translate("Arch", "Objects", None)) |
| | self.treeAxes.setText(0, QtGui.QApplication.translate("Arch", "Axes", None)) |
| | self.treeComponents.setText(0, QtGui.QApplication.translate("Arch", "Components", None)) |
| | self.treeFixtures.setText(0, QtGui.QApplication.translate("Arch", "Fixtures", None)) |
| | self.treeGroup.setText(0, QtGui.QApplication.translate("Arch", "Group", None)) |
| | self.treeHosts.setText(0, QtGui.QApplication.translate("Arch", "Hosts", None)) |
| | self.ifcButton.setText(QtGui.QApplication.translate("Arch", "Edit IFC Properties", None)) |
| | self.classButton.setText(QtGui.QApplication.translate("Arch", "Edit Standard Code", None)) |
| |
|
| | def editIfcProperties(self): |
| | """Open the IFC editor dialog box. |
| | |
| | This is the method that runs as a callback when the Edit IFC properties |
| | button is selected by the user. |
| | |
| | Defines the editor's structure, fill it with data, add a callback for |
| | the buttons and other interactions, and show it. |
| | """ |
| |
|
| | if hasattr(self, "ifcEditor"): |
| | if self.ifcEditor: |
| | self.ifcEditor.hide() |
| | del self.ifcEditor |
| | if not self.obj: |
| | return |
| | if not hasattr(self.obj, "IfcProperties"): |
| | return |
| | if not isinstance(self.obj.IfcProperties, dict): |
| | return |
| | import csv |
| | import os |
| | import Arch_rc |
| | import ArchIFCSchema |
| |
|
| | |
| | self.ptypes = list(ArchIFCSchema.IfcTypes) |
| | self.plabels = [ |
| | "".join(map(lambda x: x if x.islower() else " " + x, t[3:]))[1:] for t in self.ptypes |
| | ] |
| | self.psetdefs = {} |
| | psetspath = os.path.join( |
| | FreeCAD.getResourceDir(), "Mod", "Arch", "Presets", "pset_definitions.csv" |
| | ) |
| | if os.path.exists(psetspath): |
| | with open(psetspath, "r") as csvfile: |
| | reader = csv.reader(csvfile, delimiter=";") |
| | for row in reader: |
| | self.psetdefs[row[0]] = row[1:] |
| | self.psetkeys = [ |
| | "".join(map(lambda x: x if x.islower() else " " + x, t[5:]))[1:] |
| | for t in self.psetdefs.keys() |
| | ] |
| | self.psetkeys.sort() |
| | self.ifcEditor = FreeCADGui.PySideUic.loadUi(":/ui/dialogIfcPropertiesRedux.ui") |
| |
|
| | |
| | mw = FreeCADGui.getMainWindow() |
| | self.ifcEditor.move( |
| | mw.frameGeometry().topLeft() + mw.rect().center() - self.ifcEditor.rect().center() |
| | ) |
| | self.ifcModel = QtGui.QStandardItemModel() |
| | self.ifcEditor.treeProperties.setModel(self.ifcModel) |
| | |
| | self.ifcEditor.treeProperties.setUniformRowHeights(True) |
| | self.ifcEditor.treeProperties.setItemDelegate( |
| | IfcEditorDelegate(dialog=self, ptypes=self.ptypes, plabels=self.plabels) |
| | ) |
| | self.ifcModel.setHorizontalHeaderLabels( |
| | [ |
| | QtGui.QApplication.translate("Arch", "Property", None), |
| | QtGui.QApplication.translate("Arch", "Type", None), |
| | QtGui.QApplication.translate("Arch", "Value", None), |
| | ] |
| | ) |
| |
|
| | |
| | self.ifcEditor.comboProperty.addItems( |
| | [QtGui.QApplication.translate("Arch", "Add property", None)] + self.plabels |
| | ) |
| | self.ifcEditor.comboPset.addItems( |
| | [ |
| | QtGui.QApplication.translate("Arch", "Add property set", None), |
| | QtGui.QApplication.translate("Arch", "New...", None), |
| | ] |
| | + self.psetkeys |
| | ) |
| |
|
| | |
| | if "IfcUID" in self.obj.IfcData: |
| | self.ifcEditor.labelUUID.setText(self.obj.IfcData["IfcUID"]) |
| |
|
| | |
| | psets = {} |
| | for pname, value in self.obj.IfcProperties.items(): |
| | |
| | value = value.split(";;") |
| | if len(value) == 3: |
| | pset = value[0] |
| | ptype = value[1] |
| | pvalue = value[2] |
| | elif len(value) == 2: |
| | pset = "Default property set" |
| | ptype = value[0] |
| | pvalue = value[1] |
| | else: |
| | continue |
| | plabel = ptype |
| | if ptype in self.ptypes: |
| | plabel = self.plabels[self.ptypes.index(ptype)] |
| | psets.setdefault(pset, []).append([pname, plabel, pvalue]) |
| | for pset, plists in psets.items(): |
| | top = QtGui.QStandardItem(pset) |
| | top.setDragEnabled(False) |
| | top.setToolTip("PropertySet") |
| | self.ifcModel.appendRow([top, QtGui.QStandardItem(), QtGui.QStandardItem()]) |
| | for plist in plists: |
| | it1 = QtGui.QStandardItem(plist[0]) |
| | it1.setDropEnabled(False) |
| | it2 = QtGui.QStandardItem(plist[1]) |
| | it2.setDropEnabled(False) |
| | it3 = QtGui.QStandardItem(plist[2]) |
| | it3.setDropEnabled(False) |
| | top.appendRow([it1, it2, it3]) |
| | top.sortChildren(0) |
| |
|
| | |
| | idx = self.ifcModel.invisibleRootItem().index() |
| | for i in range(self.ifcModel.rowCount()): |
| | if self.ifcModel.item(i, 0).hasChildren(): |
| | self.ifcEditor.treeProperties.setFirstColumnSpanned(i, idx, True) |
| | self.ifcEditor.treeProperties.expandAll() |
| |
|
| | |
| | QtCore.QObject.connect( |
| | self.ifcEditor.buttonBox, QtCore.SIGNAL("accepted()"), self.acceptIfcProperties |
| | ) |
| | QtCore.QObject.connect( |
| | self.ifcEditor.comboProperty, |
| | QtCore.SIGNAL("currentIndexChanged(int)"), |
| | self.addIfcProperty, |
| | ) |
| | QtCore.QObject.connect( |
| | self.ifcEditor.comboPset, QtCore.SIGNAL("currentIndexChanged(int)"), self.addIfcPset |
| | ) |
| | QtCore.QObject.connect( |
| | self.ifcEditor.buttonDelete, QtCore.SIGNAL("clicked()"), self.removeIfcProperty |
| | ) |
| | self.ifcEditor.treeProperties.setSortingEnabled(True) |
| |
|
| | |
| | if "FlagForceBrep" in self.obj.IfcData: |
| | self.ifcEditor.checkBrep.setChecked(self.obj.IfcData["FlagForceBrep"] == "True") |
| | if "FlagParametric" in self.obj.IfcData: |
| | self.ifcEditor.checkParametric.setChecked(self.obj.IfcData["FlagParametric"] == "True") |
| | self.ifcEditor.show() |
| |
|
| | def acceptIfcProperties(self): |
| | """This method runs as a callback when the user selects the ok button in the IFC editor. |
| | |
| | Scrape through the rows of the IFC editor's items, and compare them to |
| | the object being edited's .IfcData. If the two are different, change |
| | the object's .IfcData to match the editor's items. |
| | """ |
| |
|
| | if hasattr(self, "ifcEditor") and self.ifcEditor: |
| | self.ifcEditor.hide() |
| | ifcdict = {} |
| | for row in range(self.ifcModel.rowCount()): |
| | pset = self.ifcModel.item(row, 0).text() |
| | if self.ifcModel.item(row, 0).hasChildren(): |
| | for childrow in range(self.ifcModel.item(row, 0).rowCount()): |
| | prop = self.ifcModel.item(row, 0).child(childrow, 0).text() |
| | ptype = self.ifcModel.item(row, 0).child(childrow, 1).text() |
| | if not ptype.startswith("Ifc"): |
| | ptype = self.ptypes[self.plabels.index(ptype)] |
| | pvalue = self.ifcModel.item(row, 0).child(childrow, 2).text() |
| | ifcdict[prop] = pset + ";;" + ptype + ";;" + pvalue |
| | ifcData = self.obj.IfcData |
| | ifcData["IfcUID"] = self.ifcEditor.labelUUID.text() |
| | ifcData["FlagForceBrep"] = str(self.ifcEditor.checkBrep.isChecked()) |
| | ifcData["FlagParametric"] = str(self.ifcEditor.checkParametric.isChecked()) |
| | if (ifcdict != self.obj.IfcProperties) or (ifcData != self.obj.IfcData): |
| | FreeCAD.ActiveDocument.openTransaction("Change Ifc Properties") |
| | if ifcdict != self.obj.IfcProperties: |
| | self.obj.IfcProperties = ifcdict |
| | if ifcData != self.obj.IfcData: |
| | self.obj.IfcData = ifcData |
| | FreeCAD.ActiveDocument.commitTransaction() |
| | del self.ifcEditor |
| |
|
| | def addIfcProperty(self, idx=0, pset=None, prop=None, ptype=None): |
| | """Add an IFC property to the object, within the IFC editor. |
| | |
| | This method runs as a callback when the user selects an option in the |
| | Add Property dropdown box in the IFC editor. When used via the |
| | dropdown, it adds a property with the property type selected in the |
| | dropdown. |
| | |
| | This method can also be run standalone, outside its function as a |
| | callback. |
| | |
| | Unless otherwise specified, the property will be called "New property". |
| | The property will have no value assigned. |
| | |
| | Parameters |
| | ---------- |
| | idx: int, optional |
| | The index number of the property type selected in the dropdown. If |
| | idx is not specified, the property's type must be specified in the |
| | ptype parameter. |
| | pset: str, optional |
| | The name of the property set this property will be assigned to. If |
| | not provided, the pset will be determined by which property set has |
| | been selected within the IFC editor widget. |
| | prop: str, optional |
| | The name of the property to be created. If left blank, will be set to |
| | "New Property". |
| | ptype: str, optional |
| | The name of the property type the new property will be set as. If |
| | not specified, the property's type will be determined using the |
| | idx parameter. |
| | """ |
| |
|
| | if hasattr(self, "ifcEditor") and self.ifcEditor: |
| | if not pset: |
| | sel = self.ifcEditor.treeProperties.selectedIndexes() |
| | if sel: |
| | item = self.ifcModel.itemFromIndex(sel[0]) |
| | if item.toolTip() == "PropertySet": |
| | pset = item |
| | if pset: |
| | if not prop: |
| | prop = QtGui.QApplication.translate("Arch", "New property", None) |
| | if not ptype: |
| | if idx > 0: |
| | ptype = self.plabels[idx - 1] |
| | if prop and ptype: |
| | if ptype in self.ptypes: |
| | ptype = self.plabels[self.ptypes.index(ptype)] |
| | it1 = QtGui.QStandardItem(prop) |
| | it1.setDropEnabled(False) |
| | it2 = QtGui.QStandardItem(ptype) |
| | it2.setDropEnabled(False) |
| | it3 = QtGui.QStandardItem() |
| | it3.setDropEnabled(False) |
| | pset.appendRow([it1, it2, it3]) |
| | if idx != 0: |
| | self.ifcEditor.comboProperty.setCurrentIndex(0) |
| |
|
| | def addIfcPset(self, idx=0): |
| | """Add an IFC property set to the object, within the IFC editor. |
| | |
| | This method runs as a callback when the user selects a property set |
| | within the Add property set dropdown. |
| | |
| | Add the property set selected in the dropdown, then check the property |
| | set definitions for the property set's standard properties, and add |
| | them with blank values to the new property set. |
| | |
| | Parameters |
| | ---------- |
| | idx: int |
| | The index of the options selected from the Add property set |
| | dropdown. |
| | """ |
| |
|
| | if hasattr(self, "ifcEditor") and self.ifcEditor: |
| | if idx == 1: |
| | top = QtGui.QStandardItem( |
| | QtGui.QApplication.translate("Arch", "New property set", None) |
| | ) |
| | top.setDragEnabled(False) |
| | top.setToolTip("PropertySet") |
| | self.ifcModel.appendRow([top, QtGui.QStandardItem(), QtGui.QStandardItem()]) |
| | elif idx > 1: |
| | psetlabel = self.psetkeys[idx - 2] |
| | psetdef = "Pset_" + psetlabel.replace(" ", "") |
| | if psetdef in self.psetdefs: |
| | top = QtGui.QStandardItem(psetdef) |
| | top.setDragEnabled(False) |
| | top.setToolTip("PropertySet") |
| | self.ifcModel.appendRow([top, QtGui.QStandardItem(), QtGui.QStandardItem()]) |
| | for i in range(0, len(self.psetdefs[psetdef]), 2): |
| | self.addIfcProperty( |
| | pset=top, |
| | prop=self.psetdefs[psetdef][i], |
| | ptype=self.psetdefs[psetdef][i + 1], |
| | ) |
| | if idx != 0: |
| | |
| | idx = self.ifcModel.invisibleRootItem().index() |
| | for i in range(self.ifcModel.rowCount()): |
| | if self.ifcModel.item(i, 0).hasChildren(): |
| | self.ifcEditor.treeProperties.setFirstColumnSpanned(i, idx, True) |
| | self.ifcEditor.treeProperties.expandAll() |
| | self.ifcEditor.comboPset.setCurrentIndex(0) |
| |
|
| | def removeIfcProperty(self): |
| | """Remove an IFC property or property set from the object, within the IFC editor. |
| | |
| | This method runs as a callback when the user selects the Delete |
| | selected property/set button within the IFC editor. |
| | |
| | Find the selected property, and delete it. If it's a property set, |
| | delete the children properties as well. |
| | """ |
| |
|
| | if hasattr(self, "ifcEditor") and self.ifcEditor: |
| | sel = self.ifcEditor.treeProperties.selectedIndexes() |
| | if sel: |
| | if self.ifcModel.itemFromIndex(sel[0]).toolTip() == "PropertySet": |
| | self.ifcModel.takeRow(sel[0].row()) |
| | else: |
| | pset = self.ifcModel.itemFromIndex(sel[0].parent()) |
| | pset.takeRow(sel[0].row()) |
| |
|
| | def editClass(self): |
| | """Simple wrapper for BIM_Classification Gui command. |
| | |
| | This method is called when the Edit class button is selected |
| | in the IFC editor. It relies on the presence of the BIM module. |
| | """ |
| |
|
| | FreeCADGui.Selection.clearSelection() |
| | FreeCADGui.Selection.addSelection(self.obj) |
| | FreeCADGui.runCommand("BIM_Classification") |
| |
|
| |
|
| | if FreeCAD.GuiUp: |
| |
|
| | class IfcEditorDelegate(QtGui.QStyledItemDelegate): |
| | """This class manages the editing of the individual table cells in the IFC editor. |
| | |
| | Parameters |
| | ---------- |
| | parent: <PySide2.QtWidgets.QWidget> |
| | Unclear. |
| | dialog: <ArchComponent.ComponentTaskPanel> |
| | The dialog box this delegate was created in. |
| | ptypes: list of str |
| | A list of the names of IFC property types. |
| | plables: list of str |
| | A list of the human readable names of IFC property types. |
| | """ |
| |
|
| | def __init__(self, parent=None, dialog=None, ptypes=[], plabels=[], *args): |
| | self.dialog = dialog |
| | QtGui.QStyledItemDelegate.__init__(self, parent, *args) |
| | self.ptypes = ptypes |
| | self.plabels = plabels |
| |
|
| | def createEditor(self, parent, option, index): |
| | """Return the widget used to change data. |
| | |
| | Return a text line editor if editing the property name. Return a |
| | dropdown to change property type if editing property type. If |
| | editing the property's value, return an appropriate widget |
| | depending on the datatype of the value. |
| | |
| | Parameters |
| | ---------- |
| | parent: <pyside2.qtwidgets.qwidget> |
| | The table cell that is being edited. |
| | option: |
| | Unused? |
| | index: <PySide2.QtCore.QModelIndex> |
| | The index object of the table of the IFC editor. |
| | |
| | Returns |
| | ------- |
| | <pyside2.qtwidgets.qwidget> |
| | The editor widget this method has created. |
| | """ |
| |
|
| | if index.column() == 0: |
| | editor = QtGui.QLineEdit(parent) |
| | elif index.column() == 1: |
| | editor = QtGui.QComboBox(parent) |
| | else: |
| | ptype = index.sibling(index.row(), 1).data() |
| | if "Integer" in ptype: |
| | editor = QtGui.QSpinBox(parent) |
| | elif "Real" in ptype: |
| | editor = QtGui.QDoubleSpinBox(parent) |
| | editor.setDecimals(params.get_param("Decimals", path="Units")) |
| | elif ("Boolean" in ptype) or ("Logical" in ptype): |
| | editor = QtGui.QComboBox(parent) |
| | editor.addItems(["True", "False"]) |
| | elif "Measure" in ptype: |
| | editor = FreeCADGui.UiLoader().createWidget("Gui::InputField") |
| | editor.setParent(parent) |
| | else: |
| | editor = QtGui.QLineEdit(parent) |
| | editor.setObjectName("editor_" + ptype) |
| | return editor |
| |
|
| | def setEditorData(self, editor, index): |
| | """Give data to the edit widget. |
| | |
| | Extract the data already present in the table, and write it to the |
| | editor. This means the user starts the editor with their previous |
| | data already present, instead of a blank slate. |
| | |
| | Parameters |
| | ---------- |
| | editor: <pyside2.qtwidgets.qwidget> |
| | The editor widget. |
| | index: <PySide2.QtCore.QModelIndex> |
| | The index object of the table, of the IFC editor |
| | """ |
| |
|
| | if index.column() == 0: |
| | editor.setText(index.data()) |
| | elif index.column() == 1: |
| | editor.addItems(self.plabels) |
| | if index.data() in self.plabels: |
| | idx = self.plabels.index(index.data()) |
| | editor.setCurrentIndex(idx) |
| | else: |
| | if "Integer" in editor.objectName(): |
| | try: |
| | editor.setValue(int(index.data())) |
| | except Exception: |
| | editor.setValue(0) |
| | elif "Real" in editor.objectName(): |
| | try: |
| | editor.setValue(float(index.data())) |
| | except Exception: |
| | editor.setValue(0) |
| | elif ("Boolean" in editor.objectName()) or ("Logical" in editor.objectName()): |
| | try: |
| | editor.setCurrentIndex(["true", "false"].index(index.data().lower())) |
| | except Exception: |
| | editor.setCurrentIndex(1) |
| | elif "Measure" in editor.objectName(): |
| | try: |
| | editor.setText(index.data()) |
| | except Exception: |
| | editor.setValue(0) |
| | else: |
| | editor.setText(index.data()) |
| |
|
| | def setModelData(self, editor, model, index): |
| | """Write the data in the editor to the IFC editor's table. |
| | |
| | Parameters |
| | ---------- |
| | editor: <pyside2.qtwidgets.qwidget> |
| | The editor widget. |
| | model: |
| | The table object of the IFC editor. |
| | index: <PySide2.QtCore.QModelIndex> |
| | The index object of the table, of the IFC editor |
| | """ |
| |
|
| | if index.column() == 0: |
| | model.setData(index, editor.text()) |
| | elif index.column() == 1: |
| | if editor.currentIndex() > -1: |
| | idx = editor.currentIndex() |
| | data = self.plabels[idx] |
| | model.setData(index, data) |
| | else: |
| | if ("Integer" in editor.objectName()) or ("Real" in editor.objectName()): |
| | model.setData(index, str(editor.value())) |
| | elif ("Boolean" in editor.objectName()) or ("Logical" in editor.objectName()): |
| | model.setData(index, editor.currentText()) |
| | elif "Measure" in editor.objectName(): |
| | model.setData(index, editor.property("text")) |
| | else: |
| | model.setData(index, editor.text()) |
| | self.dialog.update() |
| |
|