Fix wall edit lifecycle + drain wall_offset_gizmos cache on load

Bundled bug fixes + the forward-compat AST guard that prevents the
underlying class of bug from coming back.

* bim/module/model/wall.py: FinishEditingWall._execute early-returns
  CANCELLED when props.is_editing is False. Without this guard, a
  failed enable (e.g. on a wall without IfcMaterialLayerSetUsage)
  leaves is_editing False but a press on finish still walked the
  sub-ops below, which dereferenced layer-set-dependent state and
  crashed.

* tool/model.py: Model.offset_wall now guards against
  ifcopenshell.util.element.get_material returning None before
  calling .is_a("IfcMaterialLayerSetUsage"). Fixes the pre-existing
  test/bim/module/model/test_wall_header_refresh.py crash that has
  been the only failing test in the wall lane since this branch
  started.

* bim/handler.py: _apply_save_file_invariants drains
  wall_offset_gizmos.clear_caches() on load_post. The module-scope
  GenerationKeyedCache instance survives the .blend reload; without
  the drain the cache may serve entries whose bpy_struct references
  point into the freed bpy.data of the previous file.

* test/bim/test_handler_forward_compat.py: AST-walk test that
  enumerates every bim/module/model/*.py source declaring both a
  module-scope GenerationKeyedCache assignment AND a top-level
  clear_caches function, and asserts each module appears as a
  <module>.clear_caches() call in _apply_save_file_invariants. Pins
  the contract: any future module-scope geom cache that exposes
  clear_caches must wire into the load_post drain.

* test/bim/feature/model.feature + test/bim/test_feature.py: wall
  edit-lifecycle scenarios switch from "add cube + assign as
  IfcWallType" to "load the demo construction library + add an
  occurrence of the WAL100 wall type", so the parametric edit runs
  against a real LAYER2 wall with IfcMaterialLayerSetUsage rather
  than a vanilla-mesh promotion that lacks one. The demo-library
  step also picks the schema-matching library file (IFC2X3 /
  IFC4 / IFC4X3) so the appended types remain valid across schemas.
  Door saved-height assertion updates from 2.5 → 2500 to reflect
  that BBIM_Door pset stores project units (METRIC_MM in the
  empty-project fixture).

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-05 10:27:01 +02:00
parent 25651a1507
commit fbe6fe5384
6 changed files with 91 additions and 25 deletions
+14 -23
View File
@@ -687,8 +687,10 @@ Scenario: Saving with a door mid-edit auto-commits the draft value to the IFC ps
Then "active_object.BIMDoorProperties.is_editing" is "True"
When I press "bim.save_project(filepath='{temp_project_path}', should_save_as=True)"
Then "active_object.BIMDoorProperties.is_editing" is "False"
# BBIM_<Type> psets store project units, not raw Blender SI. The empty project
# used in an_empty_blender_session is METRIC_MM, so 2.5 m → 2500 mm in the pset.
And the variable "saved_height" is "__import__('json').loads(ifcopenshell.util.element.get_pset({ifc}.by_type('IfcDoor')[0], 'BBIM_Door', 'Data'))['overall_height']"
And the variable "saved_height" equals "2.5"
And the variable "saved_height" equals "2500.0"
Scenario: Saving with no parametric edits in progress leaves the door pset unchanged
Given an empty IFC project
@@ -705,14 +707,10 @@ Scenario: Saving with no parametric edits in progress leaves the door pset uncha
Scenario: Saving with a wall mid-edit auto-commits the draft to IFC
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{cube}"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall" is selected
And I press "bim.enable_editing_wall()"
@@ -722,21 +720,18 @@ Scenario: Saving with a wall mid-edit auto-commits the draft to IFC
Scenario: Enabling and finishing a wall edit with no drag is a no-op
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{cube}"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall" is selected
And the variable "entity_count_before" is "len(list({ifc}))"
When I press "bim.enable_editing_wall()"
And I press "bim.finish_editing_wall()"
Then "active_object.BIMWallProperties.is_editing" is "False"
And "len(list({ifc}))" is "{entity_count_before}"
And the variable "entity_count_after" is "len(list({ifc}))"
And the variable "entity_count_after" equals "{entity_count_before}"
Scenario: Cancelling a wall edit clears is_editing
Given an empty IFC project
@@ -756,14 +751,10 @@ Scenario: Cancelling a wall edit clears is_editing
Scenario: Wall parametric edit works on IFC2X3 projects
Given an empty IFC2X3 project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{cube}"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall" is selected
When I press "bim.enable_editing_wall()"