{{ message }}
Bonsai/ifc migrate ifc2x3 downgrade + patch preset system#8195
Merged
Gorgious56 merged 4 commits intoJun 23, 2026
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Two patch-module improvements that landed together because they share the same Bonsai panel:
Migrateifcpatch recipe. Previously crashed mid-loop with the crypticRuntimeError: Entity with name '' not found in schema 'IFC2X3'—class_4_to_2x3.jsonmarks IFC4-only classes with an empty-string sentinel and the old code forwarded that tocreate_entity. Real files routinely containIfcPolygonalFaceSet,IfcTriangulatedFaceSet,IfcIndexedPolyCurve,IfcLamp,IfcPipeSegment,IfcGeographicElement, etc.ExtractElementspreset doesn't pollute theMigratepreset list. Persists across files and sessions via the standard Blender preset directory.User-facing features (Bonsai panel)
Lossy-downgrade confirmation popup
ExecuteIfcPatch.invokenow shows a confirmation dialog before running a destructive migration. Fires when the user picksMigratewith 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 toIfcBuildingElementProxy,PredefinedTypeenum 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_schemanormalisation — avoids a fullifcopenshell.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_presetsmenu +AddIfcPatchPresetoperator 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.bonsai/ifc_patch/<RecipeName>/) — presets created forExtractElementsdon't show up whenMigrateis selected and vice versa.Attribute.get_value_name()(single source of truth fordata_type→ storage-field mapping) to build thepreset_valueslist dynamically per recipe — works for every recipe out of the box.BIM_MT_ifc_patch_presets.bl_labelto the canonical title. Blender'sscript.execute_presetmutates the menu'sbl_labelto 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.~/.../scripts/presets/bonsai/ifc_patch/<RecipeName>/<preset_name>.pyand survive across files and sessions.What changed under the hood
Library layer (
ifcopenshell.util):schema.Migrator(fallback_element_to_proxy=False)opt-in — IFC4-onlyIfcElementsubclasses migrate toIfcBuildingElementProxyinstead of raising. Default preserves the strict failure-on-unmappable contract for existing callers (classification API).schema.geometry_classes_introduced_after(target, source)— derives theIfcRepresentationItemsubclasses present insourcebut absent intargetdirectly from the loaded schemas (cached). Replaces hand-curated class lists.ifc4_only_geometry_classes()retained as an alias.schema.generate_default_value— synthesises a unitIfcAxis2Placement2D/IfcAxis2Placement3Dwhen downgrading entities whosePositionbecame required (IfcIShapeProfileDefetc.).schema.migrate_attribute— structural enum-mismatch detection viaget_enum_itemsreplaces the previous string-matchedRuntimeErrorcatch, so upgrade paths still surface real bugs.shape_builder.polygonal_face_set_to_faceted_brep(face_set)— direct entity-level conversion ofIfcPolygonalFaceSet/IfcTriangulatedFaceSettoIfcFacetedBrep, preserving topology incl.IfcIndexedPolygonalFaceWithVoidsinner bounds.shape_builder.arc_to_polyline_points(start, mid, end, n)— circle-fit chord approximation for arc segments.Recipe layer (
ifcpatch.recipes.Migrate+DowngradeIndexedPolyCurve):target=IFC2X3 + source in (IFC4, IFC4X3): curve flatten → face-set → brep conversion → orphan purge.DowngradeIndexedPolyCurvenow handlesIfcArcIndexsegments (16-chord approximation) and absentSegmentslist (IFC4 implicit-polyline case). Multi-indexIfcLineIndexhandled correctly.<OriginalClass>/<PredefinedType>intoObjectTypewhen empty, so the lost subclass identity survives the downgrade as searchable text (e.g."IfcLamp/COMPACTFLUORESCENT").RuntimeErrorfires at end naming up to 20 with their inverse references — no more cryptic mid-loop crash.Commits (single-concern per AGENTS.md)
a2dafc9ce—ifcopenshell.util: schema-aware downgrade helpersf710929e9—ifcpatch Migrate: defensive IFC4/IFC4X3 → IFC2X3 downgrade2ab5ca922—ifcpatch: small recipe polish (ExtractElementsdocstring,FixArchiCADToRevitDoorSwingsnull-check)44c0c2916— Bonsai patch: lossy-downgrade popup + per-recipe preset menuTest coverage
src/ifcopenshell-python/test/util/src/ifcpatch/test/MergeProjectscontext-dedup + 1 Windows temp-file PermissionError)src/bonsai/test/bim/module/patch/(new package)Plus verified untouched: 1426/1426
test/api/pass (the otherMigratorcallers —add_classification,add_reference— keep the strict-default contract).The 12 new bim-lane tests under
test/bim/module/patch/cover:IFC4X3_ADD2regression pinIfcLamp→bpy.ops.bim.execute_ifc_patch→ on-disk IFC2X3 file withIfcBuildingElementProxycarryingObjectType="IfcLamp/COMPACTFLUORESCENT"and the originalGlobalId.Verification
black --check,ruff check).IFC4X3_ADD2schema-prefix collision (thestartswith("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.Migratorno longer raises a bareRuntimeError: Entity with name '' not foundfor unmappable IFC4-only classes — instead raisesNotImplementedErrorwith the failing class + inverse references named, or falls back toIfcBuildingElementProxywhen the opt-in flag is set.Known limitations / out of scope
IfcProductrepresentation (e.g. orphanIfcAlignmentCurvein standalone-utility files) still surface in the failure summary rather than being auto-converted. These are rare in real models.Migraterecipe still raisesRuntimeErrorfor non-IfcElementIFC4-only classes (rels, materials, time entities). Same as before — these have no meaningful generic IFC2X3 stand-in.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.