mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
Make LAYER2 parametric wall tools IFC-class agnostic
The wall gizmos, joins, extend/regenerate-to-underside and fillet-corner
machinery are driven entirely by placement + axis + IfcMaterialLayerSetUsage,
never the entity type. The IfcWall restriction lived only in gate predicates
and a wall-specific pset, so any AXIS2 LAYER2 element (e.g. vertical
IfcCovering siding/cladding) was excluded despite working end-to-end.
- Drop the is_a("IfcWall") gate from is_wall and is_path_connectable_wall;
gate on LAYER2 usage (plus fillet corner) instead.
- Resolve the wall/underside gizmo partner pair via the predicate, not is_a.
- Move fillet-corner state (IsFilletCorner / FilletRadius) off the wall-only
BBIM_Wall pset onto the class-agnostic EPset_Parametric, via a shared
get_parametric_prop helper that falls back to BBIM_Wall for existing files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user