diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index eaf4c356be..f6d6efd545 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -442,6 +442,8 @@ class ExtendWallsToUnderside(_CommitWallDraftsFirstMixin, bpy.types.Operator, to element = tool.Ifc.get_entity(obj) if not element: continue + # IFC-class agnostic: any LAYER2 body (wall, covering, …) is + # extendable; everything else is treated as an underside target. if tool.Parametric.is_path_connectable_wall(element): walls.append(obj) else: @@ -3208,12 +3210,12 @@ def _pick_dominant_wall_material( def regenerate_fillet_corner_wall(element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None: - """Rebuild a fillet corner wall's banana body from ``BBIM_Wall.FilletRadius`` - and its neighbours' current layer parameters.""" + """Rebuild a fillet corner wall's banana body from + ``EPset_Parametric.FilletRadius`` and its neighbours' current layer parameters.""" ifc_file = tool.Ifc.get() if ifc_file is None: return - radius_si = ifcopenshell.util.element.get_pset(element, "BBIM_Wall", "FilletRadius") + radius_si = tool.Parametric.get_parametric_prop(element, "FilletRadius") if not radius_si: return @@ -3417,7 +3419,7 @@ class EnableWallFilletPreviewFromCorner(bpy.types.Operator): self.report({"ERROR"}, "Selection is not a fillet corner wall.") return {"CANCELLED"} - radius = ifcopenshell.util.element.get_pset(corner_elem, "BBIM_Wall", "FilletRadius") + radius = tool.Parametric.get_parametric_prop(corner_elem, "FilletRadius") if not radius: self.report({"ERROR"}, "Corner wall has no FilletRadius pset to re-edit.") return {"CANCELLED"} @@ -3633,12 +3635,13 @@ class CreateWallFillet(bpy.types.Operator, tool.Ifc.Operator): Vector((chord_length_si / unit_scale, 0.0)), ) - # Mark the corner wall BEFORE the downstream recalculate so + # Mark the corner BEFORE the downstream recalculate so # tool.Model.recreate_wall short-circuits and preserves the curved # geometry. The pset also gates the enable poll. FilletRadius is # stored alongside IsFilletCorner so the corner can be rebuilt later - # (neighbour move, layer-thickness edit, pen-icon re-edit). - pset = ifcopenshell.api.pset.add_pset(ifc_file, product=corner_elem, name="BBIM_Wall") + # (neighbour move, layer-thickness edit, pen-icon re-edit). Stored on + # the class-agnostic EPset_Parametric so LAYER2 siding corners work too. + pset = ifcopenshell.api.pset.add_pset(ifc_file, product=corner_elem, name=tool.Parametric.PARAMETRIC_PSET) ifcopenshell.api.pset.edit_pset( ifc_file, pset=pset, @@ -4340,10 +4343,12 @@ class GizmoPairDisconnect(bpy.types.GizmoGroup, gizmo.BillboardingGizmoGroupMixi if pair is None: return active, partner_obj, active_elem, partner_elem = pair - # Helper expects wall + slab regardless of which the user marked active. - if active_elem.is_a("IfcWall"): + # Helper expects the LAYER2 element + its underside target regardless of + # which the user marked active. Class-agnostic: the LAYER2 side may be a + # wall or a covering (siding), the target a slab/roof/etc. + if tool.Parametric.is_path_connectable_wall(active_elem): wall_obj, slab_obj = active, partner_obj - elif partner_elem.is_a("IfcWall"): + elif tool.Parametric.is_path_connectable_wall(partner_elem): wall_obj, slab_obj = partner_obj, active else: return diff --git a/src/bonsai/bonsai/tool/parametric.py b/src/bonsai/bonsai/tool/parametric.py index 623405fe16..d4e67e8fda 100644 --- a/src/bonsai/bonsai/tool/parametric.py +++ b/src/bonsai/bonsai/tool/parametric.py @@ -486,40 +486,70 @@ class Parametric(bonsai.core.tool.Parametric): @classmethod def is_wall(cls, element: entity_instance) -> bool: - """A wall is editable by the parametric gizmo if it is an IfcWall with LAYER2 usage. + """A LAYER2 axis-driven element editable by the parametric wall gizmo. - Unlike doors/windows/stairs, walls do not carry a proprietary BBIM_Wall pset — - their parametric state lives in standard IFC (axis polyline, IfcMaterialLayerSetUsage, - IfcExtrudedAreaSolid). Any LAYER2 wall qualifies.""" - if element is None or not element.is_a("IfcWall"): + IFC-class agnostic by design. The gizmos, edit mode and property panel + all operate on the standard-IFC parametric state — axis polyline, + ``IfcMaterialLayerSetUsage`` (LAYER2), ``IfcExtrudedAreaSolid`` — none of + which is exclusive to ``IfcWall``. So any element modelled this way + qualifies: typically an ``IfcWall``, but also vertical claddings / siding + (``IfcCovering``) that are drawn, edited and joined the same way. Unlike + doors / windows / stairs these carry no proprietary pset — their + parametric state lives entirely in standard IFC. + + Gated on LAYER2 usage rather than entity type; LAYER3 elements (slabs) + and anything without vertical layered usage are excluded.""" + if element is None: return False return tool.Model.get_usage_type(element) == "LAYER2" @classmethod def is_path_connectable_wall(cls, element: entity_instance) -> bool: - """An IfcWall that may participate in IfcRelConnectsPathElements joins — - either a LAYER2 parametric wall, or a fillet-corner wall whose body is - hand-built but whose axis still drives path connections. + """A LAYER2 axis-driven element (or fillet corner) whose axis can drive + ``IfcRelConnectsPathElements`` joins and underside clips. - Distinct from ``is_wall``: that predicate gates parametric edits that - would regenerate the body and flatten a curved fillet. Unjoin / join - gizmo polls and path-connection partner enumeration use this looser - predicate so fillet corners (which have no LAYER2 usage by spec) still - surface their join icons.""" - if element is None or not element.is_a("IfcWall"): + IFC-class agnostic, like :meth:`is_wall` — path connection, unjoin, + extend-to-wall and extend-to-underside all read placement + axis + + layer set, never the entity type — so LAYER2 claddings / siding + (``IfcCovering``) join and clip the same way an ``IfcWall`` does. + + Looser than :meth:`is_wall` in one respect: it also admits fillet-corner + walls, whose hand-built body carries no LAYER2 usage by spec but whose + axis still drives path connections. :meth:`is_wall` excludes them because + regenerating their body from the axis would flatten the curve.""" + if element is None: return False if tool.Model.get_usage_type(element) == "LAYER2": return True return cls.is_fillet_corner_wall(element) + # Bonsai's class-agnostic parametric metadata (currently the fillet-corner + # flags IsFilletCorner / FilletRadius). Deliberately not on the entity- + # specific BBIM_Wall pset, so the same state can live on any LAYER2 element + # — a wall or a vertical cladding / siding (IfcCovering). + PARAMETRIC_PSET = "EPset_Parametric" + # Files authored before the move stored these flags on the wall-only pset. + LEGACY_PARAMETRIC_PSET = "BBIM_Wall" + @classmethod - def is_fillet_corner_wall(cls, element: entity_instance) -> bool: - """``True`` if the wall carries the ``BBIM_Wall.IsFilletCorner`` flag, - marking it as a curved corner whose banana body is hand-built rather - than regenerated from the wall's axis + layer set.""" + def get_parametric_prop(cls, element: entity_instance, name: str) -> Any: + """Read a Bonsai parametric flag from ``EPset_Parametric``, falling back + to the legacy ``BBIM_Wall`` pset for files authored before the fillet- + corner state was generalized off the wall-specific pset.""" import ifcopenshell.util.element - return bool(ifcopenshell.util.element.get_pset(element, "BBIM_Wall", "IsFilletCorner")) + value = ifcopenshell.util.element.get_pset(element, cls.PARAMETRIC_PSET, name) + if value is None: + value = ifcopenshell.util.element.get_pset(element, cls.LEGACY_PARAMETRIC_PSET, name) + return value + + @classmethod + def is_fillet_corner_wall(cls, element: entity_instance) -> bool: + """``True`` if the element carries the ``EPset_Parametric.IsFilletCorner`` + flag (legacy: ``BBIM_Wall.IsFilletCorner``), marking it as a curved + corner whose banana body is hand-built rather than regenerated from the + axis + layer set. Class-agnostic — walls and LAYER2 siding alike.""" + return bool(cls.get_parametric_prop(element, "IsFilletCorner")) @classmethod def is_pipe_segment(cls, element: entity_instance) -> bool: