Compare commits

...

19 Commits

Author SHA1 Message Date
Ryan Schultz 7864657f88 Fix #6334: Preserve annotation vertex order during subdivision to prevent arrow/text flip 2025-11-30 15:53:42 -06:00
Ryan Schultz 8d617c87c8 Extend github.com/IfcOpenShell/IfcOpenShell/commit/1b2ad6c93f633df46d55f65bf97e730461fd7e6c - apply new type to all the selected objects. 2025-11-29 15:53:08 -06:00
Ryan Schultz 1b2ad6c93f Add interactive dialog for type duplication with assignment option
Add dialog to DuplicateType operator allowing users to set name, description,
and optionally assign the active object to the duplicated type. Add duplicate
button to product UI in type panel with auto-assignment enabled by default.
2025-11-29 15:13:38 -06:00
Gorgious ffe9f65b3c Refactor and improve parametric gizmo system
- You can now input values using the keyboard once you clicked on a gizmo
- Gizmos now support click > move > click  in addition to drag and drop (yay carpal tunnel !)
- Optimize snapping performance
- Consolidate door and window type in model.py
- You can now cycle through window and door types using the cycle gizmo
- Pen, validate, cancel, lock, and cycle gizmos are now billboards and follow view direction
- Draggable gizmos are now billboard 2D arrows instead of 3D cones
2025-11-28 23:22:42 +01:00
Ryan Schultz 5a8f557e7f Fix #7398: move purge funtions with other purge functions 2025-11-28 10:41:34 -06:00
Andrej730 5b56093147 black . 2025-11-28 21:03:50 +05:00
Andrej730 bcb63f450b build-deps - make rocksdb installation minimal 2025-11-28 21:03:50 +05:00
Andrej730 e4c82e2c92 FindOpenCASCADE - log found config 2025-11-28 21:03:50 +05:00
Esteban DUGUEPEROUX 8fb10d2ff3 cmake: Use OBJECT target instead of STATIC one to avoid mix static and shared targets in a same build 2025-11-28 17:03:25 +01:00
Esteban DUGUEPEROUX 37d29e5d47 cmake: Use same PRIVATE scope for Serializers target 2025-11-28 16:17:41 +01:00
Diego Mateos 7221e72cfa fix some target folders to respect parent cmake 2025-11-28 14:11:57 +01:00
Gorgious c828c1b08d Refactor parametric element gizmos
- Add gizmos for windows and stairs
- Add BaseParametricGizmoGroup mixin for doors, windows, and stairs
- Improve snapping with screen-space distance calculation
- Add GizmoPlus, GizmoMinus, GizmoCycle icon gizmos
- Add GizmoPropConfig dataclass for cleaner gizmo configuration
2025-11-27 22:56:27 +01:00
Ryan Schultz fcf5614004 Is user saving .blend file? 2025-11-27 09:50:31 -06:00
myoualid 5a4d60e08c util functions to retrieve material profiles and material layers for an element 2025-11-27 13:30:38 +01:00
Bruno Postle a675365dfd Correct fix for OCC built with VTK support 2025-11-26 22:53:38 +00:00
Bruno Postle 5ab6deb121 Revert cbd2689 Propagate CMP0167 policy 2025-11-25 23:08:52 +00:00
Bruno Postle cbd26896ba Propagate CMP0167 policy to fix cmake warnings 2025-11-25 18:45:59 +00:00
Bruno Postle ceeca752ea Fix build error from OCC with VTK support
OpenCASCADE may be built with VTK support. Try to find VTK first to avoid
CMake errors when OpenCASCADE's config references VTK targets.
2025-11-25 18:44:32 +00:00
Thomas Krijnen 9d437b5bc3 Mark simple type instance file ownership #7226 2025-11-25 09:19:28 +01:00
26 changed files with 2664 additions and 487 deletions
+3 -6
View File
@@ -595,13 +595,13 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
foreach(schema ${SCHEMA_VERSIONS})
set(GEOM_SERIALIZER_SCHEMA_LIBRARIES ${GEOM_SERIALIZER_SCHEMA_LIBRARIES} GeometrySerializers_ifc${schema})
add_library(geometry_serializer_ifc${schema} STATIC ../src/ifcgeom/Serialization/schema/Serialization.cpp)
add_library(geometry_serializer_ifc${schema} OBJECT ../src/ifcgeom/Serialization/schema/Serialization.cpp)
set_target_properties(geometry_serializer_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_serializer_ifc${schema} ${OpenCASCADE_LIBRARIES})
list(APPEND geometry_serializer_libraries geometry_serializer_ifc${schema})
endforeach()
add_library(geometry_serializer STATIC ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
add_library(geometry_serializer OBJECT ../src/ifcgeom/Serialization/Serialization.h ../src/ifcgeom/Serialization/Serialization.cpp)
set_target_properties(geometry_serializer PROPERTIES COMPILE_FLAGS "-DIFC_GEOMSERIALIZATION_EXPORTS")
target_link_libraries(geometry_serializer ${geometry_serializer_libraries} IfcParse)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} geometry_serializer ${geometry_serializer_libraries})
@@ -636,12 +636,9 @@ if(BUILD_IFCGEOM)
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/mapping/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES} ${IFCGEOM_I_FILES})
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
add_library(geometry_mapping_ifc${schema} OBJECT ${IFCGEOM_FILES})
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
if (NOT BUILD_SHARED_LIBS)
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
endif()
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach()
+6
View File
@@ -29,7 +29,13 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
# find_package creates variables:
# - `OpenCASCADE_INCLUDE_DIR`
# - `OpenCASCADE_LIBRARIES`
# OpenCASCADE may be built with VTK support. Try to find VTK first to avoid
# CMake errors when OpenCASCADE's config references VTK targets.
find_package(VTK QUIET)
find_package(OpenCASCADE CONFIG REQUIRED)
message(STATUS "Found OpenCASCADE config: ${OpenCASCADE_DIR}")
return()
endif()
+20 -1
View File
@@ -98,7 +98,7 @@ def initialize_bbim_semver():
def get_debug_info():
bbim_version = bbim_semver["version"]
return {
debug_info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
@@ -113,6 +113,25 @@ def get_debug_info():
"last_error": last_error,
}
# Add .blend file save information
if bpy.data.is_saved:
debug_info["blend_file_path"] = bpy.data.filepath
debug_info["blend_file_dirty"] = bpy.data.is_dirty
else:
debug_info["blend_file_path"] = "Not saved"
debug_info["blend_file_dirty"] = "N/A"
# Add IFC file information
bim_props = tool.Blender.get_bim_props()
if bim_props.ifc_file:
debug_info["ifc_file_path"] = bim_props.ifc_file
debug_info["ifc_is_dirty"] = bim_props.is_dirty
else:
debug_info["ifc_file_path"] = "No IFC loaded"
debug_info["ifc_is_dirty"] = "N/A"
return debug_info
def format_debug_info(info: dict):
last_actions = ""
+7
View File
@@ -149,6 +149,8 @@ classes = [
ui.BIM_UL_generic,
ui.DocPreferences,
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
ui.GizmoPreferencesStair, # Register before GizmoPreferences
ui.GizmoPreferences,
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
# Tabs panel
@@ -205,13 +207,18 @@ classes = [
ui.BIM_PT_decorators_overlay,
ui.BIM_PT_snappping,
# Gizmos
gizmo.BIM_OT_gizmo_value_input,
gizmo.GizmoArrow,
gizmo.GizmoArrow2D,
gizmo.GizmoCone,
gizmo.GizmoLock,
gizmo.GizmoArc,
gizmo.GizmoPen,
gizmo.GizmoValidate,
gizmo.GizmoCancel,
gizmo.GizmoPlus,
gizmo.GizmoMinus,
gizmo.GizmoCycle,
]
for mod in modules.values():
File diff suppressed because it is too large Load Diff
-17
View File
@@ -109,23 +109,6 @@ class BIM_PT_debug(Panel):
row.prop(props, "display_type", text="")
row.operator("bim.override_display_type").display = props.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
layout.label(text="Inspector:")
row = layout.row(align=True)
@@ -2409,6 +2409,14 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Geometry.get_geometry_props()
item = tool.Geometry.get_active_representation(obj)
assert item
# Fix vertex order for annotation items
rep_obj = props.representation_obj
if rep_obj:
element = tool.Ifc.get_entity(rep_obj)
if element and self._is_annotation_object(element):
tool.Geometry.ensure_annotation_vertex_order(obj)
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.is_geometric_data(obj.data) and (
item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons
@@ -2548,6 +2556,31 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
props.mode = "EDIT"
props.is_changing_mode = False
def _is_annotation_object(self, element: ifcopenshell.entity_instance) -> bool:
"""Check if element is an annotation object that needs vertex order preservation."""
if not element.is_a("IfcAnnotation"):
return False
# Object types that have semantic vertex order (arrow direction, text position)
annotation_types_with_order = {
"TEXT_LEADER",
"DIMENSION",
"RADIUS",
"DIAMETER",
"ANGLE",
"FALL",
"SLOPE_ANGLE",
"SLOPE_FRACTION",
"SLOPE_PERCENT",
"STAIR_ARROW",
"PLAN_LEVEL",
"SECTION_LEVEL",
"SECTION",
"ELEVATION",
}
return element.ObjectType in annotation_types_with_order
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.direct_profile_edit"
@@ -3374,6 +3407,17 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
tool.Root.reload_item_decorator()
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcAnnotation") and element.ObjectType in {
"TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE",
"FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT",
"STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION"
}:
for item_obj_data in props.item_objs:
item_obj = item_obj_data.obj
if item_obj and isinstance(item_obj.data, bpy.types.Mesh) and item_obj.data.vertices:
item_obj.data["bonsai_first_vert_co"] = item_obj.data.vertices[0].co[:]
class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_item_attributes"
@@ -161,6 +161,10 @@ classes = (
stair.FinishEditingStair,
stair.EnableEditingStair,
stair.RemoveStair,
stair.ToggleStairTotalLengthLock,
stair.AdjustStairTreads,
stair.CycleStairType,
stair.GizmoStairEdition,
sverchok_modifier.CreateNewSverchokGraph,
sverchok_modifier.UpdateDataFromSverchok,
sverchok_modifier.DeleteSverchokGraph,
@@ -172,6 +176,8 @@ classes = (
window.FinishEditingWindow,
window.EnableEditingWindow,
window.RemoveWindow,
window.CycleWindowType,
window.GizmoWindowEdition,
door.BIM_OT_add_door,
door.AddDoor,
door.CancelEditingDoor,
@@ -179,6 +185,7 @@ classes = (
door.EnableEditingDoor,
door.RemoveDoor,
door.ToggleDoorSwing,
door.CycleDoorType,
door.GizmoDoorEdition,
railing.BIM_OT_add_railing,
railing.CopyRailingParameters,
+164 -193
View File
@@ -33,14 +33,15 @@ import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.root
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
import math
from mathutils import Vector, Matrix
from typing import Union, Any, Optional
import json
import collections
import collections.abc
from typing import get_args
V_ = tool.Blender.V_
@@ -193,8 +194,8 @@ def bm_mirror(
def create_bm_extruded_profile(
bm: bmesh.types.BMesh,
points: list[Vector],
edges: Optional[list[tuple[int, int]]] = None,
faces: Optional[list[list[int]]] = None,
edges: list[tuple[int, int]] | None = None,
faces: list[list[int]] | None = None,
position: Vector = V_(0, 0, 0).freeze(),
magnitude: float = 1.0,
extrusion_vector: Vector = V_(0, 0, 1).freeze(),
@@ -473,6 +474,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
@@ -684,12 +686,75 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
"""Toggle door swing direction between LEFT and RIGHT"""
"""Toggle door swing direction and optionally flip door geometry.
Shift+Click (when flip_geometry=True): Flip geometry only without changing door direction"""
bl_idname = "bim.toggle_door_swing"
bl_label = "Toggle Door Swing"
bl_options = {"REGISTER", "UNDO"}
flip_geometry: bpy.props.BoolProperty(name="Flip Geometry", default=False)
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
skip_direction_change: bpy.props.BoolProperty(
name="Skip Direction Change", default=False, options={"HIDDEN", "SKIP_SAVE"}
)
def invoke(self, context, event):
self.skip_direction_change = event.shift
return self.execute(context)
def _toggle_swing_direction(self, obj: bpy.types.Object) -> bool:
"""Toggle door swing direction between LEFT and RIGHT."""
props = tool.Model.get_door_props(obj)
current_type = props.door_type
if "LEFT" in current_type:
props.door_type = current_type.replace("LEFT", "RIGHT")
return True
elif "RIGHT" in current_type:
props.door_type = current_type.replace("RIGHT", "LEFT")
return True
return False
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
return {"CANCELLED"}
is_door = tool.Blender.Modifier.is_door(element)
if self.flip_geometry:
tool.Geometry.flip_object(obj, self.flip_local_axes)
if not self.skip_direction_change and is_door:
self._toggle_swing_direction(obj)
elif is_door:
self._toggle_swing_direction(obj)
else:
return {"CANCELLED"}
return {"FINISHED"}
class CycleDoorType(bpy.types.Operator, tool.Ifc.Operator):
"""Cycle through available door types. Shift+click to cycle in reverse."""
bl_idname = "bim.cycle_door_type"
bl_label = "Cycle Door Type"
bl_options = {"REGISTER", "UNDO"}
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
def invoke(self, context, event):
self.reverse = event.shift
return self.execute(context)
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
@@ -700,88 +765,72 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"}
props = tool.Model.get_door_props(obj)
current_type = props.door_type
if "LEFT" in current_type:
props.door_type = current_type.replace("LEFT", "RIGHT")
elif "RIGHT" in current_type:
props.door_type = current_type.replace("RIGHT", "LEFT")
door_types = get_args(tool.Model.DoorType)
current_index = door_types.index(props.door_type) if props.door_type in door_types else 0
direction = -1 if self.reverse else 1
next_index = (current_index + direction) % len(door_types)
props.door_type = door_types[next_index]
return {"FINISHED"}
class GizmoDoorEdition(bpy.types.GizmoGroup):
class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_door_edition"
bl_label = "Door Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
COLOR_RED = (1.0, 0.2, 0.2)
COLOR_GREEN = (0.1, 0.8, 0.1)
COLOR_BLUE = (0.2, 0.2, 1.0)
ARROW_SCALE = 0.25
@classmethod
def get_arrow_color_from_axis(cls, axis):
"""Get arrow color based on axis direction (X=red, Y=green, Z=blue)."""
if axis[0] != 0:
return cls.COLOR_RED
elif axis[1] != 0:
return cls.COLOR_GREEN
else:
return cls.COLOR_BLUE
enable_editing_operator = "bim.enable_editing_door"
finish_editing_operator = "bim.finish_editing_door"
cancel_editing_operator = "bim.cancel_editing_door"
cycle_type_operator = "bim.cycle_door_type"
gizmo_props = [
{"attr_name": "overall_height", "axis": (0, 0, 1)},
{"attr_name": "overall_width", "axis": (1, 0, 0)},
{"attr_name": "threshold_thickness", "axis": (0, 0, 1)},
{"attr_name": "threshold_depth", "axis": (0, 1, 0)},
{"attr_name": "lining_depth", "axis": (0, 1, 0)},
{"attr_name": "lining_thickness", "axis": (1, 0, 0)},
{"attr_name": "transom_offset", "axis": (0, 0, 1)},
{"attr_name": "transom_thickness", "axis": (0, 0, 1)},
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("threshold_thickness", (0, 0, 1)),
GizmoPropConfig("threshold_depth", (0, 1, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (-1, 0, 0)),
GizmoPropConfig("transom_offset", (0, 0, 1)),
GizmoPropConfig("transom_thickness", (0, 0, 1)),
GizmoPropConfig("casing_thickness", (-1, 0, 0)),
GizmoPropConfig("casing_depth", (0, 1, 0)),
]
@classmethod
def poll(cls, context):
"""Show gizmo only when a single door object is selected and active."""
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_door(element)
def get_props(self, obj):
return tool.Model.get_door_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
if not prefs.gizmos.draw_gizmos_in_3d_viewport:
return False
return prefs.gizmos.door
obj = tool.Blender.get_active_object(is_selected=True)
if not obj:
return False
if len(tool.Blender.get_selected_objects()) != 1:
return False
element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_door(element):
return False
return True
def get_axis_rotation_matrix(self, axis):
"""Get rotation matrix to align arrow (default +X direction) with the given axis.
Args:
axis: Tuple of (x, y, z) representing the target axis direction
Returns:
Rotation matrix to align arrow with the axis
"""
axis_vec = V_(*axis).normalized()
default_dir = V_(1, 0, 0)
return default_dir.rotation_difference(axis_vec).to_matrix().to_4x4()
def should_hide_gizmo(self, attr_name, props):
"""Door-specific visibility rules for gizmos."""
if not props.is_editing:
return True
if attr_name == "threshold_depth" and props.threshold_thickness == 0.0:
return True
if attr_name == "transom_offset" and props.transom_thickness == 0.0:
return True
if attr_name == "casing_thickness" and props.lining_offset != 0.0:
return True
if attr_name == "casing_depth" and (props.lining_offset != 0.0 or props.casing_thickness == 0.0):
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, 0.0, props.overall_height))
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, 0.0, props.overall_height - 0.05))
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
@@ -789,7 +838,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
translation = Matrix.Translation(
V_(
props.overall_width / 2,
props.threshold_offset + props.threshold_depth / 2,
props.threshold_offset + props.threshold_depth / 2 + props.lining_offset,
props.threshold_thickness,
)
)
@@ -798,7 +847,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
def get_gizmo_matrix_threshold_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.threshold_depth, props.threshold_thickness / 2)
V_(props.overall_width / 2, props.threshold_depth + props.lining_offset, props.threshold_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
@@ -812,146 +861,90 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation(
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
V_(props.overall_width - props.lining_thickness, props.lining_depth / 2, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_offset(self, props):
translation = Matrix.Translation(V_(props.overall_width / 2, 0.0, props.transom_offset))
# Position at the bottom of the transom (transom extends upward from offset)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.transom_offset)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
# Position at the top of the transom (offset + thickness)
translation = Matrix.Translation(
V_(props.overall_width / 2, 0.0, props.transom_offset + props.transom_thickness)
V_(props.overall_width / 2, props.lining_offset, props.transom_offset + props.transom_thickness)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
@staticmethod
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
"""Get common base position for casing gizmos."""
x = -props.casing_thickness + props.lining_thickness
y_base = props.lining_depth + props.lining_offset
z = props.overall_height / 2
return x, y_base, z
def get_gizmo_matrix_casing_thickness(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth / 2, z))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_casing_depth(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth, z))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Door-specific: swing arc gizmos
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
inactive_color = prefs.decorator_color_background[:3]
special_color = prefs.decorator_color_special[:3]
def add_gizmo_prop(prop_config):
attr_name = prop_config["attr_name"]
gizmo = self.gizmos.new("BIM_GT_gizmo_cone")
def move_get():
obj = bpy.context.active_object
if not obj:
return 0.0
props = tool.Model.get_door_props(obj)
return getattr(props, attr_name)
def move_set(value):
obj = bpy.context.active_object
if not obj:
return
props = tool.Model.get_door_props(obj)
setattr(props, attr_name, max(0.0, value))
# Set callbacks on the gizmo instance
gizmo.move_get_cb = move_get
gizmo.move_set_cb = move_set
gizmo.axis = V_(*prop_config["axis"])
gizmo.local_axis = V_(*prop_config["axis"])
gizmo.color = self.get_arrow_color_from_axis(prop_config["axis"])
gizmo.color_highlight = highlight_color
gizmo.alpha = 0.99
gizmo.use_draw_modal = True
gizmo.use_draw_scale = True
setattr(self, f"gizmo_{attr_name}", gizmo)
for prop_config in self.gizmo_props:
add_gizmo_prop(prop_config)
self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_door_type.use_draw_scale = False
self.gizmo_door_type.color = special_color
self.gizmo_door_type.alpha = 0.5
self.gizmo_door_type.color_highlight = highlight_color
self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type"
self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = False
# Flip arc gizmo - mirrors the swing arc along X axis, flips door when clicked
# Flip arc gizmo - mirrors the swing arc along X axis, flips door and toggles direction when clicked
self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_flip_arc.use_draw_scale = False
self.gizmo_flip_arc.color = inactive_color
self.gizmo_flip_arc.alpha = 0.5
self.gizmo_flip_arc.color_highlight = highlight_color
self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type"
op = self.gizmo_flip_arc.target_set_operator("bim.flip_object")
op = self.gizmo_flip_arc.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = True
op.flip_local_axes = "XY"
self.pen_gizmo = self.gizmos.new("VIEW3D_GT_pen")
self.pen_gizmo.use_draw_scale = False
self.pen_gizmo.color = default_color
self.pen_gizmo.color_highlight = highlight_color
self.pen_gizmo.alpha = 0.8
self.pen_gizmo.target_set_operator("bim.enable_editing_door")
self.validate_gizmo = self.gizmos.new("VIEW3D_GT_validate")
self.validate_gizmo.use_draw_scale = False
self.validate_gizmo.color = self.COLOR_GREEN
self.validate_gizmo.color_highlight = highlight_color
self.validate_gizmo.alpha = 0.8
self.validate_gizmo.target_set_operator("bim.finish_editing_door")
self.cancel_gizmo = self.gizmos.new("VIEW3D_GT_cancel")
self.cancel_gizmo.use_draw_scale = False
self.cancel_gizmo.color = self.COLOR_RED
self.cancel_gizmo.color_highlight = highlight_color
self.cancel_gizmo.alpha = 0.8
self.cancel_gizmo.target_set_operator("bim.cancel_editing_door")
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = tool.Model.get_door_props(obj)
props = self.get_props(obj)
mw = obj.matrix_world
self.update_arrows(mw, props)
self.update_swing_gizmo(mw, props)
self.update_editing_gizmos(mw, props)
self.update_property_gizmos(mw, props)
self.update_swing_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
def update_arrows(self, mw, props):
"""Update arrow gizmos position and visibility based on editing state."""
prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door
for prop_config in self.gizmo_props:
attr_name = prop_config["attr_name"]
gizmo = getattr(self, f"gizmo_{attr_name}", None)
if gizmo is None:
continue
if not getattr(door_gizmo_prefs, attr_name, True):
gizmo.hide = True
continue
if (
not props.is_editing
or (attr_name == "threshold_depth" and props.threshold_thickness == 0.0)
or (attr_name == "transom_offset" and props.transom_thickness == 0.0)
):
gizmo.hide = True
continue
# Update position and show arrow when editing
gizmo.hide = False
matrix_method = getattr(self, f"get_gizmo_matrix_{attr_name}", None)
if matrix_method:
gizmo.matrix_basis = mw @ matrix_method(props)
gizmo.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
def update_swing_gizmo(self, mw, props):
def update_swing_gizmos(self, mw, props):
"""Update swing gizmo position and color based on editing state."""
prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door
@@ -965,37 +958,15 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
# Calculate base swing transformation (common to both arcs)
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
base_swing_transform = Matrix.Translation(V_(swing_x_offset, 0, 0)) @ Matrix.Scale(props.overall_width, 4)
base_swing_transform = Matrix.Translation(V_(swing_x_offset, props.lining_offset, 0)) @ Matrix.Scale(
props.overall_width, 4
)
if not self.gizmo_door_type.hide:
self.gizmo_door_type.matrix_basis = mw @ base_swing_transform
self.gizmo_door_type.color = prefs.decorations_colour[:3]
if not self.gizmo_flip_arc.hide:
mirror_x = Matrix.Scale(-1, 4, (0, 1, 0))
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_x
def update_editing_gizmos(self, mw, props):
"""Update editing control gizmos (pen/validate/cancel) visibility and position."""
icon_z = props.overall_height + 0.5
local_transform = (
Matrix.Translation(V_(0, 0.0, icon_z))
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.5, 4)
)
icon_matrix_base = mw @ local_transform
if props.is_editing:
self.pen_gizmo.hide = True
self.validate_gizmo.hide = False
self.validate_gizmo.matrix_basis = icon_matrix_base
self.cancel_gizmo.hide = False
cancel_local = Matrix.Translation(V_(0.5, 0.0, 0.0)) @ local_transform
self.cancel_gizmo.matrix_basis = mw @ cancel_local
else:
self.pen_gizmo.hide = False
self.pen_gizmo.matrix_basis = icon_matrix_base
self.validate_gizmo.hide = True
self.cancel_gizmo.hide = True
# Mirror the flip arc along Y axis to show the opposite swing direction
mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
+17 -34
View File
@@ -217,7 +217,12 @@ class BIMModelProperties(PropertyGroup):
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
x_angle: bpy.props.FloatProperty(
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
name="X Angle",
default=0,
subtype="ANGLE",
min=math.radians(-180),
max=math.radians(180),
update=update_x_angle,
)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
@@ -666,19 +671,6 @@ def window_type_prop_update(self, context):
update_window(self, context)
WindowType = Literal[
"SINGLE_PANEL",
"DOUBLE_PANEL_HORIZONTAL",
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_VERTICAL",
]
# default prop values are in mm and converted later
class BIMWindowProperties(PropertyGroup):
non_si_units_props = (
@@ -707,7 +699,7 @@ class BIMWindowProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
window_type: bpy.props.EnumProperty(
name="Window Type",
items=[(i, i, "") for i in get_args(WindowType)],
items=[(i, i, "") for i in get_args(tool.Model.WindowType)],
default="SINGLE_PANEL",
update=window_type_prop_update,
)
@@ -717,9 +709,11 @@ class BIMWindowProperties(PropertyGroup):
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window)
# lining properties
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_window)
lining_depth: bpy.props.FloatProperty(
name="Lining Depth", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
lining_thickness: bpy.props.FloatProperty(
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_window
name="Lining Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
lining_offset: bpy.props.FloatProperty(
name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window
@@ -748,12 +742,13 @@ class BIMWindowProperties(PropertyGroup):
update=update_window,
)
transom_thickness: bpy.props.FloatProperty(
name="Transom Thickness", default=0.050, subtype="DISTANCE", update=update_window
name="Transom Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
first_transom_offset: bpy.props.FloatProperty(
name="First Transom Offset",
description="Distance from the first lining to the first transom center",
default=0.3,
min=0.001,
subtype="DISTANCE",
update=update_window,
)
@@ -780,7 +775,7 @@ class BIMWindowProperties(PropertyGroup):
if TYPE_CHECKING:
is_editing: bool
window_type: WindowType
window_type: tool.Model.WindowType
overall_height: float
overall_width: float
lining_depth: float
@@ -814,7 +809,7 @@ class BIMWindowProperties(PropertyGroup):
return tool.Model.convert_data_to_project_units(kwargs, ["window_type"])
def get_lining_kwargs(
self, window_type: Optional[WindowType] = None, convert_to_project_units: bool = False
self, window_type: Optional[tool.Model.WindowType] = None, convert_to_project_units: bool = False
) -> dict[str, Any]:
if not window_type:
window_type = self.window_type
@@ -881,18 +876,6 @@ class BIMWindowProperties(PropertyGroup):
setattr(target_props, prop_name, prop_value)
DoorType = Literal[
"SINGLE_SWING_LEFT",
"SINGLE_SWING_RIGHT",
"DOUBLE_SWING_LEFT",
"DOUBLE_SWING_RIGHT",
"DOUBLE_DOOR_SINGLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
]
class BIMDoorProperties(PropertyGroup):
non_si_units_props = (
"is_editing",
@@ -905,7 +888,7 @@ class BIMDoorProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
door_type: bpy.props.EnumProperty(
name="Door Operation Type",
items=tuple((i, i, "") for i in get_args(DoorType)),
items=tuple((i, i, "") for i in get_args(tool.Model.DoorType)),
default="SINGLE_SWING_LEFT",
update=update_door,
)
@@ -1049,7 +1032,7 @@ class BIMDoorProperties(PropertyGroup):
if TYPE_CHECKING:
is_editing: bool
door_type: DoorType
door_type: tool.Model.DoorType
overall_height: float
overall_width: float
+390 -1
View File
@@ -19,6 +19,7 @@
import bpy
import json
import bmesh
import math
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.element
@@ -26,11 +27,14 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.core.root
import bonsai.tool as tool
from mathutils import Vector
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from mathutils import Vector, Matrix
from bmesh.types import BMVert
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from typing import get_args
def regenerate_stair_mesh(obj: bpy.types.Object) -> None:
@@ -293,3 +297,388 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
class ToggleStairTotalLengthLock(bpy.types.Operator):
"""Toggle the total length lock for stair editing"""
bl_idname = "bim.toggle_stair_total_length_lock"
bl_label = "Toggle Stair Total Length Lock"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
props.total_length_lock = not props.total_length_lock
return {"FINISHED"}
class AdjustStairTreads(bpy.types.Operator):
"""Adjust the number of treads"""
bl_idname = "bim.adjust_stair_treads"
bl_label = "Adjust Stair Treads"
bl_options = {"REGISTER", "UNDO"}
increment: IntProperty(name="Increment", default=1)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
new_value = props.number_of_treads + self.increment
if new_value >= 1:
props.number_of_treads = new_value
return {"FINISHED"}
class CycleStairType(bpy.types.Operator):
"""Cycle through stair types. Shift+click to cycle in reverse."""
bl_idname = "bim.cycle_stair_type"
bl_label = "Cycle Stair Type"
bl_options = {"REGISTER", "UNDO"}
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
def invoke(self, context, event):
self.reverse = event.shift
return self.execute(context)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
stair_types = get_args(tool.Model.StairType)
current_idx = stair_types.index(props.stair_type) if props.stair_type in stair_types else 0
direction = -1 if self.reverse else 1
props.stair_type = stair_types[(current_idx + direction) % len(stair_types)]
return {"FINISHED"}
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_stair_edition"
bl_label = "Stair Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
# Gizmo layout offsets (meters)
GIZMO_X_OFFSET = 0.5 # Horizontal offset from stair end
GIZMO_PLUS_X_OFFSET = 0.25 # Additional X offset for plus button
GIZMO_MINUS_X_OFFSET = 0.5 # Additional X offset for minus button
GIZMO_BUTTON_Z_OFFSET = 0.15 # Vertical offset for +/- buttons above lock
GIZMO_CYCLE_Z_OFFSET = 0.5 # Vertical offset for cycle gizmo above lock
enable_editing_operator = "bim.enable_editing_stair"
finish_editing_operator = "bim.finish_editing_stair"
cancel_editing_operator = "bim.cancel_editing_stair"
gizmo_props = [
GizmoPropConfig("width", (0, 1, 0)),
GizmoPropConfig("height", (0, 0, 1)),
GizmoPropConfig("tread_run", (1, 0, 0)),
GizmoPropConfig("tread_depth", (0, 0, -1)),
GizmoPropConfig("nosing_length", (-1, 0, 0)),
GizmoPropConfig("nosing_depth", (0, 0, -1)),
GizmoPropConfig("total_length_target", (1, 0, 0)),
GizmoPropConfig("base_slab_depth", (0, 0, -1)),
GizmoPropConfig("top_slab_depth", (0, 0, -1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_stair(element)
def get_props(self, obj):
return tool.Model.get_stair_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.stair
def should_hide_gizmo(self, attr_name, props):
"""Stair-specific visibility rules for gizmos."""
if not props.is_editing:
return True
# Hide concrete-specific gizmos for non-concrete stairs
if attr_name in ("base_slab_depth", "top_slab_depth") and props.stair_type != "CONCRETE":
return True
# Hide tread_depth for generic stairs (has no tread geometry)
if attr_name == "tread_depth" and props.stair_type == "GENERIC":
return True
# Hide nosing_depth when nosing_length is 0 or for Wood/Steel stair types
if attr_name == "nosing_depth" and (props.nosing_length == 0.0 or props.stair_type == "WOOD/STEEL"):
return True
return False
@staticmethod
def _get_stair_total_run(props) -> float:
"""Calculate the total horizontal run of the stair."""
return props.tread_run * props.number_of_treads
@staticmethod
def _get_first_riser_height(props) -> float:
"""Calculate the height of the first riser."""
return props.height / (props.number_of_treads + 1)
def get_gizmo_matrix_width(self, props):
translation = Matrix.Translation(Vector((0, props.width, 0)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_width_top(self, props):
"""Position for the secondary width gizmo at the top of the stairs."""
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width, props.height)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_height(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_tread_run(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((props.tread_run, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_tread_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(
Vector((props.tread_run / 2, props.width / 2, riser_height - props.tread_depth))
)
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_nosing_length(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_nosing_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(
Vector((-props.nosing_length, props.width / 2, riser_height - props.nosing_depth))
)
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_total_length_target(self, props):
translation = Matrix.Translation(Vector((props.total_length_target, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_base_slab_depth(self, props):
translation = Matrix.Translation(Vector((0, props.width / 2, -props.base_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_top_slab_depth(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height - props.top_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def setup(self, context):
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Stair-specific gizmos
prefs = tool.Blender.get_addon_preferences()
highlight_color = prefs.decorator_color_selected[:3]
# Secondary width gizmo at the top of the stairs (linked to same width property)
self.gizmo_width_top = self.gizmos.new("BIM_GT_gizmo_arrow_2d")
def make_width_get():
def move_get():
obj = bpy.context.active_object
if not obj:
return 0.0
props = self.get_props(obj)
return props.width
return move_get
def make_width_set():
def move_set(value):
obj = bpy.context.active_object
if not obj:
return
props = self.get_props(obj)
props.width = max(0.0, value)
return move_set
self.gizmo_width_top.move_get_cb = make_width_get()
self.gizmo_width_top.move_set_cb = make_width_set()
self.gizmo_width_top.axis = Vector((0, 1, 0))
self.gizmo_width_top.local_axis = Vector((0, 1, 0))
self.gizmo_width_top.invert_delta = False
self.gizmo_width_top.delta_scale = 1.0
self.gizmo_width_top.prop_name = "Width"
self.gizmo_width_top.gizmo_group = self
self.gizmo_width_top.color = self.COLOR_GREEN
self.gizmo_width_top.color_highlight = highlight_color
self.gizmo_width_top.alpha = 0.99
self.gizmo_width_top.use_draw_modal = True
self.gizmo_width_top.use_draw_scale = True
# Total length lock gizmo
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
self.lock_gizmo.use_draw_scale = False
self.lock_gizmo.color = self.COLOR_BLUE
self.lock_gizmo.color_highlight = highlight_color
self.lock_gizmo.alpha = 0.8
self.lock_gizmo.prop_path = "BIMStairProperties.total_length_lock"
self.lock_gizmo.target_set_operator("bim.toggle_stair_total_length_lock")
# Plus gizmo for increasing treads
self.plus_gizmo = self.gizmos.new("VIEW3D_GT_plus")
self.plus_gizmo.use_draw_scale = False
self.plus_gizmo.color = self.COLOR_GREEN
self.plus_gizmo.color_highlight = highlight_color
self.plus_gizmo.alpha = 0.8
op = self.plus_gizmo.target_set_operator("bim.adjust_stair_treads")
op.increment = 1
# Minus gizmo for decreasing treads
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
self.minus_gizmo.use_draw_scale = False
self.minus_gizmo.color = self.COLOR_RED
self.minus_gizmo.color_highlight = highlight_color
self.minus_gizmo.alpha = 0.8
op = self.minus_gizmo.target_set_operator("bim.adjust_stair_treads")
op.increment = -1
# Cycle gizmo for stair type
default_color = prefs.decorations_colour[:3]
self.cycle_gizmo = self.gizmos.new("VIEW3D_GT_cycle")
self.cycle_gizmo.use_draw_scale = False
self.cycle_gizmo.color = default_color
self.cycle_gizmo.color_highlight = highlight_color
self.cycle_gizmo.alpha = 0.8
self.cycle_gizmo.target_set_operator("bim.cycle_stair_type")
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
billboard_rot = gizmo.get_billboard_rotation(context)
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
self.update_width_top_gizmo(mw, props)
self.update_lock_gizmo(mw, props, billboard_rot)
self.update_tread_count_gizmos(mw, props, billboard_rot)
self.update_cycle_gizmo(mw, props, billboard_rot)
def update_width_top_gizmo(self, mw, props):
"""Update the secondary width gizmo at the top of the stairs."""
gizmo_prefs = self.get_gizmo_prefs()
# Use same visibility as the main width gizmo
self.gizmo_width_top.hide = not props.is_editing or not getattr(gizmo_prefs, "width", True)
if self.gizmo_width_top.hide:
return
self.gizmo_width_top.matrix_basis = mw @ self.get_gizmo_matrix_width_top(props)
self.gizmo_width_top.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
def update_lock_gizmo(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
if self.lock_gizmo.hide:
return
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
total_run = self._get_stair_total_run(props)
lock_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
local_transform = (
Matrix.Translation(Vector((lock_x, props.width / 2, props.height)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.lock_gizmo.matrix_basis = mw @ local_transform
def update_tread_count_gizmos(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
if self.plus_gizmo.hide and self.minus_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
base_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
if not self.plus_gizmo.hide:
plus_local_transform = (
Matrix.Translation(Vector((
base_x + self.GIZMO_PLUS_X_OFFSET,
props.width / 2,
props.height + self.GIZMO_BUTTON_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.plus_gizmo.matrix_basis = mw @ plus_local_transform
if not self.minus_gizmo.hide:
minus_local_transform = (
Matrix.Translation(Vector((
base_x + self.GIZMO_MINUS_X_OFFSET,
props.width / 2,
props.height + self.GIZMO_BUTTON_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.minus_gizmo.matrix_basis = mw @ minus_local_transform
def update_cycle_gizmo(self, mw, props, billboard_rot):
gizmo_prefs = self.get_gizmo_prefs()
self.cycle_gizmo.hide = not props.is_editing or not gizmo_prefs.cycle
if self.cycle_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
cycle_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
local_transform = (
Matrix.Translation(Vector((
cycle_x,
props.width / 2,
props.height + self.GIZMO_CYCLE_Z_OFFSET
)))
@ billboard_rot
@ Matrix.Scale(0.2, 4)
)
self.cycle_gizmo.matrix_basis = mw @ local_transform
def draw_prepare(self, context):
"""Called before drawing - updates gizmos to face camera."""
# Call base class implementation
super().draw_prepare(context)
# Also update the secondary width gizmo to face camera
if hasattr(self, "gizmo_width_top") and not self.gizmo_width_top.hide:
self.gizmo_width_top.draw_prepare(context)
+233 -4
View File
@@ -28,6 +28,8 @@ import ifcopenshell.api.pset
import bonsai.tool as tool
import bonsai.core.root
import bonsai.core.geometry
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
import ifcopenshell.api
import ifcopenshell.api.material
@@ -36,8 +38,8 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
from bmesh.types import BMVert
from mathutils import Vector
from typing import Optional, Union
from mathutils import Vector, Matrix
from typing import get_args
V_ = tool.Blender.V_
@@ -139,7 +141,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
def create_bm_window_frame(
bm: bmesh.types.BMesh, size: Vector, thickness: Union[float, list[float]], position: Vector = V_(0, 0, 0).freeze()
bm: bmesh.types.BMesh, size: Vector, thickness: float | list[float], position: Vector = V_(0, 0, 0).freeze()
) -> list[bmesh.types.BMVert]:
"""`thickness` of the profile is defined as list in the following order:
`(LEFT, TOP, RIGHT, BOTTOM)`
@@ -226,7 +228,7 @@ def create_bm_window(
frame_thickness: float,
glass_thickness: float,
position: Vector,
x_offsets: Optional[list] = None,
x_offsets: list | None = None,
) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMVert], list[bmesh.types.BMVert]]:
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
similarly to `create_bm_window_frame` `thickness` argument"""
@@ -391,6 +393,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
@@ -575,3 +578,229 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator):
"""Cycle through available window types."""
bl_idname = "bim.cycle_window_type"
bl_label = "Cycle Window Type"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_window(element):
return {"CANCELLED"}
props = tool.Model.get_window_props(obj)
window_types = list(get_args(tool.Model.WindowType))
current_index = window_types.index(props.window_type) if props.window_type in window_types else 0
next_index = (current_index + 1) % len(window_types)
props.window_type = window_types[next_index]
return {"FINISHED"}
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_window_edition"
bl_label = "Window Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
enable_editing_operator = "bim.enable_editing_window"
finish_editing_operator = "bim.finish_editing_window"
cancel_editing_operator = "bim.cancel_editing_window"
cycle_type_operator = "bim.cycle_window_type"
gizmo_props = [
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_x", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_y", (0, 1, 0)),
GizmoPropConfig("mullion_thickness", (1, 0, 0), delta_scale=2.0),
GizmoPropConfig("first_mullion_offset", (1, 0, 0)),
GizmoPropConfig("second_mullion_offset", (1, 0, 0)),
GizmoPropConfig("transom_thickness", (0, 0, 1), delta_scale=2.0),
GizmoPropConfig("first_transom_offset", (0, 0, 1)),
GizmoPropConfig("second_transom_offset", (0, 0, 1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_window(element)
def get_props(self, obj):
return tool.Model.get_window_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.window
def should_hide_gizmo(self, attr_name, props):
"""Window-specific visibility rules for gizmos."""
if not props.is_editing:
return True
has_mullion = self._has_mullion(props)
has_second_mullion = self._has_second_mullion(props)
has_transom = self._has_transom(props)
has_second_transom = self._has_second_transom(props)
if attr_name == "mullion_thickness" and not has_mullion:
return True
if attr_name == "first_mullion_offset" and not has_mullion:
return True
if attr_name == "second_mullion_offset" and not has_second_mullion:
return True
if attr_name == "transom_thickness" and not has_transom:
return True
if attr_name == "first_transom_offset" and not has_transom:
return True
if attr_name == "second_transom_offset" and not has_second_transom:
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation(
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
@staticmethod
def _get_lining_to_panel_offset_y_full(props) -> float:
"""Get the full Y offset for lining-to-panel positioning."""
return (props.lining_depth - props.frame_depth[0]) + props.lining_to_panel_offset_y
def get_gizmo_matrix_lining_to_panel_offset_x(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(props.lining_to_panel_offset_x, y_full + props.lining_offset, props.lining_thickness)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_to_panel_offset_y(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(
props.lining_to_panel_offset_x,
y_full + props.frame_depth[0] + props.lining_offset,
props.lining_thickness,
)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_mullion_thickness(self, props):
# Position at the right edge of the mullion (offset + thickness/2) so gizmo movement matches property 1:1
translation = Matrix.Translation(
V_(props.first_mullion_offset + props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_first_mullion_offset(self, props):
# Position at the left edge of the mullion (offset - thickness/2)
translation = Matrix.Translation(
V_(props.first_mullion_offset - props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_second_mullion_offset(self, props):
translation = Matrix.Translation(V_(props.second_mullion_offset, props.lining_offset, props.overall_height / 2))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
# Position at the top edge of the transom (offset + thickness/2) so gizmo movement matches property 1:1
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset + props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_first_transom_offset(self, props):
# Position at the bottom edge of the transom (offset - thickness/2)
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset - props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_second_transom_offset(self, props):
translation = Matrix.Translation(V_(props.overall_width / 2, props.lining_offset, props.second_transom_offset))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def _has_mullion(self, props):
"""Check if the window type uses mullions (vertical dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_VERTICAL",
)
def _has_second_mullion(self, props):
"""Check if the window type uses a second mullion."""
return props.window_type == "TRIPLE_PANEL_VERTICAL"
def _has_transom(self, props):
"""Check if the window type uses transoms (horizontal dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
)
def _has_second_transom(self, props):
"""Check if the window type uses a second transom."""
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(context, mw, props)
@@ -635,6 +635,7 @@ class BIM_PT_purge(Panel):
bl_parent_id = "BIM_PT_tab_quality_control"
def draw(self, context):
props = tool.Debug.get_debug_props()
layout = self.layout
layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
@@ -655,3 +656,23 @@ class BIM_PT_purge(Panel):
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
merge_op.object_type = object_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and (data := context.active_object.data):
mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
+41 -1
View File
@@ -322,6 +322,15 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Duplicate Type"
bl_options = {"REGISTER", "UNDO"}
element: bpy.props.IntProperty()
name: bpy.props.StringProperty(name="Name")
description: bpy.props.StringProperty(name="Description")
assign_selected_objects: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
element: int
name: str
description: str
assign_selected_objects: bool
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
@@ -332,8 +341,25 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
if obj.data:
new_obj.data = obj.data.copy()
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
new.Name += " Copy"
# Apply the name and description from the dialog
new.Name = self.name
if self.description:
new.Description = self.description
bpy.ops.bim.load_type_thumbnails()
# Assign selected objects to the new type if requested
if self.assign_selected_objects:
selected_objects = tool.Blender.get_selected_objects()
prefs = tool.Blender.get_addon_preferences()
for selected_obj in selected_objects:
selected_element = tool.Ifc.get_entity(selected_obj)
if selected_element and selected_element.is_a("IfcObject"):
core.assign_type(tool.Ifc, tool.Type, element=selected_element, type=new)
if prefs.occurrence_name_style == "TYPE":
selected_obj.name = tool.Model.generate_occurrence_name(new, selected_element.is_a())
if obj in context.selectable_objects:
tool.Blender.select_and_activate_single_object(context, new_obj)
else:
@@ -347,3 +373,17 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
props.ifc_class = new.is_a()
props.relating_type_id = str(tool.Blender.get_ifc_definition_id(new_obj))
return {"FINISHED"}
def invoke(self, context, event):
element = tool.Ifc.get().by_id(self.element)
self.name = (element.Name or "Unnamed") + " Copy"
self.description = element.Description or ""
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
self.layout.prop(self, "name")
self.layout.prop(self, "description")
selected_objects = tool.Blender.get_selected_objects()
ifc_objects = [obj for obj in selected_objects if tool.Ifc.get_entity(obj) and tool.Ifc.get_entity(obj).is_a("IfcObject")]
if ifc_objects:
self.layout.prop(self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type")
+4
View File
@@ -96,6 +96,10 @@ class BIM_PT_type(Panel):
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
op.relating_type = 0 # will only select the relating types of only the selected objects
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
# Add duplicate button here with assign_selected_objects enabled
op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="")
op.element = TypeData.data["relating_type"]["id"]
op.assign_selected_objects = True
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_type", icon="X", text="")
else:
+100 -1
View File
@@ -253,6 +253,8 @@ class GizmoPreferencesDoor(bpy.types.PropertyGroup):
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
transom_offset: BoolProperty(name="Transom Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
casing_thickness: BoolProperty(name="Casing Thickness", default=True)
casing_depth: BoolProperty(name="Casing Depth", default=True)
swing_arc: BoolProperty(name="Swing Arc", default=True, description="Show door swing direction arc")
flip_arc: BoolProperty(name="Flip Arc", default=True, description="Show flip door orientation arc")
@@ -265,10 +267,76 @@ class GizmoPreferencesDoor(bpy.types.PropertyGroup):
lining_thickness: bool
transom_offset: bool
transom_thickness: bool
casing_thickness: bool
casing_depth: bool
swing_arc: bool
flip_arc: bool
class GizmoPreferencesWindow(bpy.types.PropertyGroup):
"""Property group for window gizmo visibility settings."""
overall_height: BoolProperty(name="Overall Height", default=True)
overall_width: BoolProperty(name="Overall Width", default=True)
lining_depth: BoolProperty(name="Lining Depth", default=True)
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
lining_to_panel_offset_x: BoolProperty(name="Lining to Panel Offset X", default=True)
lining_to_panel_offset_y: BoolProperty(name="Lining to Panel Offset Y", default=True)
mullion_thickness: BoolProperty(name="Mullion Thickness", default=True)
first_mullion_offset: BoolProperty(name="First Mullion Offset", default=True)
second_mullion_offset: BoolProperty(name="Second Mullion Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
first_transom_offset: BoolProperty(name="First Transom Offset", default=True)
second_transom_offset: BoolProperty(name="Second Transom Offset", default=True)
if TYPE_CHECKING:
overall_height: bool
overall_width: bool
lining_depth: bool
lining_thickness: bool
lining_to_panel_offset_x: bool
lining_to_panel_offset_y: bool
mullion_thickness: bool
first_mullion_offset: bool
second_mullion_offset: bool
transom_thickness: bool
first_transom_offset: bool
second_transom_offset: bool
class GizmoPreferencesStair(bpy.types.PropertyGroup):
"""Property group for stair gizmo visibility settings."""
width: BoolProperty(name="Width", default=True)
height: BoolProperty(name="Height", default=True)
tread_run: BoolProperty(name="Tread Run", default=True)
tread_depth: BoolProperty(name="Tread Depth", default=True)
nosing_length: BoolProperty(name="Nosing Length", default=True)
nosing_depth: BoolProperty(name="Nosing Depth", default=True)
total_length_target: BoolProperty(name="Total Length Target", default=True)
base_slab_depth: BoolProperty(name="Base Slab Depth", default=True)
top_slab_depth: BoolProperty(name="Top Slab Depth", default=True)
lock: BoolProperty(name="Total Length Lock", default=True)
plus: BoolProperty(name="Add Tread (+)", default=True)
minus: BoolProperty(name="Remove Tread (-)", default=True)
cycle: BoolProperty(name="Cycle Stair Type", default=True)
if TYPE_CHECKING:
width: bool
height: bool
tread_run: bool
tread_depth: bool
nosing_length: bool
nosing_depth: bool
total_length_target: bool
base_slab_depth: bool
top_slab_depth: bool
lock: bool
plus: bool
minus: bool
cycle: bool
class GizmoPreferences(bpy.types.PropertyGroup):
"""Property group for all gizmo visibility settings."""
@@ -278,10 +346,14 @@ class GizmoPreferences(bpy.types.PropertyGroup):
description="Show interactive gizmos in the 3D viewport for parametric elements",
)
door: bpy.props.PointerProperty(type=GizmoPreferencesDoor)
window: bpy.props.PointerProperty(type=GizmoPreferencesWindow)
stair: bpy.props.PointerProperty(type=GizmoPreferencesStair)
if TYPE_CHECKING:
draw_gizmos_in_3d_viewport: bool
door: GizmoPreferencesDoor
window: GizmoPreferencesWindow
stair: GizmoPreferencesStair
class DocPreferences(bpy.types.PropertyGroup):
@@ -681,13 +753,40 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
)
def draw_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.label(text="Toggle visibility of gizmos in editing mode")
box = layout.box()
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Window", self.draw_window_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Stair", self.draw_stair_gizmo_parameters)
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.door import GizmoDoorEdition
door_gizmos = self.gizmos.door
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.gizmo_props}
gizmo_prop_names.update(("swing_arc", "flip_arc"))
for prop in door_gizmos.__annotations__:
layout.prop(door_gizmos, prop)
if prop in gizmo_prop_names:
layout.prop(door_gizmos, prop)
def draw_window_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.window import GizmoWindowEdition
window_gizmos = self.gizmos.window
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.gizmo_props}
for prop in window_gizmos.__annotations__:
if prop in gizmo_prop_names:
layout.prop(window_gizmos, prop)
def draw_stair_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.stair import GizmoStairEdition
stair_gizmos = self.gizmos.stair
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.gizmo_props}
special_gizmo_names = {"lock", "plus", "minus"}
for prop in stair_gizmos.__annotations__:
if prop in gizmo_prop_names or prop in special_gizmo_names:
layout.prop(stair_gizmos, prop)
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "occurrence_name_style")
+102
View File
@@ -1904,6 +1904,17 @@ class Geometry(bonsai.core.tool.Geometry):
obj.data.transform(transformation_i)
cls.record_object_position(obj)
# ADD THIS AT THE END - Store initial vertex order for annotations
if rep_obj and (element := tool.Ifc.get_entity(rep_obj)):
if element.is_a("IfcAnnotation") and element.ObjectType in {
"TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE",
"FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT",
"STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION"
}:
# Store the initial first vertex position
if isinstance(obj.data, bpy.types.Mesh) and obj.data.vertices:
obj.data["bonsai_first_vert_co"] = obj.data.vertices[0].co[:]
@classmethod
def disable_item_mode(cls) -> None:
props = tool.Geometry.get_geometry_props()
@@ -2340,3 +2351,94 @@ class Geometry(bonsai.core.tool.Geometry):
continue
objects.add(obj)
return objects
@classmethod
def ensure_annotation_vertex_order(cls, obj: bpy.types.Object) -> None:
"""
Ensure vertices form a continuous path from start to end.
Uses the original first vertex position as a reference point.
"""
mesh = obj.data
if not isinstance(mesh, bpy.types.Mesh):
return
# Get the original first vertex position from custom properties
if "bonsai_first_vert_co" in mesh:
original_first_co = Vector(mesh["bonsai_first_vert_co"])
else:
# Store it for next time
if mesh.vertices:
original_first_co = Vector(mesh.vertices[0].co)
mesh["bonsai_first_vert_co"] = original_first_co[:]
else:
return
bm = bmesh.new()
bm.from_mesh(mesh)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
if len(bm.verts) == 0:
bm.free()
return
# Find endpoints (vertices with only one connected edge)
endpoints = [v for v in bm.verts if len(v.link_edges) == 1]
# Choose the endpoint closest to the original first vertex position
if len(endpoints) == 0:
# Closed loop - pick any vertex as start
start_vert = bm.verts[0]
elif len(endpoints) == 1:
# Single endpoint
start_vert = endpoints[0]
else:
# Choose endpoint closest to where the original first vertex was
start_vert = min(endpoints, key=lambda v: (v.co - original_first_co).length)
# Build ordered vertex list by following edges
ordered_verts = [start_vert]
current_vert = start_vert
visited_edges = set()
while True:
# Find next unvisited edge
next_edge = None
for edge in current_vert.link_edges:
if edge not in visited_edges:
next_edge = edge
break
if not next_edge:
break
visited_edges.add(next_edge)
next_vert = next_edge.other_vert(current_vert)
# Avoid going back on ourselves
if next_vert not in ordered_verts:
ordered_verts.append(next_vert)
current_vert = next_vert
# Store vertex coordinates in the correct order
new_verts_co = [v.co.copy() for v in ordered_verts]
# Update the stored first vertex position to the new first vertex
mesh["bonsai_first_vert_co"] = new_verts_co[0][:]
# Clear and rebuild mesh with correct vertex order
bm.clear()
# Create new vertices in order
new_verts = [bm.verts.new(co) for co in new_verts_co]
bm.verts.ensure_lookup_table()
# Create edges connecting consecutive vertices
for i in range(len(new_verts) - 1):
bm.edges.new([new_verts[i], new_verts[i + 1]])
# Write back to mesh
bm.to_mesh(mesh)
bm.free()
mesh.update()
+23
View File
@@ -1401,6 +1401,29 @@ class Model(bonsai.core.tool.Model):
StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"]
DoorType = Literal[
"SINGLE_SWING_LEFT",
"SINGLE_SWING_RIGHT",
"DOUBLE_SWING_LEFT",
"DOUBLE_SWING_RIGHT",
"DOUBLE_DOOR_SINGLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
]
WindowType = Literal[
"SINGLE_PANEL",
"DOUBLE_PANEL_HORIZONTAL",
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_VERTICAL",
]
@classmethod
def generate_stair_2d_profile(
cls,
+5 -1
View File
@@ -14,4 +14,8 @@ if(WITH_RELATIONSHIP_VALIDATION)
set_property(TARGET IfcConvert APPEND_STRING PROPERTY COMPILE_FLAGS " -DWITH_RELATIONSHIP_VALIDATION")
endif()
install(TARGETS IfcConvert)
install(TARGETS IfcConvert
ARCHIVE DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
LIBRARY DESTINATION ${LIBDIR}
)
+5 -1
View File
@@ -5,4 +5,8 @@ set(SOURCE_FILES ${CPP_FILES})
add_executable(IfcGeomServer ${SOURCE_FILES})
target_link_libraries(IfcGeomServer IfcGeom ${kernel_libraries} ${OpenCASCADE_LIBRARIES})
install(TARGETS IfcGeomServer)
install(TARGETS IfcGeomServer
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
+5 -1
View File
@@ -79,7 +79,11 @@ if(HAS_MAX)
set_target_properties(IfcMax_${max_year} PROPERTIES SUFFIX ".dli")
install(TARGETS IfcMax_${max_year} RUNTIME DESTINATION ${BINDIR})
install(TARGETS IfcMax_${max_year}
ARCHIVE DESTINATION ${LIBDIR}
LIBRARY DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
)
endforeach()
else()
message(STATUS "Autodesk 3ds Max SDK not found, is required to build IFCMax.")
@@ -35,6 +35,9 @@ MATERIAL_TYPE = Literal[
"IfcMaterialList",
]
PrioritisedLayer = namedtuple("PrioritisedLayer", "priority material thickness")
PrioritisedProfile = namedtuple("PrioritisedProfile", "priority material profile")
def get_pset(
element: ifcopenshell.entity_instance,
@@ -1845,3 +1848,48 @@ def has_openings(element: ifcopenshell.entity_instance) -> bool:
:return: True if element has openings.
"""
return bool(next(get_openings(element), False))
def get_material_layers(element: ifcopenshell.entity_instance) -> list[PrioritisedLayer]:
"""
Retrieves all material layers assigned to an element.
:param element: The IFC element
:return: A list of IfcMaterialLayer entities
Example:
.. code:: python
element = ifcopenshell.by_type("IfcWall")[0]
material_layers = ifcopenshell.util.element.get_material_layers(element)
"""
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if not material or not material.is_a("IfcMaterialLayerSet"):
return []
return [
PrioritisedLayer(getattr(layer, "Priority", 0) or 0, layer.Material, layer.LayerThickness)
for layer in material.MaterialLayers
]
def get_material_profiles(element: ifcopenshell.entity_instance) -> list[PrioritisedProfile]:
"""
Retrieves all material profiles assigned to an element.
:param element: The IFC element
:return: A list of IfcMaterialProfile entities
Example:
.. code:: python
element = ifcopenshell.by_type("IfcBeam")[0]
material_profiles = ifcopenshell.util.element.get_material_profiles(element)
"""
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if not material or not material.is_a("IfcMaterialProfileSet"):
return []
return [
PrioritisedProfile(
getattr(material_profile, "Priority", 0) or 0, material_profile.Material, material_profile.Profile
)
for material_profile in material.MaterialProfiles
]
+5
View File
@@ -1607,6 +1607,11 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::FileRead
// Move the storage of simple type instances so that they are retained during the lifetime of the file
read_simple_type_instances = streamer.stealInstances();
// Set file ownership on simple type instances, so that when adding them to other files, proper copies are created
for (auto& inst : read_simple_type_instances) {
inst->file_ = file;
}
Logger::Status("\rDone scanning file ");
delete tokens;
+6 -2
View File
@@ -17,9 +17,13 @@ if(WITH_PROJ)
target_link_libraries(Serializers PRIVATE proj::proj)
endif()
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES} IfcGeom ${OPENCASCADE_LIBRARIES} ${kernel_libraries} IfcParse)
target_link_libraries(Serializers PRIVATE ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES} IfcGeom ${OPENCASCADE_LIBRARIES} ${kernel_libraries} IfcParse)
install(TARGETS Serializers)
install(TARGETS Serializers
ARCHIVE DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
LIBRARY DESTINATION ${LIBDIR}
)
install(FILES ${SERIALIZERS_H_FILES}
DESTINATION ${INCLUDEDIR}/serializers/
@@ -3,7 +3,7 @@ file(GLOB SERIALIZERS_S_CPP_FILES *.cpp)
set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
add_library(Serializers_ifc${schema} OBJECT ${SERIALIZERS_S_FILES})
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} Serializers_ifc${schema})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if(WASM_BUILD)
@@ -12,8 +12,12 @@ foreach(schema ${SCHEMA_VERSIONS})
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
endif()
install(TARGETS Serializers_ifc${schema})
install(TARGETS Serializers_ifc${schema}
ARCHIVE DESTINATION ${LIBDIR}
RUNTIME DESTINATION ${BINDIR}
LIBRARY DESTINATION ${LIBDIR}
)
endforeach()
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} PARENT_SCOPE)
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} PARENT_SCOPE)
+4
View File
@@ -717,6 +717,10 @@ call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\rocksdb" ^
-DROCKSDB_INSTALL_ON_WINDOWS=On ^
-DFAIL_ON_WARNINGS=Off ^
-DWITH_TESTS=OFF ^
-DWITH_TOOLS=OFF ^
-DWITH_BENCHMARK_TOOLS=OFF ^
-DWITH_CORE_TOOLS=OFF ^
-DROCKSDB_BUILD_SHARED=OFF ^
-DWITH_ZSTD=On ^
-DPORTABLE=1
IF NOT %ERRORLEVEL%==0 GOTO :Error