Bonsai/ifc migrate ifc2x3 downgrade + patch preset system by Gorgious56 · Pull Request #8195 · IfcOpenShell/IfcOpenShell · GitHub
Skip to content

Bonsai/ifc migrate ifc2x3 downgrade + patch preset system#8195

Merged
Gorgious56 merged 4 commits into
IfcOpenShell:v0.8.0from
Gorgious56:bonsai/ifc-migrate-ifc2x3-downgrade
Jun 23, 2026
Merged

Bonsai/ifc migrate ifc2x3 downgrade + patch preset system#8195
Gorgious56 merged 4 commits into
IfcOpenShell:v0.8.0from
Gorgious56:bonsai/ifc-migrate-ifc2x3-downgrade

Conversation

@Gorgious56

Copy link
Copy Markdown
Contributor

Summary

Two patch-module improvements that landed together because they share the same Bonsai panel:

  1. Defensive IFC4 / IFC4X3 → IFC2X3 downgrade in the Migrate ifcpatch recipe. Previously crashed mid-loop with the cryptic RuntimeError: Entity with name '' not found in schema 'IFC2X3'class_4_to_2x3.json marks IFC4-only classes with an empty-string sentinel and the old code forwarded that to create_entity. Real files routinely contain IfcPolygonalFaceSet, IfcTriangulatedFaceSet, IfcIndexedPolyCurve, IfcLamp, IfcPipeSegment, IfcGeographicElement, etc.
  2. Per-recipe preset system for the IFC Patch panel. Save / load / remove sets of recipe arguments via Blender's standard preset infrastructure, scoped per recipe so an ExtractElements preset doesn't pollute the Migrate preset list. Persists across files and sessions via the standard Blender preset directory.

User-facing features (Bonsai panel)

Lossy-downgrade confirmation popup

ExecuteIfcPatch.invoke now shows a confirmation dialog before running a destructive migration. Fires when the user picks Migrate with a target schema older than the source's (IFC4 → IFC2X3, IFC4X3 → IFC2X3). Lists what's preserved (geometry via curve flatten + face-set → brep conversion) vs lost (IFC4-only element classes demoted to IfcBuildingElementProxy, PredefinedType enum values absent from IFC2X3 dropped). User explicitly approves before the recipe runs.

Backed by tool.Patch.migration_is_lossy_downgrade(), which resolves the source schema via header-only parsing (regex on first ~2KB) + get_fallback_schema normalisation — avoids a full ifcopenshell.open() on every Execute click. The target schema is looked up by argument name, not position.

Per-recipe preset system

New BIM_MT_ifc_patch_presets menu + AddIfcPatchPreset operator wire Blender's standard preset infrastructure into the IFC Patch panel. The preset row sits above the dynamic argument fields with a "Preset" heading, the menu dropdown, and + / - buttons.

  • Per-recipe preset subdirectory (bonsai/ifc_patch/<RecipeName>/) — presets created for ExtractElements don't show up when Migrate is selected and vice versa.
  • Preset save uses Attribute.get_value_name() (single source of truth for data_type → storage-field mapping) to build the preset_values list dynamically per recipe — works for every recipe out of the box.
  • Recipe-change callback resets BIM_MT_ifc_patch_presets.bl_label to the canonical title. Blender's script.execute_preset mutates the menu's bl_label to the loaded preset's name as a "currently-selected" indicator; without an explicit reset the previous recipe's preset name would falsely advertise itself in the new recipe's menu.
  • Standard Blender preset persistence — presets live under ~/.../scripts/presets/bonsai/ifc_patch/<RecipeName>/<preset_name>.py and survive across files and sessions.

What changed under the hood

Library layer (ifcopenshell.util):

  • schema.Migrator(fallback_element_to_proxy=False) opt-in — IFC4-only IfcElement subclasses migrate to IfcBuildingElementProxy instead of raising. Default preserves the strict failure-on-unmappable contract for existing callers (classification API).
  • schema.geometry_classes_introduced_after(target, source) — derives the IfcRepresentationItem subclasses present in source but absent in target directly from the loaded schemas (cached). Replaces hand-curated class lists. ifc4_only_geometry_classes() retained as an alias.
  • schema.generate_default_value — synthesises a unit IfcAxis2Placement2D / IfcAxis2Placement3D when downgrading entities whose Position became required (IfcIShapeProfileDef etc.).
  • schema.migrate_attribute — structural enum-mismatch detection via get_enum_items replaces the previous string-matched RuntimeError catch, so upgrade paths still surface real bugs.
  • shape_builder.polygonal_face_set_to_faceted_brep(face_set) — direct entity-level conversion of IfcPolygonalFaceSet / IfcTriangulatedFaceSet to IfcFacetedBrep, preserving topology incl. IfcIndexedPolygonalFaceWithVoids inner bounds.
  • shape_builder.arc_to_polyline_points(start, mid, end, n) — circle-fit chord approximation for arc segments.

Recipe layer (ifcpatch.recipes.Migrate + DowngradeIndexedPolyCurve):

  • New preprocessing pipeline gated on target=IFC2X3 + source in (IFC4, IFC4X3): curve flatten → face-set → brep conversion → orphan purge.
  • DowngradeIndexedPolyCurve now handles IfcArcIndex segments (16-chord approximation) and absent Segments list (IFC4 implicit-polyline case). Multi-index IfcLineIndex handled correctly.
  • Post-pass encodes <OriginalClass>/<PredefinedType> into ObjectType when empty, so the lost subclass identity survives the downgrade as searchable text (e.g. "IfcLamp/COMPACTFLUORESCENT").
  • Migration loop collects per-entity failures into a list; summary RuntimeError fires at end naming up to 20 with their inverse references — no more cryptic mid-loop crash.

Commits (single-concern per AGENTS.md)

  1. a2dafc9ceifcopenshell.util: schema-aware downgrade helpers
  2. f710929e9ifcpatch Migrate: defensive IFC4/IFC4X3 → IFC2X3 downgrade
  3. 2ab5ca922ifcpatch: small recipe polish (ExtractElements docstring, FixArchiCADToRevitDoorSwings null-check)
  4. 44c0c2916 — Bonsai patch: lossy-downgrade popup + per-recipe preset menu

Test coverage

Lane Tests added Total in lane after
src/ifcopenshell-python/test/util/ 17 360 passed (1 pre-existing C++ stub-validation failure, untouched)
src/ifcpatch/test/ 11 (6 Migrate + 5 DowngradeIndexedPolyCurve) 54 passed (4 pre-existing failures: 3 MergeProjects context-dedup + 1 Windows temp-file PermissionError)
src/bonsai/test/bim/module/patch/ (new package) 12 12 passed

Plus verified untouched: 1426/1426 test/api/ pass (the other Migrator callers — add_classification, add_reference — keep the strict-default contract).

The 12 new bim-lane tests under test/bim/module/patch/ cover:

  • Lossy-downgrade detection truth table (6 cases: source × target × recipe)
  • Header-only schema parsing incl. IFC4X3_ADD2 regression pin
  • Preset menu label reset on recipe change
  • End-to-end downgrade: in-memory IFC4 IfcLampbpy.ops.bim.execute_ifc_patch → on-disk IFC2X3 file with IfcBuildingElementProxy carrying ObjectType="IfcLamp/COMPACTFLUORESCENT" and the original GlobalId.

Verification

  • Lint clean across all touched files (black --check, ruff check).
  • Original failing IFC4 file reported by the issue exports to IFC2X3 successfully (manually verified in Bonsai).
  • Regression pins for the two highest-risk bugs caught during review: IFC4X3_ADD2 schema-prefix collision (the startswith("IFC4") mismatch) and the IFC4X3-source preprocessing gap.

API additions / changes

  • Migrator(*, fallback_element_to_proxy: bool = False) — new keyword-only constructor parameter. Default preserves backwards compat.
  • ifcopenshell.util.schema.geometry_classes_introduced_after(target, source="IFC4") — new public function.
  • ifcopenshell.util.shape_builder.polygonal_face_set_to_faceted_brep(face_set) — new public function.
  • ifcopenshell.util.shape_builder.arc_to_polyline_points(start, mid, end, subdivisions=16) — new public function.
  • bonsai.tool.Patch.{get_preset_subdir, migration_is_lossy_downgrade} — new public class methods.
  • Migrator no longer raises a bare RuntimeError: Entity with name '' not found for unmappable IFC4-only classes — instead raises NotImplementedError with the failing class + inverse references named, or falls back to IfcBuildingElementProxy when the opt-in flag is set.

Known limitations / out of scope

  • IFC4X3-only entities outside any IfcProduct representation (e.g. orphan IfcAlignmentCurve in standalone-utility files) still surface in the failure summary rather than being auto-converted. These are rare in real models.
  • The Migrate recipe still raises RuntimeError for non-IfcElement IFC4-only classes (rels, materials, time entities). Same as before — these have no meaningful generic IFC2X3 stand-in.
  • Preset persistence is per-Blender-version (standard Blender preset directory is version-specific). A future Blender upgrade does not migrate presets automatically.

AI-assisted code attribution (per AGENTS.md §0b)

All four commits in this PR were authored with the assistance of an AI coding tool (commit bodies carry the marker). New files added by this PR include the top-of-file # This file was generated with the assistance of an AI coding tool. marker. The architectural decisions (API shapes, opt-in flag, ObjectType encoding strategy, per-recipe preset subdirectory) were reviewed and approved by the human author before each commit.

Adds the IFC-library primitives the ifcpatch Migrate recipe needs for a
defensive IFC4 / IFC4X3 -> IFC2X3 downgrade without each caller
reinventing the wheel.

In ifcopenshell.util.schema:
- Migrator(fallback_element_to_proxy=False) opt-in: when True, IFC4-only
  IfcElement subclasses (IfcLamp, IfcPipeSegment, IfcGeographicElement,
  ...) migrate to IfcBuildingElementProxy instead of raising. Default
  preserves the strict failure-on-unmappable contract for existing
  callers (classification API, etc.).
- geometry_classes_introduced_after(target, source) derives the
  IfcRepresentationItem subclasses present in `source` but absent in
  `target` directly from the loaded schemas. Cached per pair. Replaces
  hand-curated class lists that drift with each IFC update.
  ifc4_only_geometry_classes() retained as an alias.
- generate_default_value synthesises a unit IfcAxis2Placement2D /
  IfcAxis2Placement3D when downgrading entities whose Position became
  required in the target schema (IfcIShapeProfileDef and friends in
  IFC2X3).
- Enum-mismatch detection upgraded from string-matched RuntimeError to a
  structural check via ifcopenshell.util.attribute.get_enum_items so
  upgrade paths still surface real bugs loudly.

In ifcopenshell.util.shape_builder:
- polygonal_face_set_to_faceted_brep converts IfcPolygonalFaceSet /
  IfcTriangulatedFaceSet (IFC4-only) directly to IfcFacetedBrep,
  preserving topology including IfcIndexedPolygonalFaceWithVoids inner
  bounds. Validates inputs at the boundary.
- arc_to_polyline_points approximates a circular arc through three
  points with a chord polyline of configurable subdivisions. Tolerates
  floating-point noise on planar Z. Raises on non-planar or invalid
  inputs.

Test coverage: 47 unit tests across schema + shape_builder lanes
covering each helper directly (no transitive-only coverage), including
regression pins for the IFC4X3-prefix ordering invariant in
get_fallback_schema and the strict-default Migrator contract.

Generated with the assistance of an AI coding tool.
The Migrate recipe previously crashed mid-loop with the cryptic
`RuntimeError: Entity with name '' not found in schema 'IFC2X3'` when
asked to downgrade an IFC4 or IFC4X3 file to IFC2X3 — the
class_4_to_2x3 mapping marks IFC4-only geometry / element classes with
an empty-string sentinel and the old code blindly forwarded that to
create_entity. Real files routinely contain IfcPolygonalFaceSet,
IfcTriangulatedFaceSet, IfcIndexedPolyCurve, IfcLamp, IfcPipeSegment,
IfcGeographicElement, etc.

The recipe now runs a preprocessing pipeline when the target is IFC2X3
and the source is IFC4 or IFC4X3:

- DowngradeIndexedPolyCurve flattens IfcIndexedPolyCurve to IfcPolyline
  for the whole file (arcs included — see below).
- IfcPolygonalFaceSet / IfcTriangulatedFaceSet are converted directly
  to IfcFacetedBrep at the entity level via
  ifcopenshell.util.shape_builder.polygonal_face_set_to_faceted_brep,
  preserving topology including IfcIndexedPolygonalFaceWithVoids inner
  bounds. IfcShapeRepresentation carriers have their RepresentationType
  tag updated from "Tessellation" to "Brep".
- Orphan source-only geometry instances (left over after the rewires)
  are purged iteratively via
  geometry_classes_introduced_after(target, source).

The Migrator is invoked with fallback_element_to_proxy=True so
IFC4-only IfcElement subclasses (IfcLamp, IfcPipeSegment,
IfcGeographicElement, ...) become IfcBuildingElementProxy in the
output. A post-pass encodes "<OriginalClass>/<PredefinedType>" into
ObjectType (e.g. "IfcLamp/COMPACTFLUORESCENT") when ObjectType is
empty, so the lost subclass identity survives the downgrade as
searchable text.

The migration loop now collects per-entity failures into a list rather
than crashing on the first; a summary RuntimeError fires at end if any
failed, naming up to 20 with their inverse references. Successful
migrations log a single count line via self.logger.

DowngradeIndexedPolyCurve extended:
- Arc segments (IfcArcIndex) are flattened via
  ifcopenshell.util.shape_builder.arc_to_polyline_points with
  ARC_SUBDIVISION=16 chord points per arc.
- Multi-index IfcLineIndex segments handled correctly.
- Absent Segments list (IFC4 polyline-through-all-coords case) handled.

Test coverage: 11 tests across the two recipes covering all four
preprocessing branches, the IFC4X3 source gate, the ObjectType
encoding (incl. author-supplied ObjectType preservation), the summary
RuntimeError shape, and the arc subdivision.

Generated with the assistance of an AI coding tool.
ExtractElements: expand the `query` docstring to cover the exclusion
syntax (`!` on entity classes, `!=` on attribute / pset / material /
classification / location / group facets) and the "seed with a broad
include before subtracting" gotcha — entity-class exclusion does not
auto-seed from "all elements", so a bare `! IfcSlab` query returns
nothing.

FixArchiCADToRevitDoorSwings: guard the `IfcIndexedPolyCurve.Segments`
loop against the IFC4 case where Segments is absent (a polyline
through all coords in declared order). Previously crashed on
`None.__iter__`.

Generated with the assistance of an AI coding tool.
Two new UX features in the IFC Patch panel, both backed by helpers on
bonsai.tool.Patch.

Lossy-downgrade confirmation popup. When the user picks the Migrate
recipe with a target schema older than the source's (IFC4 -> IFC2X3,
IFC4X3 -> IFC2X3), ExecuteIfcPatch.invoke shows a properties dialog
listing what's preserved vs lost: IfcIndexedPolyCurve flattened with
arcs approximated, IfcPolygonalFaceSet / IfcTriangulatedFaceSet
converted to IfcFacetedBrep, IFC4-only IfcElement subclasses (IfcLamp,
IfcPipeSegment, IfcGeographicElement, ...) demoted to
IfcBuildingElementProxy with the original class + PredefinedType
encoded into ObjectType, and PredefinedType enum values absent from
IFC2X3 dropped. The user explicitly approves before the recipe runs.

The popup is gated on tool.Patch.migration_is_lossy_downgrade() which
resolves the source schema via header-only parsing
(tool.Patch._patch_source_schema reads the first ~2KB and matches a
FILE_SCHEMA regex, then normalises via ifcopenshell.util.schema.
get_fallback_schema). Avoids a full ifcopenshell.open() on every
Execute click — multi-second saving on large files. The target schema
is looked up by argument name rather than position so it survives
recipe-parameter reordering.

Per-recipe preset menu. New BIM_MT_ifc_patch_presets + AddIfcPatchPreset
wire Blender's standard preset system into the panel. Each recipe gets
its own preset subdirectory (bonsai/ifc_patch/<RecipeName>/), so a
preset saved for ExtractElements does not pollute the Migrate preset
list. The preset operator uses Attribute.get_value_name() (single
source of truth for data_type -> storage-field mapping) to build the
preset_values list dynamically per recipe.

The recipe-change callback resets
BIM_MT_ifc_patch_presets.bl_label to the canonical title — Blender's
script.execute_preset mutates the menu's bl_label to the loaded
preset's name as a "currently-selected" indicator, and without an
explicit reset the previous recipe's preset name would falsely advertise
itself in the new recipe's menu.

tool.Patch gains get_preset_subdir, migration_is_lossy_downgrade,
_patch_source_schema as cross-cutting helpers. _SCHEMA_AGE module
constant provides the ordering used by the downgrade-detection
predicate.

Test coverage: 12 bim-lane tests under test/bim/module/patch/. The
truth table for migration_is_lossy_downgrade covers IFC4/IFC4X3 source
x downgrade/upgrade/same-schema target x Migrate/non-Migrate recipe.
The schema-sniffing tests write a real IFC4X3_ADD2 file to disk and
assert the helper resolves it to IFC4X3 (regression for the original
startswith iteration-order bug). An end-to-end test drives
bpy.ops.bim.execute_ifc_patch with an in-memory IfcLamp source and
verifies the on-disk IFC2X3 file contains a single
IfcBuildingElementProxy with ObjectType "IfcLamp/COMPACTFLUORESCENT"
and the original GlobalId preserved.

Generated with the assistance of an AI coding tool.
@Gorgious56 Gorgious56 changed the title Bonsai/ifc migrate ifc2x3 downgrade Bonsai/ifc migrate ifc2x3 downgrade + patch preset system Jun 23, 2026
@Gorgious56 Gorgious56 merged commit 262f5f9 into IfcOpenShell:v0.8.0 Jun 23, 2026
2 of 4 checks passed
@Gorgious56 Gorgious56 deleted the bonsai/ifc-migrate-ifc2x3-downgrade branch June 23, 2026 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant