mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-08 00:41:36 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7864657f88 | |||
| 8d617c87c8 | |||
| 1b2ad6c93f | |||
| ffe9f65b3c | |||
| 5a8f557e7f | |||
| 5b56093147 | |||
| bcb63f450b | |||
| e4c82e2c92 | |||
| 8fb10d2ff3 | |||
| 37d29e5d47 | |||
| 7221e72cfa | |||
| c828c1b08d | |||
| fcf5614004 | |||
| 5a4d60e08c | |||
| a675365dfd | |||
| 5ab6deb121 | |||
| cbd26896ba | |||
| ceeca752ea | |||
| 9d437b5bc3 |
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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():
|
||||
|
||||
+1397
-221
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user