diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index facc68bb43..2c2886f464 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -41,20 +41,8 @@ import bonsai.bim.helper import bonsai.tool as tool from bonsai.bim.ifc import is_cache_locked_by_other_process from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty -from bonsai.bim.module.model.prop import ( - BIMDoorProperties, - BIMRailingProperties, - BIMRoofProperties, - BIMStairProperties, - BIMWindowProperties, -) -from bonsai.bim.module.model.ui import ( - draw_door_properties, - draw_railing_properties, - draw_roof_properties, - draw_stair_properties, - draw_window_properties, -) +from bonsai.bim.module.model import prop as _model_prop +from bonsai.bim.module.model import ui as _model_ui from bonsai.bim.module.pset.prop import IfcProperty from bonsai.bim.prop import Attribute @@ -279,34 +267,29 @@ class BIM_UL_panel_visibilities(bpy.types.UIList): class GizmoPreferences(bpy.types.PropertyGroup): """Aggregator for parametric gizmo visibility settings. One flat bool per parametric feature; controls whether that feature's gizmo group polls - visible in the viewport.""" + visible in the viewport. + + The per-feature ``: BoolProperty`` fields are derived from + ``tool.Parametric.EDIT_TYPES`` at module load — adding a new parametric + type to the registry automatically surfaces its toggle here, with no + parallel hand-maintained list to keep in sync.""" draw_gizmos_in_3d_viewport: BoolProperty( name="Draw Gizmos In 3D Viewport", default=True, description="Show interactive gizmos in the 3D viewport for parametric elements", ) - door: BoolProperty(name="Door", default=True) - window: BoolProperty(name="Window", default=True) - stair: BoolProperty(name="Stair", default=True) - railing: BoolProperty(name="Railing", default=True) - roof: BoolProperty(name="Roof", default=True) - array: BoolProperty(name="Array", default=True) - pipe_segment: BoolProperty(name="Pipe Segment", default=True) - duct_segment: BoolProperty(name="Duct Segment", default=True) - wall: BoolProperty(name="Wall", default=True) if TYPE_CHECKING: draw_gizmos_in_3d_viewport: bool - door: bool - window: bool - stair: bool - railing: bool - roof: bool - array: bool - pipe_segment: bool - duct_segment: bool - wall: bool + + +for _gizmo_pref_entry in tool.Parametric.EDIT_TYPES: + GizmoPreferences.__annotations__[_gizmo_pref_entry.name] = BoolProperty( + name=_gizmo_pref_entry.name.replace("_", " ").title(), + default=True, + ) +del _gizmo_pref_entry class DocPreferences(bpy.types.PropertyGroup): @@ -402,11 +385,22 @@ class DocPreferences(bpy.types.PropertyGroup): class DefaultParameters(bpy.types.PropertyGroup): - door: bpy.props.PointerProperty(type=BIMDoorProperties) - window: bpy.props.PointerProperty(type=BIMWindowProperties) - railing: bpy.props.PointerProperty(type=BIMRailingProperties) - roof: bpy.props.PointerProperty(type=BIMRoofProperties) - stair: bpy.props.PointerProperty(type=BIMStairProperties) + """Per-type preset values used to seed new parametric instances. + + The ``: PointerProperty`` fields are derived from the subset of + ``tool.Parametric.EDIT_TYPES`` flagged ``has_default_parameters=True``, + each pointing at the matching ``BIMProperties`` class. Adding a + new entry with that flag automatically surfaces a preferences section + and gives the create operator a preset to copy from.""" + + +for _default_params_entry in tool.Parametric.EDIT_TYPES: + if not _default_params_entry.has_default_parameters: + continue + DefaultParameters.__annotations__[_default_params_entry.name] = bpy.props.PointerProperty( + type=getattr(_model_prop, _default_params_entry.props_attr), + ) +del _default_params_entry class BIM_ADDON_preferences(bpy.types.AddonPreferences): @@ -828,36 +822,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_default_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: box = layout.box() - bonsai.bim.helper.draw_expandable_panel( - box, - context, - "Door", - lambda _layout, _context: draw_door_properties(_layout, self.default_parameters.door), - ) - bonsai.bim.helper.draw_expandable_panel( - box, - context, - "Window", - lambda _layout, _context: draw_window_properties(_layout, self.default_parameters.window), - ) - bonsai.bim.helper.draw_expandable_panel( - box, - context, - "Railing", - lambda _layout, _context: draw_railing_properties(_layout, self.default_parameters.railing), - ) - bonsai.bim.helper.draw_expandable_panel( - box, - context, - "Roof", - lambda _layout, _context: draw_roof_properties(_layout, self.default_parameters.roof), - ) - bonsai.bim.helper.draw_expandable_panel( - box, - context, - "Stair", - lambda _layout, _context: draw_stair_properties(_layout, self.default_parameters.stair), - ) + for entry in tool.Parametric.EDIT_TYPES: + if not entry.has_default_parameters: + continue + props = getattr(self.default_parameters, entry.name) + draw_props = getattr(_model_ui, f"draw_{entry.name}_properties") + bonsai.bim.helper.draw_expandable_panel( + box, + context, + entry.name.replace("_", " ").title(), + lambda _layout, _context, _draw=draw_props, _props=props: _draw(_layout, _props), + ) def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "opening_focus_opacity") diff --git a/src/bonsai/bonsai/tool/parametric.py b/src/bonsai/bonsai/tool/parametric.py index 313133f192..623405fe16 100644 --- a/src/bonsai/bonsai/tool/parametric.py +++ b/src/bonsai/bonsai/tool/parametric.py @@ -81,11 +81,19 @@ class ParametricObject: ``_cancel_targets``) and that therefore wire their operators through ``build_edit_lifecycle``. Entries with bespoke edit lifecycles (per-attribute diff dispatch, layer-stack editing, mid-spline gizmo drag) leave this - False and declare their operator classes directly.""" + False and declare their operator classes directly. + + ``has_default_parameters`` marks entries whose ``BIMProperties`` + class exposes ``get_general_kwargs`` / ``copy_to`` and a matching + ``draw__properties`` UI helper, so the addon-preferences panel can + surface a per-type defaults section and the create operator can seed new + instances from the preset. Entries without that machinery leave this False + and don't appear in the preferences ``Default Parameters`` panel.""" name: str has_non_editable_path: bool = False supports_build_edit_lifecycle: bool = False + has_default_parameters: bool = False def __post_init__(self) -> None: if not _VALID_NAME_RE.match(self.name): @@ -148,11 +156,17 @@ class Parametric(bonsai.core.tool.Parametric): self._gen = None EDIT_TYPES: list[ParametricObject] = [ - ParametricObject("door", has_non_editable_path=True, supports_build_edit_lifecycle=True), - ParametricObject("window", has_non_editable_path=True, supports_build_edit_lifecycle=True), - ParametricObject("stair", has_non_editable_path=True, supports_build_edit_lifecycle=True), - ParametricObject("railing", supports_build_edit_lifecycle=True), - ParametricObject("roof", supports_build_edit_lifecycle=True), + ParametricObject( + "door", has_non_editable_path=True, supports_build_edit_lifecycle=True, has_default_parameters=True + ), + ParametricObject( + "window", has_non_editable_path=True, supports_build_edit_lifecycle=True, has_default_parameters=True + ), + ParametricObject( + "stair", has_non_editable_path=True, supports_build_edit_lifecycle=True, has_default_parameters=True + ), + ParametricObject("railing", supports_build_edit_lifecycle=True, has_default_parameters=True), + ParametricObject("roof", supports_build_edit_lifecycle=True, has_default_parameters=True), ParametricObject("array", supports_build_edit_lifecycle=True), ParametricObject("pipe_segment", supports_build_edit_lifecycle=True), ParametricObject("duct_segment", supports_build_edit_lifecycle=True), diff --git a/src/bonsai/test/bim/test_parametric_registry.py b/src/bonsai/test/bim/test_parametric_registry.py index 1c1e3ec3f7..09a2f9175d 100644 --- a/src/bonsai/test/bim/test_parametric_registry.py +++ b/src/bonsai/test/bim/test_parametric_registry.py @@ -123,6 +123,31 @@ def test_every_predicate_does_not_raise_on_non_matching_element(registry): ) +def test_default_parameters_field_per_registry_entry_with_defaults(registry): + """Every entry flagged ``has_default_parameters=True`` must have a matching + ``: PointerProperty`` field on ``ui.DefaultParameters`` pointing at + its ``BIMProperties`` class. + + The addon-preferences ``Default Parameters`` panel iterates flagged entries + to render per-type defaults sections, and the matching create operator + (``bim.add_door``, ``bim.add_window``, …) reads the field to seed new + instances from the user's preset values. A missing field means the preset + silently never reaches the operator. + + Entries WITHOUT the flag are not required to appear — the contract is + one-directional: ``has_default_parameters=True`` implies a field, but + ``False`` allows absence.""" + from bonsai.bim import ui + + annotations = getattr(ui.DefaultParameters, "__annotations__", {}) + missing = [e.name for e in registry if e.has_default_parameters and e.name not in annotations] + assert not missing, ( + f"ui.DefaultParameters missing PointerProperty field(s) for: {missing} — " + f"each EDIT_TYPES entry with has_default_parameters=True must have a matching " + f": PointerProperty(type=BIMProperties) field" + ) + + def test_gizmo_preferences_field_per_registry_entry(registry): """Every registry entry must have a matching ``: BoolProperty`` field on ``ui.GizmoPreferences`` so the addon-preferences UI auto-renders a