mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Add array parametric edit lifecycle + GizmoArrayEdition / Child
Ports the array parametric-edit lifecycle, gizmo group, child guard,
per-layer ARRAY entry icons, and the array bbox decorators
(preview + selection highlight + layer-children) from gizmos-8088.
Restores the array_gizmo icon's positioning + visibility in the
framework's parametric edit row.
Registry (tool/parametric.py):
* EDIT_TYPES adds ParametricObject("array", supports_build_edit_lifecycle=True).
_ArrayEditMixin in array.py feeds build_edit_lifecycle which auto-
generates EnableEditingArray / FinishEditingArray / CancelEditingArray
with the conventional bl_idnames the gizmo references.
tool/blender.py:
* Adds is_array predicate wrapper around tool.Parametric.is_array.
The registry contract test test_every_entry_has_modifier_predicate
enforces every EDIT_TYPES entry has a matching is_<name> wrapper on
tool.Blender.Modifier.
array.py (+1130 LOC port from gizmos-8088):
* _ArrayEditMixin(ParametricEditMixinBase) drives the auto-generated
enable / finish / cancel lifecycle.
* GizmoArrayEdition: validate + cancel + count display + +/- adjusters
+ method toggle + delete button + per-layer ARRAY entry icons
(preallocated pool of MAX_LAYER_GIZMOS=8).
* GizmoArrayChild: child-array gizmo for the array-replica case.
* EditArrayFromChild: resolves the spawning layer via
tool.Array.get_child_layer_index so clicking a child's array gizmo
opens the layer that produced that child rather than always layer 0
(the gizmos-8088 source itself hardcoded item=0; HEAD has the helper
to do it right).
* New operators: EnableEditingArrayItem, ArrayParentGizmoClick,
ArrayGizmoClick, ToggleArrayMethod, RemoveArrayLayerFromEdit,
InputArrayCount, AdjustArrayCount.
prop.py: BIMArrayProperties gets per_child_opening BoolProperty
(when the array parent fills a host, give each child its own
opening + filling pair).
Bug fix: guard update_relating_array_from_object against the
cleanup-time None set. _finish_one writes relating_array_object = None
to clear the source-array reference; that fired the update callback,
which dispatched bpy.ops.bim.enable_editing_array(item=self.is_editing).
With is_editing just flipped to False, the bool coerced to 0 and
re-opened layer-0 edit immediately after every validate. The guard
short-circuits on None; item is also fixed to 0 (the bool-as-layer-
index was always meaningless for the legitimate user-pick path).
decorator.py (+312 LOC, all ports from gizmos-8088):
* bbox_world_edges / draw_polyline_segments / _BBOX_EDGES - shared
geometry helpers usable across array decorators.
* draw_array_layer_children_bbox - green wireframe bbox per child of
one array layer, drawn inline from a gizmo's draw() so the highlight
tracks the hover cursor without POST_VIEW lag.
* ArrayPreviewDecorator - faint cyan ghost bboxes at each future
array instance during the edit lifecycle (offset math mirrors
Model.regenerate_array, gated on props.is_editing).
* ArraySelectionHighlightDecorator - bounding-box overlay surfacing
the array family of the selected object. Child selected -> parent
in special color + siblings in unselected color; parent selected
(idle) -> all children in unselected color. TokenCache-backed.
handler.py: imports + uninstall/install the 2 always-on decorators in
_install_viewport_overlays. Both self-poll, so installation has no
cost when no array is selected / in edit mode.
Registration (bim/module/model/__init__.py):
* Adds the 3 lifecycle classes generated by build_edit_lifecycle
(CancelEditingArray, EnableEditingArray, FinishEditingArray) -
they exist as module-level names but are only visible to Blender's
operator registry when included in the classes tuple.
* Adds the 8 new operators + 2 new gizmo groups in alphabetical order.
gizmos.py: restores the array_gizmo icon position + visibility block
in BaseParametricGizmoGroup.update_editing_gizmos. Was force-hidden
in c250b2c1a because no array gizmo existed; the icon's plumbing
comes back online now that GizmoArrayEdition is registered.
Verified by test/bim/test_parametric_registry.py: all 8 tests pass -
enable/finish/cancel ops resolve, PropertyGroup attached, is_array
predicate present, predicate is total on non-matching elements.
Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -43,6 +43,8 @@ from bonsai.bim.module.aggregate.decorator import AggregateDecorator
|
||||
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bonsai.bim.module.model.decorator import (
|
||||
ArrayPreviewDecorator,
|
||||
ArraySelectionHighlightDecorator,
|
||||
BoundingBoxDecorator,
|
||||
SlabDirectionDecorator,
|
||||
WallAxisDecorator,
|
||||
@@ -464,6 +466,8 @@ def _install_viewport_overlays() -> None:
|
||||
WallAxisDecorator.uninstall()
|
||||
SlabDirectionDecorator.uninstall()
|
||||
WallFilletPreviewDecorator.uninstall()
|
||||
ArrayPreviewDecorator.uninstall()
|
||||
ArraySelectionHighlightDecorator.uninstall()
|
||||
uninstall_decorator_cache_handlers()
|
||||
try:
|
||||
if georeference_props.should_visualise:
|
||||
@@ -482,6 +486,13 @@ def _install_viewport_overlays() -> None:
|
||||
# wall_fillet.is_active, so installation has no cost when no preview
|
||||
# is open. No corresponding addon-preference toggle.
|
||||
WallFilletPreviewDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on the active object's array
|
||||
# family membership, so installation has no cost when no array
|
||||
# element is selected.
|
||||
ArraySelectionHighlightDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on props.is_editing — only
|
||||
# paints during an active array edit lifecycle.
|
||||
ArrayPreviewDecorator.install(bpy.context)
|
||||
finally:
|
||||
install_decorator_cache_handlers()
|
||||
|
||||
|
||||
@@ -5916,14 +5916,25 @@ class BaseParametricGizmoGroup:
|
||||
billboard_rot=billboard_rot,
|
||||
scale=0.30,
|
||||
)
|
||||
# Array gizmo integration is in-progress: the icon binds to
|
||||
# bim.add_array_from_feature_edit but the array-from-parametric-draft
|
||||
# operator + per-feature gizmo positioning haven't fully landed.
|
||||
# Force-hide the icon while parametric-item editing is active to
|
||||
# keep the user from triggering a half-wired add-array flow. Drop
|
||||
# this gate when array integration completes.
|
||||
# ARRAY button sits past the last feature-specific icon. Each
|
||||
# gizmo group declares its own ``FEATURE_ICON_MAX_X`` (default
|
||||
# 0.87 past the cycle slot; wall / stair override it) so the
|
||||
# ARRAY button never lands on top of a rotate / tread-lock icon.
|
||||
if hasattr(self, "array_gizmo"):
|
||||
self.array_gizmo.hide = True
|
||||
self.array_gizmo.hide = self.is_gizmo_hidden_by_modal(self.array_gizmo)
|
||||
# 30% smaller than the editing-icon-row default (0.50 → 0.35):
|
||||
# the array button is a tertiary affordance compared to the
|
||||
# primary pen / validate / cancel triad, and the smaller
|
||||
# footprint keeps the edit-mode row from sprawling.
|
||||
self.set_icon_gizmo_position(
|
||||
"array_gizmo",
|
||||
mw=mw,
|
||||
x=self.ICON_VALIDATE_X + self.FEATURE_ICON_MAX_X + self.ICON_ARRAY_GAP,
|
||||
y=icon_y,
|
||||
z=icon_z,
|
||||
billboard_rot=billboard_rot,
|
||||
scale=0.35,
|
||||
)
|
||||
else:
|
||||
# ``hide_pen_button = True`` keeps the pen permanently hidden — for
|
||||
# groups whose edit-mode entry is already provided by another widget
|
||||
|
||||
@@ -50,19 +50,31 @@ from . import (
|
||||
|
||||
classes = (
|
||||
array.AddArray,
|
||||
array.CancelEditingArray,
|
||||
array.DisableEditingArray,
|
||||
array.EditArray,
|
||||
array.EnableEditingArray,
|
||||
array.EnableEditingArrayItem,
|
||||
array.FinishEditingArray,
|
||||
array.ApplyArray,
|
||||
array.RegenerateArray,
|
||||
array.RemoveArray,
|
||||
array.SelectAllArrayObjects,
|
||||
array.SelectArrayParent,
|
||||
array.ArrayParentGizmoClick,
|
||||
array.EditArrayFromChild,
|
||||
array.Input3DCursorXArray,
|
||||
array.Input3DCursorYArray,
|
||||
array.Input3DCursorZArray,
|
||||
array.EnableEditingParametric,
|
||||
array.AddArrayFromFeatureEdit,
|
||||
array.ArrayGizmoClick,
|
||||
array.ToggleArrayMethod,
|
||||
array.RemoveArrayLayerFromEdit,
|
||||
array.InputArrayCount,
|
||||
array.AdjustArrayCount,
|
||||
array.GizmoArrayEdition,
|
||||
array.GizmoArrayChild,
|
||||
product.AddDefaultType,
|
||||
product.AddEmptyType,
|
||||
product.AddOccurrence,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from math import cos, pi, radians, sin, tan
|
||||
from typing import Any, Literal
|
||||
@@ -41,6 +42,7 @@ from mathutils import Matrix, Quaternion, Vector
|
||||
|
||||
import bonsai.core.geometry
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.decorator_cache import TokenCache
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
|
||||
|
||||
@@ -2177,3 +2179,313 @@ class WallFilletPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
d1 = (p1.x - intersection[0]) ** 2 + (p1.y - intersection[1]) ** 2 + (p1.z - intersection[2]) ** 2
|
||||
d2 = (p2.x - intersection[0]) ** 2 + (p2.y - intersection[1]) ** 2 + (p2.z - intersection[2]) ** 2
|
||||
return p2 if d2 >= d1 else p1
|
||||
|
||||
|
||||
_BBOX_EDGES = (
|
||||
(0, 1), (1, 2), (2, 3), (3, 0),
|
||||
(4, 5), (5, 6), (6, 7), (7, 4),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7),
|
||||
) # fmt: skip
|
||||
|
||||
|
||||
def bbox_world_edges(
|
||||
obj: bpy.types.Object,
|
||||
) -> list[tuple[tuple[float, float, float], tuple[float, float, float]]]:
|
||||
"""Return world-space (start, end) tuples for the 12 edges of ``obj``'s
|
||||
bounding box. Empty list if the object has no bound_box (e.g. Empties)."""
|
||||
if not obj.bound_box:
|
||||
return []
|
||||
mw = obj.matrix_world
|
||||
corners = [mw @ Vector(c) for c in obj.bound_box]
|
||||
return [(tuple(corners[a]), tuple(corners[b])) for a, b in _BBOX_EDGES]
|
||||
|
||||
|
||||
def draw_polyline_segments(
|
||||
context: bpy.types.Context,
|
||||
segments: list[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||
color_rgb: tuple[float, float, float],
|
||||
alpha: float,
|
||||
line_width: float,
|
||||
) -> None:
|
||||
"""Render ``segments`` as one anti-aliased LINES batch in world space."""
|
||||
if not segments:
|
||||
return
|
||||
verts: list[tuple[float, float, float]] = []
|
||||
indices: list[tuple[int, int]] = []
|
||||
for start, end in segments:
|
||||
base = len(verts)
|
||||
verts.append(start)
|
||||
verts.append(end)
|
||||
indices.append((base, base + 1))
|
||||
if not tool.Blender.validate_shader_batch_data(verts, indices):
|
||||
return
|
||||
region = getattr(context, "region", None)
|
||||
if region is None:
|
||||
return
|
||||
shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
shader.bind()
|
||||
shader.uniform_float("viewportSize", (region.width, region.height))
|
||||
shader.uniform_float("lineWidth", line_width)
|
||||
shader.uniform_float("color", (*color_rgb, alpha))
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": verts}, indices=indices)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
batch.draw(shader)
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
_ARRAY_LAYER_BBOX_LINE_WIDTH = 1.8
|
||||
_ARRAY_LAYER_BBOX_LINE_ALPHA = 0.8
|
||||
_ARRAY_LAYER_BBOX_MAX_CHILDREN = 200
|
||||
|
||||
|
||||
def draw_array_layer_children_bbox(
|
||||
context: bpy.types.Context,
|
||||
parent_element: ifcopenshell.entity_instance,
|
||||
layer_index: int,
|
||||
max_children: int = _ARRAY_LAYER_BBOX_MAX_CHILDREN,
|
||||
) -> None:
|
||||
"""Paint a wireframe bbox around every child of one array layer in the
|
||||
same 3D pass. Called inline from gizmo ``draw()`` methods so the highlight
|
||||
tracks the hover cursor one-for-one — no POST_VIEW handler, no timing lag.
|
||||
|
||||
Total: silently no-ops on missing pset, unparseable JSON, out-of-range
|
||||
layer index, unresolvable child GUIDs, or empty child geometry."""
|
||||
if layer_index < 0:
|
||||
return
|
||||
data_text = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data")
|
||||
if not data_text:
|
||||
return
|
||||
try:
|
||||
layers = json.loads(data_text)
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
if layer_index >= len(layers):
|
||||
return
|
||||
child_guids = layers[layer_index].get("children", [])
|
||||
if not child_guids:
|
||||
return
|
||||
ifc_file = tool.Ifc.get()
|
||||
segments: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = []
|
||||
for guid in child_guids[:max_children]:
|
||||
try:
|
||||
child_element = ifc_file.by_guid(guid)
|
||||
except RuntimeError:
|
||||
continue
|
||||
child_obj = tool.Ifc.get_object(child_element)
|
||||
if child_obj is None:
|
||||
continue
|
||||
segments.extend(bbox_world_edges(child_obj))
|
||||
if not segments:
|
||||
return
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
color = prefs.decorator_color_special[:3]
|
||||
draw_polyline_segments(
|
||||
context,
|
||||
segments,
|
||||
color,
|
||||
_ARRAY_LAYER_BBOX_LINE_ALPHA,
|
||||
_ARRAY_LAYER_BBOX_LINE_WIDTH,
|
||||
)
|
||||
|
||||
|
||||
class ArrayPreviewDecorator(tool.Blender.ViewportDecorator):
|
||||
"""Faint bbox wireframe at each future array instance during the edit lifecycle.
|
||||
Pure GPU preview gated on the array's draft props — no IFC mutation."""
|
||||
|
||||
LINE_WIDTH = 1.2
|
||||
LINE_ALPHA = 0.45
|
||||
MAX_PREVIEW_INSTANCES = 200
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
if not tool.Blender.are_viewport_gizmos_enabled():
|
||||
return
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
obj = context.active_object
|
||||
if obj is None or not obj.bound_box:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Parametric.is_array(element):
|
||||
return
|
||||
props = tool.Model.get_array_props(obj)
|
||||
if not props.is_editing:
|
||||
return
|
||||
count = int(props.count)
|
||||
if count <= 1 or count > self.MAX_PREVIEW_INSTANCES:
|
||||
return
|
||||
|
||||
segments = self._compute_segments(obj, props, count)
|
||||
if not segments:
|
||||
return
|
||||
|
||||
color = prefs.decorator_color_selected[:3]
|
||||
draw_polyline_segments(context, segments, color, self.LINE_ALPHA, self.LINE_WIDTH)
|
||||
|
||||
def _compute_segments(
|
||||
self,
|
||||
parent_obj: bpy.types.Object,
|
||||
props,
|
||||
count: int,
|
||||
) -> list[tuple[tuple[float, float, float], tuple[float, float, float]]]:
|
||||
"""World-space (start, end) line segments for the bbox edges of
|
||||
every future instance (i = 1 … count-1; i = 0 is the parent itself).
|
||||
props.x/y/z are SI — the edit-lifecycle Enable hydrates them via
|
||||
si_conversion, so no unit_scale multiplier here."""
|
||||
offset = Vector((props.x, props.y, props.z))
|
||||
if props.method == "DISTRIBUTE":
|
||||
divider = (count - 1) if count > 1 else 1
|
||||
offset = offset / divider
|
||||
|
||||
parent_mw = parent_obj.matrix_world
|
||||
parent_corners = [Vector(c) for c in parent_obj.bound_box]
|
||||
segments: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = []
|
||||
for i in range(1, count):
|
||||
delta = offset * i
|
||||
child_mw = parent_mw.copy()
|
||||
if props.use_local_space:
|
||||
child_mw.translation = parent_mw @ delta
|
||||
else:
|
||||
child_mw.translation = parent_mw.translation + delta
|
||||
world_corners = [child_mw @ corner for corner in parent_corners]
|
||||
for a, b in _BBOX_EDGES:
|
||||
segments.append((tuple(world_corners[a]), tuple(world_corners[b])))
|
||||
return segments
|
||||
|
||||
|
||||
class ArraySelectionHighlightDecorator(tool.Blender.ViewportDecorator):
|
||||
"""Bounding-box overlay surfacing the array family of the selected object.
|
||||
|
||||
Two activation modes:
|
||||
|
||||
- **Child selected** — parent drawn in the addon's *special*
|
||||
decorator color (bright accent); other siblings in the *unselected*
|
||||
color at lower alpha so the parent stands out. The selected child
|
||||
itself keeps Blender's standard selection outline.
|
||||
- **Parent selected** (idle, not editing) — every existing child drawn
|
||||
in the *unselected* color at lower alpha. The parent is already
|
||||
visually flagged by Blender's selection outline. Suppressed during
|
||||
an active array edit lifecycle so the live preview wireframes don't
|
||||
double-draw with the existing-children overlay."""
|
||||
|
||||
LINE_WIDTH = 1.5
|
||||
PARENT_ALPHA = 0.7
|
||||
SIBLING_ALPHA = 0.35
|
||||
MAX_SIBLINGS = 200
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._family_cache: TokenCache = TokenCache()
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
if not tool.Blender.are_viewport_gizmos_enabled():
|
||||
return
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
obj = context.active_object
|
||||
if obj is None:
|
||||
return
|
||||
if not obj.select_get():
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
if tool.Blender.Modifier.is_array_child(element):
|
||||
self._draw_for_child(context, prefs, element, obj)
|
||||
elif tool.Parametric.is_array(element):
|
||||
props = tool.Model.get_array_props(obj)
|
||||
if not props.is_editing:
|
||||
self._draw_for_parent(context, prefs, element, obj)
|
||||
|
||||
def _draw_for_child(self, context, prefs, element, obj):
|
||||
family = self._resolve_family_for_child(obj, element)
|
||||
if family is None:
|
||||
return
|
||||
parent_obj, sibling_objs = family
|
||||
|
||||
parent_segments = bbox_world_edges(parent_obj)
|
||||
if parent_segments:
|
||||
draw_polyline_segments(
|
||||
context,
|
||||
parent_segments,
|
||||
prefs.decorator_color_special[:3],
|
||||
self.PARENT_ALPHA,
|
||||
self.LINE_WIDTH,
|
||||
)
|
||||
self._draw_siblings(context, prefs, sibling_objs)
|
||||
|
||||
def _draw_for_parent(self, context, prefs, element, obj):
|
||||
child_objs = self._resolve_children_for_parent(obj, element)
|
||||
self._draw_siblings(context, prefs, child_objs)
|
||||
|
||||
def _resolve_family_for_child(self, obj, element):
|
||||
return self._family_cache.get_or_compute(
|
||||
("child", obj.session_uid, element.id()),
|
||||
lambda: self._collect_family_from_child(element, obj),
|
||||
)
|
||||
|
||||
def _resolve_children_for_parent(self, obj, element):
|
||||
return (
|
||||
self._family_cache.get_or_compute(
|
||||
("parent", obj.session_uid, element.id()),
|
||||
lambda: self._collect_children(element, exclude=obj),
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
def _draw_siblings(self, context, prefs, sibling_objs):
|
||||
if not sibling_objs:
|
||||
return
|
||||
if len(sibling_objs) > self.MAX_SIBLINGS:
|
||||
sibling_objs = sibling_objs[: self.MAX_SIBLINGS]
|
||||
segments: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = []
|
||||
for sib_obj in sibling_objs:
|
||||
segments.extend(bbox_world_edges(sib_obj))
|
||||
draw_polyline_segments(
|
||||
context,
|
||||
segments,
|
||||
prefs.decorator_color_unselected[:3],
|
||||
self.SIBLING_ALPHA,
|
||||
self.LINE_WIDTH,
|
||||
)
|
||||
|
||||
def _collect_family_from_child(self, element, obj):
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
return None
|
||||
parent_guid = pset.get("Parent")
|
||||
if not parent_guid:
|
||||
return None
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(parent_guid)
|
||||
except RuntimeError:
|
||||
return None
|
||||
parent_obj = tool.Ifc.get_object(parent_element)
|
||||
if not parent_obj:
|
||||
return None
|
||||
siblings = self._collect_children(parent_element, exclude=obj, also_exclude=parent_obj)
|
||||
return parent_obj, siblings
|
||||
|
||||
def _collect_children(self, parent_element, exclude=None, also_exclude=None):
|
||||
parent_data_text = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data")
|
||||
if not parent_data_text:
|
||||
return []
|
||||
try:
|
||||
layers = json.loads(parent_data_text)
|
||||
except (ValueError, TypeError):
|
||||
return []
|
||||
children: list[bpy.types.Object] = []
|
||||
seen_ids: set[int] = set()
|
||||
if exclude is not None:
|
||||
seen_ids.add(id(exclude))
|
||||
if also_exclude is not None:
|
||||
seen_ids.add(id(also_exclude))
|
||||
for layer in layers:
|
||||
for child_guid in layer.get("children", []):
|
||||
try:
|
||||
child_element = tool.Ifc.get().by_guid(child_guid)
|
||||
except RuntimeError:
|
||||
continue
|
||||
child_obj = tool.Ifc.get_object(child_element)
|
||||
if child_obj is None or id(child_obj) in seen_ids:
|
||||
continue
|
||||
seen_ids.add(id(child_obj))
|
||||
children.append(child_obj)
|
||||
return children
|
||||
|
||||
@@ -103,8 +103,12 @@ def update_type_page(self: "BIMModelProperties", context: bpy.types.Context) ->
|
||||
|
||||
|
||||
def update_relating_array_from_object(self: "BIMArrayProperties", context: bpy.types.Context) -> None:
|
||||
bpy.ops.bim.enable_editing_array(item=self.is_editing)
|
||||
return
|
||||
# Skip the cleanup-time clear: Finish/Cancel sets relating_array_object back to None,
|
||||
# which has no source to hydrate from. Only the user-driven pick (None → some array)
|
||||
# should auto-enter edit on the picked source's layer 0.
|
||||
if self.relating_array_object is None:
|
||||
return
|
||||
bpy.ops.bim.enable_editing_array(item=0)
|
||||
|
||||
|
||||
def is_object_array_applicable(self: "BIMArrayProperties", obj: bpy.types.Object) -> bool:
|
||||
@@ -397,8 +401,13 @@ class BIMModelProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMArrayProperties(PropertyGroup):
|
||||
is_editing: bpy.props.IntProperty(
|
||||
default=-1, description="Currently edited array index. -1 if not in array editing mode."
|
||||
is_editing: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
description="True while an array layer is in parametric edit mode. The specific layer is in editing_item_index.",
|
||||
)
|
||||
editing_item_index: bpy.props.IntProperty(
|
||||
default=-1,
|
||||
description="Index of the array layer currently being edited; -1 when not in edit mode.",
|
||||
)
|
||||
count: bpy.props.IntProperty(name="Count", default=0, min=0)
|
||||
x: bpy.props.FloatProperty(name="X", default=0, subtype="DISTANCE")
|
||||
@@ -414,6 +423,15 @@ class BIMArrayProperties(PropertyGroup):
|
||||
name="Method",
|
||||
default="OFFSET",
|
||||
)
|
||||
per_child_opening: bpy.props.BoolProperty(
|
||||
name="Per-Child Opening",
|
||||
description=(
|
||||
"When the array parent fills a wall (or any voidable host), give each array child its own opening + "
|
||||
"filling pair so the host is cut once per child. Disable to leave the host uncut by the children — "
|
||||
"only the parent's original opening remains"
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
relating_array_object: bpy.props.PointerProperty(
|
||||
type=bpy.types.Object,
|
||||
name="Copy Array Properties",
|
||||
@@ -422,13 +440,15 @@ class BIMArrayProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: int
|
||||
is_editing: bool
|
||||
editing_item_index: int
|
||||
count: int
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
use_local_space: bool
|
||||
method: Literal["OFFSET", "DISTRIBUTE"]
|
||||
per_child_opening: bool
|
||||
sync_children: bool
|
||||
relating_array_object: Union[bpy.types.Object, None]
|
||||
|
||||
|
||||
@@ -1363,6 +1363,10 @@ class Blender(bonsai.core.tool.Blender):
|
||||
def is_window(cls, element: entity_instance) -> bool:
|
||||
return tool.Parametric.is_window(element)
|
||||
|
||||
@classmethod
|
||||
def is_array(cls, element: entity_instance) -> bool:
|
||||
return tool.Parametric.is_array(element)
|
||||
|
||||
class Array:
|
||||
@classmethod
|
||||
def bake_children_transform(cls, parent_element: ifcopenshell.entity_instance, item: int) -> None:
|
||||
|
||||
@@ -147,17 +147,17 @@ class Parametric(bonsai.core.tool.Parametric):
|
||||
self._data.clear()
|
||||
self._gen = None
|
||||
|
||||
# FIXME(PR4): array / pipe_segment / duct_segment land with their
|
||||
# finish/cancel operators in PR4. Adding them to EDIT_TYPES without those
|
||||
# operators makes auto-commit-on-save dispatch bim.finish_editing_<name>
|
||||
# for objects flagged as in-edit, which then raises because the operator
|
||||
# doesn't exist. PR4 re-adds the three entries together with the operators.
|
||||
# FIXME(PR5): pipe_segment / duct_segment land with their finish/cancel
|
||||
# operators in the MEP slice of PR5 (PR5d). Until then they stay out of
|
||||
# EDIT_TYPES so auto-commit-on-save doesn't try to dispatch a
|
||||
# non-existent operator.
|
||||
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("array", supports_build_edit_lifecycle=True),
|
||||
ParametricObject("wall"),
|
||||
]
|
||||
|
||||
@@ -169,6 +169,7 @@ class Parametric(bonsai.core.tool.Parametric):
|
||||
STAIR: ClassVar[ParametricObject]
|
||||
RAILING: ClassVar[ParametricObject]
|
||||
ROOF: ClassVar[ParametricObject]
|
||||
ARRAY: ClassVar[ParametricObject]
|
||||
WALL: ClassVar[ParametricObject]
|
||||
|
||||
_geom_generation: int = 0
|
||||
|
||||
Reference in New Issue
Block a user