mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ dependencies = [
|
||||
"black==25.12",
|
||||
"ruff==0.14.13",
|
||||
"poethepoet",
|
||||
"gersemi==0.24",
|
||||
"gersemi==0.25.1",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
|
||||
@@ -511,14 +511,15 @@ def draw_filter(
|
||||
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
|
||||
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
|
||||
row = layout.row(align=True)
|
||||
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if not preferences.chain_filter_with_set_operations:
|
||||
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
|
||||
else:
|
||||
if not filter_groups or not any(fg.filters for fg in filter_groups):
|
||||
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
|
||||
op.type = "entity"
|
||||
op.index = 0
|
||||
op.module = module
|
||||
row.prop(sprops, "facet", text="")
|
||||
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
|
||||
op.type = sprops.facet
|
||||
op.index = 0
|
||||
op.module = module
|
||||
op = row.operator("bim.edit_filter_query", text="", icon="FILTER")
|
||||
if "module" in op.bl_rna.properties:
|
||||
op.module = module
|
||||
@@ -526,26 +527,23 @@ def draw_filter(
|
||||
for i, filter_group in enumerate(filter_groups):
|
||||
box = layout.box()
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(sprops, "facet", text="")
|
||||
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
|
||||
op.type = sprops.facet
|
||||
op.index = i
|
||||
op.module = module
|
||||
op = row.operator("bim.remove_filter_group", text="", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if not preferences.chain_filter_with_set_operations:
|
||||
row = box.row(align=True)
|
||||
row.prop(sprops, "facet", text="")
|
||||
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
|
||||
op.type = sprops.facet
|
||||
op.index = i
|
||||
op.module = module
|
||||
op = row.operator("bim.remove_filter_group", text="", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
for j, ifc_filter in enumerate(filter_group.filters):
|
||||
if ifc_filter.type == "entity":
|
||||
row = box.row(align=True)
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
show_mode_toggle = j > 0
|
||||
else:
|
||||
show_mode_toggle = (
|
||||
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
|
||||
) # PR 7315 mode
|
||||
show_mode_toggle = preferences.chain_filter_with_set_operations and j > 0
|
||||
if show_mode_toggle:
|
||||
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
|
||||
op = row.operator(
|
||||
@@ -761,12 +759,7 @@ def draw_filter(
|
||||
elif ifc_filter.type == "instance":
|
||||
row = box.row(align=True)
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
show_mode_toggle = j > 0
|
||||
else:
|
||||
show_mode_toggle = (
|
||||
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
|
||||
) # PR 7315 mode
|
||||
show_mode_toggle = preferences.chain_filter_with_set_operations and j > 0
|
||||
if show_mode_toggle:
|
||||
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
|
||||
op = row.operator(
|
||||
|
||||
@@ -290,7 +290,6 @@ class IfcImporter:
|
||||
self.profile_code("Load linked models")
|
||||
self.add_project_to_scene()
|
||||
self.profile_code("Add project to scene")
|
||||
self.hide_ifc_spaces()
|
||||
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
||||
self.clean_mesh()
|
||||
self.profile_code("Mesh cleaning")
|
||||
@@ -1287,14 +1286,6 @@ class IfcImporter:
|
||||
properties={"Aggregate_Index": aggregate_index, "Name": name},
|
||||
)
|
||||
|
||||
def hide_ifc_spaces(self):
|
||||
"""Hide IfcSpace objects after they've been added to the scene."""
|
||||
for ifc_definition_id, obj in self.added_data.items():
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
element = self.file.by_id(ifc_definition_id)
|
||||
if element.is_a("IfcSpace"):
|
||||
obj.hide_set(True)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
"""
|
||||
@@ -1312,7 +1303,7 @@ class IfcImportSettings:
|
||||
self.should_load_geometry = True
|
||||
self.should_clean_mesh = False
|
||||
self.should_cache = True
|
||||
self.deflection_tolerance = 0.001
|
||||
self.deflection_tolerance = 0.05 # Default is 0.001, but I find this to be more practical
|
||||
self.angular_tolerance = 0.5
|
||||
self.void_limit = 30
|
||||
self.style_limit = 300
|
||||
|
||||
@@ -74,7 +74,7 @@ class ItemDecorator:
|
||||
special_verts = []
|
||||
special_edges = []
|
||||
|
||||
if len(obj.data.loop_triangles) > 0:
|
||||
if (total_triangles := len(obj.data.loop_triangles)) > 0:
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
|
||||
|
||||
@@ -140,7 +140,6 @@ classes = (
|
||||
prop.BIMRailingProperties,
|
||||
prop.BIMRoofProperties,
|
||||
prop.BIMPolylineProperties,
|
||||
prop.BIMProductPreviewProperties,
|
||||
prop.BIMExternalParametricGeometryProperties,
|
||||
ui.BIM_PT_array,
|
||||
ui.BIM_PT_stair,
|
||||
@@ -263,7 +262,6 @@ def register():
|
||||
|
||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
|
||||
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
|
||||
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
|
||||
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
|
||||
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
|
||||
@@ -288,7 +286,6 @@ def unregister():
|
||||
|
||||
del bpy.types.Scene.BIMModelProperties
|
||||
del bpy.types.Scene.BIMPolylineProperties
|
||||
del bpy.types.Scene.BIMProductPreviewProperties
|
||||
del bpy.types.Object.BIMArrayProperties
|
||||
del bpy.types.Object.BIMStairProperties
|
||||
del bpy.types.Object.BIMSverchokProperties
|
||||
|
||||
@@ -52,6 +52,11 @@ class AuthoringData:
|
||||
cls.is_loaded = True
|
||||
cls.props = tool.Model.get_model_props()
|
||||
cls.data["default_container"] = cls.default_container()
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
if ifc_element_type == "IfcDoorType":
|
||||
ifc_element_type = "IfcDoorStyle"
|
||||
elif ifc_element_type == "IfcWindowType":
|
||||
ifc_element_type = "IfcWindowStyle"
|
||||
cls.data["ifc_element_type"] = ifc_element_type
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
cls.data["ifc_class_current"] = cls.ifc_class_current()
|
||||
|
||||
@@ -26,16 +26,16 @@ import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
import math
|
||||
import mathutils
|
||||
from math import sin, cos, radians
|
||||
from math import sin, cos, tan, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from typing import Union
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
from itertools import chain
|
||||
from typing import Union, Any
|
||||
from typing import Union, Any, Literal
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
||||
|
||||
@@ -521,6 +521,9 @@ class PolylineDecorator:
|
||||
color = self.addon_prefs.decorations_colour
|
||||
|
||||
blf.color(self.font_id, *color)
|
||||
|
||||
screen_coords = {}
|
||||
|
||||
for i in range(len(self.polyline_points)):
|
||||
if i < 1 and self.measure_type == "POLY_AREA":
|
||||
continue
|
||||
@@ -528,40 +531,32 @@ class PolylineDecorator:
|
||||
continue
|
||||
dim_text_pos = (Vector(self.polyline_points[i].position) + Vector(self.polyline_points[i - 1].position)) / 2
|
||||
dim_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, dim_text_pos)
|
||||
|
||||
formatted_value = self.polyline_points[i].dim
|
||||
|
||||
blf.position(self.font_id, dim_text_coords[0], dim_text_coords[1], 0)
|
||||
text = "d: " + formatted_value
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
self.draw_text_background(context, dim_text_coords, text_length)
|
||||
blf.draw(self.font_id, text)
|
||||
if dim_text_coords:
|
||||
formatted_value = self.polyline_points[i].dim
|
||||
text = "d: " + formatted_value
|
||||
screen_coords[f"distance_{i}"] = (Vector(dim_text_coords), text)
|
||||
|
||||
if i == 1:
|
||||
continue
|
||||
angle_text_pos = Vector(self.polyline_points[i - 1].position)
|
||||
angle_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, angle_text_pos)
|
||||
blf.position(self.font_id, angle_text_coords[0], angle_text_coords[1], 0)
|
||||
text = "a: " + self.polyline_points[i].angle
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
self.draw_text_background(context, angle_text_coords, text_length)
|
||||
blf.draw(self.font_id, text)
|
||||
if angle_text_coords:
|
||||
text = "a: " + self.polyline_points[i].angle
|
||||
screen_coords[f"angle_{i}"] = (Vector(angle_text_coords), text)
|
||||
|
||||
if self.measure_type == "SINGLE":
|
||||
axis_line, axis_line_center = self.calculate_measurement_x_y_and_z(context)
|
||||
for i, dim_text_pos in enumerate(axis_line_center):
|
||||
dim_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, dim_text_pos)
|
||||
pos = blf.position(self.font_id, dim_text_coords[0], dim_text_coords[1], 0)
|
||||
value = round((axis_line[i][1] - axis_line[i][0]).length, 4)
|
||||
direction = axis_line[i][1] - axis_line[i][0]
|
||||
if (i == 0 and direction.x < 0) or (i == 1 and direction.y < 0) or (i == 2 and direction.z < 0):
|
||||
value = -value
|
||||
prefix = "xyz"[i]
|
||||
formatted_value = tool.Polyline.format_input_ui_units(value)
|
||||
text = f"{prefix}: {formatted_value}"
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
self.draw_text_background(context, dim_text_coords, text_length)
|
||||
blf.draw(self.font_id, text)
|
||||
if dim_text_coords:
|
||||
value = round((axis_line[i][1] - axis_line[i][0]).length, 4)
|
||||
direction = axis_line[i][1] - axis_line[i][0]
|
||||
if (i == 0 and direction.x < 0) or (i == 1 and direction.y < 0) or (i == 2 and direction.z < 0):
|
||||
value = -value
|
||||
prefix = "xyz"[i]
|
||||
formatted_value = tool.Polyline.format_input_ui_units(value)
|
||||
text = f"{prefix}: {formatted_value}"
|
||||
screen_coords[f"xyz_{i}"] = (Vector(dim_text_coords), text)
|
||||
|
||||
# Area and Length text
|
||||
polyline_verts = [Vector((p.x, p.y, p.z)) for p in self.polyline_points]
|
||||
@@ -569,35 +564,107 @@ class PolylineDecorator:
|
||||
# Area
|
||||
if self.measure_type == "POLY_AREA" and self.polyline_data.area:
|
||||
if len(polyline_verts) < 3:
|
||||
blf.disable(self.font_id, blf.SHADOW)
|
||||
return
|
||||
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
|
||||
center = sum(polyline_verts, Vector()) / len(polyline_verts)
|
||||
if polyline_verts[0] == polyline_verts[-1]:
|
||||
center = sum(polyline_verts[:-1], Vector()) / len(
|
||||
polyline_verts[:-1]
|
||||
) # Doesn't use the last point if is a closed polyline
|
||||
center = sum(polyline_verts[:-1], Vector()) / len(polyline_verts[:-1])
|
||||
area_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, center)
|
||||
value = self.polyline_data.area
|
||||
text = f"area: {value}"
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
area_text_coords[0] -= text_length[0] / 2 # Center text horizontally
|
||||
blf.position(self.font_id, area_text_coords[0], area_text_coords[1], 0)
|
||||
self.draw_text_background(context, area_text_coords, text_length)
|
||||
blf.draw(self.font_id, text)
|
||||
if area_text_coords:
|
||||
value = self.polyline_data.area
|
||||
text = f"area: {value}"
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
area_text_coords = list(area_text_coords)
|
||||
area_text_coords[0] -= text_length[0] / 2
|
||||
screen_coords["area"] = (Vector(area_text_coords), text)
|
||||
|
||||
# Length
|
||||
if self.measure_type in {"POLYLINE", "POLY_AREA"}:
|
||||
if len(polyline_verts) < 3:
|
||||
blf.disable(self.font_id, blf.SHADOW)
|
||||
return
|
||||
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
|
||||
blf.position(self.font_id, total_length_text_coords[0], total_length_text_coords[1], 0)
|
||||
value = self.polyline_data.total_length
|
||||
text = f"length: {value}"
|
||||
if total_length_text_coords:
|
||||
value = self.polyline_data.total_length
|
||||
text = f"length: {value}"
|
||||
screen_coords["length"] = (Vector(total_length_text_coords), text)
|
||||
|
||||
self.adjust_overlapping_labels(screen_coords)
|
||||
|
||||
for label_key, (screen_co, text) in screen_coords.items():
|
||||
blf.position(self.font_id, screen_co.x, screen_co.y, 0)
|
||||
blf.color(self.font_id, 1, 1, 1, 1)
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
self.draw_text_background(context, total_length_text_coords, text_length)
|
||||
self.draw_text_background(context, screen_co, text_length)
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
blf.disable(self.font_id, blf.SHADOW)
|
||||
|
||||
def adjust_overlapping_labels(self, screen_coords):
|
||||
font_id = self.font_id
|
||||
text_dimensions = {}
|
||||
|
||||
for label_key, (screen_co, text) in screen_coords.items():
|
||||
text_dimensions[label_key] = blf.dimensions(font_id, text)
|
||||
|
||||
if text_dimensions:
|
||||
first_height = next(iter(text_dimensions.values()))[1]
|
||||
min_spacing = max(2, first_height * 0.3)
|
||||
else:
|
||||
min_spacing = 2
|
||||
|
||||
label_keys = list(screen_coords.keys())
|
||||
for pass_num in range(3): # 3 passes to try to optimize complex overlaps
|
||||
for i in range(len(label_keys)):
|
||||
for j in range(i + 1, len(label_keys)):
|
||||
key1, key2 = label_keys[i], label_keys[j]
|
||||
co1, _ = screen_coords[key1]
|
||||
co2, _ = screen_coords[key2]
|
||||
dim1 = text_dimensions[key1]
|
||||
dim2 = text_dimensions[key2]
|
||||
|
||||
bounds1 = {
|
||||
"left": co1.x - min_spacing,
|
||||
"right": co1.x + dim1[0] + min_spacing,
|
||||
"top": co1.y + dim1[1] + min_spacing,
|
||||
"bottom": co1.y - min_spacing,
|
||||
}
|
||||
bounds2 = {
|
||||
"left": co2.x - min_spacing,
|
||||
"right": co2.x + dim2[0] + min_spacing,
|
||||
"top": co2.y + dim2[1] + min_spacing,
|
||||
"bottom": co2.y - min_spacing,
|
||||
}
|
||||
|
||||
if (
|
||||
bounds1["left"] < bounds2["right"]
|
||||
and bounds1["right"] > bounds2["left"]
|
||||
and bounds1["bottom"] < bounds2["top"]
|
||||
and bounds1["top"] > bounds2["bottom"]
|
||||
):
|
||||
x_overlap = min(bounds1["right"], bounds2["right"]) - max(bounds1["left"], bounds2["left"])
|
||||
y_overlap = min(bounds1["top"], bounds2["top"]) - max(bounds1["bottom"], bounds2["bottom"])
|
||||
|
||||
separation_multiplier = 1.25
|
||||
|
||||
# Move labels in the direction requiring less movement
|
||||
if x_overlap < y_overlap:
|
||||
separation_distance = (x_overlap / 2 + min_spacing) * separation_multiplier
|
||||
if co1.x < co2.x:
|
||||
co1.x -= separation_distance
|
||||
co2.x += separation_distance
|
||||
else:
|
||||
co1.x += separation_distance
|
||||
co2.x -= separation_distance
|
||||
else:
|
||||
separation_distance = (y_overlap / 2 + min_spacing) * separation_multiplier
|
||||
if co1.y < co2.y:
|
||||
co1.y -= separation_distance
|
||||
co2.y += separation_distance
|
||||
else:
|
||||
co1.y += separation_distance
|
||||
co2.y -= separation_distance
|
||||
|
||||
def draw_measurements_poly(self, context):
|
||||
self.shader_config(context)
|
||||
polyline_verts: list[Vector] = []
|
||||
@@ -879,13 +946,43 @@ class PolylineDecorator:
|
||||
class ProductDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
preview_mode: Literal["PROFILE_VERTICAL", "PROFILE_HORIZONTAL", "LAYER2", "LAYER3", "GENERIC"]
|
||||
relating_type = None
|
||||
obj_data: dict[str, list] = {}
|
||||
obj_matrix_i = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
|
||||
props = tool.Model.get_model_props()
|
||||
|
||||
handler = cls()
|
||||
if (
|
||||
(props.relating_type_id)
|
||||
and (relating_type := tool.Ifc.get().by_id(int(props.relating_type_id)))
|
||||
and (relating_type_obj := tool.Ifc.get_object(relating_type))
|
||||
):
|
||||
handler.relating_type = relating_type
|
||||
if tool.Model.get_usage_type(relating_type) == "PROFILE":
|
||||
if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}:
|
||||
handler.preview_mode = "PROFILE_VERTICAL"
|
||||
else:
|
||||
handler.preview_mode = "PROFILE_HORIZONTAL"
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
handler.preview_mode = "LAYER2"
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
|
||||
handler.preview_mode = "LAYER3"
|
||||
else:
|
||||
handler.preview_mode = "GENERIC"
|
||||
if relating_type_obj.data:
|
||||
handler.obj_data = ItemDecorator.get_obj_data(relating_type_obj)
|
||||
handler.obj_data["raw_verts"] = [Vector(v) for v in handler.obj_data["verts"]]
|
||||
handler.obj_matrix_i = relating_type_obj.matrix_world.inverted()
|
||||
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
@@ -893,10 +990,6 @@ class ProductDecorator:
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
props = tool.Model.get_product_preview_props() # updated by model/polyline.py
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
@@ -912,14 +1005,6 @@ class ProductDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def get_product_preview_data(self, context) -> dict[str, Any]:
|
||||
props = tool.Model.get_product_preview_props()
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = [(*v.value_3d,) for v in props.verts]
|
||||
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
|
||||
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
|
||||
return data
|
||||
|
||||
def draw_product_preview(self, context):
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
@@ -945,12 +1030,549 @@ class ProductDecorator:
|
||||
else:
|
||||
return
|
||||
|
||||
product_preview_data = self.get_product_preview_data(context)
|
||||
if product_preview_data:
|
||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
||||
self.draw_batch(
|
||||
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"]
|
||||
if self.preview_mode == "LAYER2":
|
||||
data = self.get_wall_preview_data()
|
||||
elif self.preview_mode == "LAYER3":
|
||||
data = self.get_slab_preview_data()
|
||||
elif self.preview_mode == "PROFILE_VERTICAL":
|
||||
data = self.get_vertical_profile_preview_data()
|
||||
elif self.preview_mode == "PROFILE_HORIZONTAL":
|
||||
data = self.get_horizontal_profile_preview_data()
|
||||
elif self.preview_mode == "GENERIC":
|
||||
data = self.get_generic_preview_data()
|
||||
if data:
|
||||
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color), data["tris"])
|
||||
|
||||
def get_wall_preview_data(self):
|
||||
relating_type = self.relating_type
|
||||
# Get properties from object type
|
||||
model_props = tool.Model.get_model_props()
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"]
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_vertical
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * unit_scale
|
||||
|
||||
height = float(model_props.extrusion_depth)
|
||||
rl = float(model_props.rl1)
|
||||
x_angle = float(model_props.x_angle)
|
||||
if x_angle > radians(90) or x_angle < radians(-90):
|
||||
height *= -1
|
||||
angle_distance = height * tan(x_angle)
|
||||
thickness *= 1 / cos(x_angle)
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
|
||||
is_closed = False
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
is_closed = True
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
|
||||
bm_base = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distance + offset)
|
||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + thickness + offset)
|
||||
if is_closed:
|
||||
base_vertices.append(base_vertices[0])
|
||||
offset_base_verts.append(offset_base_verts[0])
|
||||
top_vertices.append(top_vertices[0])
|
||||
offset_top_verts.append(offset_top_verts[0])
|
||||
|
||||
if offset_base_verts is not None:
|
||||
for v in base_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in offset_base_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in top_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
for v in offset_top_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
bm_base.free()
|
||||
|
||||
# Edges and Tris
|
||||
points = []
|
||||
side_edges_1 = []
|
||||
side_edges_2 = []
|
||||
base_edges = []
|
||||
|
||||
for i in range(len(data["verts"])):
|
||||
points.append(Vector(data["verts"][i]))
|
||||
|
||||
n = len(points) // 2
|
||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||
side_edges_1.extend(bottom_side_1)
|
||||
side_edges_2.extend(bottom_side_2)
|
||||
base_edges.extend(bottom_loop)
|
||||
|
||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||
side_edges_1.extend(upper_side_1)
|
||||
side_edges_2.extend(upper_side_2)
|
||||
base_edges.extend(upper_loop)
|
||||
|
||||
loops = [side_edges_1, side_edges_2, base_edges]
|
||||
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
for i, group in enumerate(loops):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
if i == 2:
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||
data["edges"].extend(edges)
|
||||
data["tris"].extend(tris)
|
||||
|
||||
data["edges"] = list(set(tuple(e) for e in data["edges"]))
|
||||
data["tris"] = list(set(tuple(t) for t in data["tris"]))
|
||||
return data
|
||||
|
||||
def get_slab_preview_data(self):
|
||||
relating_type = self.relating_type
|
||||
model_props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"] * abs(1 / cos(x_angle))
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_horizontal
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 3:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
if x_angle:
|
||||
# Get vertices relative to the first polyline point as origin
|
||||
local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices]
|
||||
# Make the transformation relative to the x_angle
|
||||
transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices]
|
||||
# Convert back to world origin
|
||||
polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices]
|
||||
if offset != 0:
|
||||
polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices]
|
||||
is_closed = True
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
bm = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
bm.verts.ensure_lookup_table()
|
||||
if x_angle:
|
||||
rot_mat = Matrix.Rotation(x_angle, 3, "X")
|
||||
if abs(x_angle) > (pi / 2):
|
||||
rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0))
|
||||
bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat)
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, thickness))
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
return data
|
||||
|
||||
def get_vertical_profile_preview_data(self) -> dict[str, Any]:
|
||||
relating_type = self.relating_type
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
extrusion_depth = model_props.extrusion_depth
|
||||
cardinal_point = model_props.cardinal_point
|
||||
rot_mat = Quaternion()
|
||||
if relating_type.is_a("IfcBeamType"):
|
||||
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
||||
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
||||
rot_mat = y_rot @ z_rot
|
||||
# Get profile data
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
min_z = min(v.co.z for v in bm.verts)
|
||||
max_z = max(v.co.z for v in bm.verts)
|
||||
# Add axis verts
|
||||
verts.append(tuple(mouse_point))
|
||||
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
|
||||
# Add only profile edges
|
||||
edges = []
|
||||
for edge in bm.edges:
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||
):
|
||||
edges.append(edge)
|
||||
# Add axis edge
|
||||
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||
edges.append((len(verts) - 1, len(verts) - 2))
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
return data
|
||||
|
||||
def get_horizontal_profile_preview_data(self) -> dict[str, Any]:
|
||||
relating_type = self.relating_type
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
cardinal_point = model_props.cardinal_point
|
||||
|
||||
polyline_verts = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
return {}
|
||||
for point in polyline_points:
|
||||
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
|
||||
|
||||
# Get profile shape
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = []
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
|
||||
grouped_verts = [(v) for v in grouped_verts]
|
||||
|
||||
all_bm = bmesh.new()
|
||||
for i in range(len(polyline_verts) - 1):
|
||||
mesh = bpy.data.meshes.new("TempMesh")
|
||||
# Create the initial mesh from the profile verts
|
||||
bm = tool.Model.create_bmesh_from_vertices(grouped_verts, is_closed=True)
|
||||
bm.verts.ensure_lookup_table()
|
||||
# Creates the clipping plane formed by two segments.
|
||||
# The first one is for the profile start, based on the current and previous segment of the polyline.
|
||||
# The second is for the profile end, based on the current and the next segment.
|
||||
if i == 0:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_start = d
|
||||
else:
|
||||
d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
|
||||
d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
|
||||
clip_start = (d1 - d2).normalized()
|
||||
|
||||
if i == len(polyline_verts) - 2:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_end = d
|
||||
else:
|
||||
d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
|
||||
clip_end = (d1 - d2).normalized()
|
||||
|
||||
# Rotates the profile face to the right direction
|
||||
direction = polyline_verts[i + 1] - polyline_verts[i]
|
||||
position = polyline_verts[i]
|
||||
rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
|
||||
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
|
||||
|
||||
# Extrude and move the new face
|
||||
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
|
||||
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
|
||||
# Apply the cutting planes
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i],
|
||||
plane_no=clip_start,
|
||||
clear_inner=True,
|
||||
)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i + 1],
|
||||
plane_no=clip_end,
|
||||
clear_outer=True,
|
||||
)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
all_bm.from_mesh(mesh)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
|
||||
|
||||
# It's necessary to add the mesh to an object to get the expected result.
|
||||
mesh = bpy.data.meshes.new("TempMesh2")
|
||||
all_bm.to_mesh(mesh)
|
||||
all_bm.free()
|
||||
obj = bpy.data.objects.new("TempObj", mesh)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
bm.free()
|
||||
return data
|
||||
|
||||
def get_generic_preview_data(self):
|
||||
if not (data := self.obj_data):
|
||||
return
|
||||
relating_type = self.relating_type
|
||||
model_props = tool.Model.get_model_props()
|
||||
if relating_type.is_a("IfcDoorType"):
|
||||
rl = float(model_props.rl1)
|
||||
elif relating_type.is_a("IfcWindowType"):
|
||||
rl = float(model_props.rl2)
|
||||
else:
|
||||
rl = 0
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
default_container_elevation = tool.Root.get_default_container_elevation()
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||
rot_mat = Matrix()
|
||||
if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"):
|
||||
layers = tool.Model.get_material_layer_parameters(snap_element)
|
||||
axes = tool.Model.get_wall_axis(snap_obj, layers=layers)
|
||||
axis_base = axes["base"]
|
||||
axis_side = axes["side"]
|
||||
point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base)
|
||||
point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side)
|
||||
if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared:
|
||||
# mouse is snapped to the base axis, the preview looks exactly like the placed door / window
|
||||
rot_mat = snap_obj.matrix_world
|
||||
else:
|
||||
# mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later
|
||||
rot_mat = (
|
||||
(snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180)))
|
||||
.to_matrix()
|
||||
.to_4x4()
|
||||
)
|
||||
|
||||
mouse_point.z = snap_obj.matrix_world.translation.z
|
||||
|
||||
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
mouse_point.z = container_obj.location.z
|
||||
|
||||
obj_type = tool.Ifc.get_object(relating_type)
|
||||
|
||||
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
|
||||
if not subcontexts:
|
||||
subcontexts = [("Model", "Body", "MODEL_VIEW")]
|
||||
|
||||
active_context = tool.Geometry.get_active_representation_context(obj_type)
|
||||
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
|
||||
for subcontext in subcontexts:
|
||||
if subcontext == active_context_params:
|
||||
break
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
|
||||
if representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj_type,
|
||||
representation,
|
||||
)
|
||||
context.view_layer.update()
|
||||
break
|
||||
|
||||
translate_mouse = Matrix.Translation(mouse_point)
|
||||
translate_rl = Matrix.Translation((0.0, 0.0, rl))
|
||||
combined_m = translate_mouse @ rot_mat @ translate_rl @ self.obj_matrix_i
|
||||
data["verts"] = [tuple(combined_m @ v) for v in data["raw_verts"]]
|
||||
return data
|
||||
|
||||
|
||||
class WallAxisDecorator:
|
||||
|
||||
@@ -35,563 +35,12 @@ import bonsai.core.root
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.model as core
|
||||
import bonsai.tool as tool
|
||||
from math import pi, sin, cos, degrees, tan, radians
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
from mathutils import Vector
|
||||
from bonsai.bim.module.model.decorator import PolylineDecorator
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
from typing import Optional, Union, Literal, Any
|
||||
from lark import Lark, Transformer
|
||||
|
||||
|
||||
def create_bmesh_from_vertices(vertices, is_closed=False):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in vertices]
|
||||
if is_closed:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
new_edges.append(
|
||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||
) # Add an edge between the last an first point to make it closed.
|
||||
else:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
return bm
|
||||
|
||||
|
||||
def get_wall_preview_data(context, relating_type):
|
||||
# Get properties from object type
|
||||
model_props = tool.Model.get_model_props()
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"]
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_vertical
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * unit_scale
|
||||
|
||||
height = float(model_props.extrusion_depth)
|
||||
rl = float(model_props.rl1)
|
||||
x_angle = float(model_props.x_angle)
|
||||
if x_angle > radians(90) or x_angle < radians(-90):
|
||||
height *= -1
|
||||
angle_distance = height * tan(x_angle)
|
||||
thickness *= 1 / cos(x_angle)
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
|
||||
is_closed = False
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
is_closed = True
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
|
||||
bm_base = create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distance + offset)
|
||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + thickness + offset)
|
||||
if is_closed:
|
||||
base_vertices.append(base_vertices[0])
|
||||
offset_base_verts.append(offset_base_verts[0])
|
||||
top_vertices.append(top_vertices[0])
|
||||
offset_top_verts.append(offset_top_verts[0])
|
||||
|
||||
if offset_base_verts is not None:
|
||||
for v in base_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in offset_base_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in top_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
for v in offset_top_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
bm_base.free()
|
||||
|
||||
# Edges and Tris
|
||||
points = []
|
||||
side_edges_1 = []
|
||||
side_edges_2 = []
|
||||
base_edges = []
|
||||
|
||||
for i in range(len(data["verts"])):
|
||||
points.append(Vector(data["verts"][i]))
|
||||
|
||||
n = len(points) // 2
|
||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||
side_edges_1.extend(bottom_side_1)
|
||||
side_edges_2.extend(bottom_side_2)
|
||||
base_edges.extend(bottom_loop)
|
||||
|
||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||
side_edges_1.extend(upper_side_1)
|
||||
side_edges_2.extend(upper_side_2)
|
||||
base_edges.extend(upper_loop)
|
||||
|
||||
loops = [side_edges_1, side_edges_2, base_edges]
|
||||
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
for i, group in enumerate(loops):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
if i == 2:
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||
data["edges"].extend(edges)
|
||||
data["tris"].extend(tris)
|
||||
|
||||
data["edges"] = list(set(tuple(e) for e in data["edges"]))
|
||||
data["tris"] = list(set(tuple(t) for t in data["tris"]))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_slab_preview_data(context, relating_type):
|
||||
model_props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"] * abs(1 / cos(x_angle))
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_horizontal
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 3:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
if x_angle:
|
||||
# Get vertices relative to the first polyline point as origin
|
||||
local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices]
|
||||
# Make the transformation relative to the x_angle
|
||||
transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices]
|
||||
# Convert back to world origin
|
||||
polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices]
|
||||
if offset != 0:
|
||||
polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices]
|
||||
is_closed = True
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
bm = create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
bm.verts.ensure_lookup_table()
|
||||
if x_angle:
|
||||
rot_mat = Matrix.Rotation(x_angle, 3, "X")
|
||||
if abs(x_angle) > (pi / 2):
|
||||
rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0))
|
||||
bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat)
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, thickness))
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
return data
|
||||
|
||||
|
||||
def get_vertical_profile_preview_data(
|
||||
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
|
||||
) -> dict[str, Any]:
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
extrusion_depth = model_props.extrusion_depth
|
||||
cardinal_point = model_props.cardinal_point
|
||||
rot_mat = Quaternion()
|
||||
if relating_type.is_a("IfcBeamType"):
|
||||
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
||||
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
||||
rot_mat = y_rot @ z_rot
|
||||
# Get profile data
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
min_z = min(v.co.z for v in bm.verts)
|
||||
max_z = max(v.co.z for v in bm.verts)
|
||||
# Add axis verts
|
||||
verts.append(tuple(mouse_point))
|
||||
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
|
||||
# Add only profile edges
|
||||
edges = []
|
||||
for edge in bm.edges:
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||
):
|
||||
edges.append(edge)
|
||||
# Add axis edge
|
||||
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||
edges.append((len(verts) - 1, len(verts) - 2))
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_horizontal_profile_preview_data(
|
||||
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
|
||||
) -> dict[str, Any]:
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
cardinal_point = model_props.cardinal_point
|
||||
|
||||
polyline_verts = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
return {}
|
||||
for point in polyline_points:
|
||||
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
|
||||
|
||||
# Get profile shape
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = []
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
|
||||
grouped_verts = [(v) for v in grouped_verts]
|
||||
|
||||
all_bm = bmesh.new()
|
||||
for i in range(len(polyline_verts) - 1):
|
||||
mesh = bpy.data.meshes.new("TempMesh")
|
||||
# Create the initial mesh from the profile verts
|
||||
bm = create_bmesh_from_vertices(grouped_verts, is_closed=True)
|
||||
bm.verts.ensure_lookup_table()
|
||||
# Creates the clipping plane formed by two segments.
|
||||
# The first one is for the profile start, based on the current and previous segment of the polyline.
|
||||
# The second is for the profile end, based on the current and the next segment.
|
||||
if i == 0:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_start = d
|
||||
else:
|
||||
d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
|
||||
d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
|
||||
clip_start = (d1 - d2).normalized()
|
||||
|
||||
if i == len(polyline_verts) - 2:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_end = d
|
||||
else:
|
||||
d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
|
||||
clip_end = (d1 - d2).normalized()
|
||||
|
||||
# Rotates the profile face to the right direction
|
||||
direction = polyline_verts[i + 1] - polyline_verts[i]
|
||||
position = polyline_verts[i]
|
||||
rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
|
||||
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
|
||||
|
||||
# Extrude and move the new face
|
||||
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
|
||||
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
|
||||
# Apply the cutting planes
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i],
|
||||
plane_no=clip_start,
|
||||
clear_inner=True,
|
||||
)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i + 1],
|
||||
plane_no=clip_end,
|
||||
clear_outer=True,
|
||||
)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
all_bm.from_mesh(mesh)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
|
||||
|
||||
# It's necessary to add the mesh to an object to get the expected result.
|
||||
mesh = bpy.data.meshes.new("TempMesh2")
|
||||
all_bm.to_mesh(mesh)
|
||||
all_bm.free()
|
||||
obj = bpy.data.objects.new("TempObj", mesh)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
bm.free()
|
||||
return data
|
||||
|
||||
|
||||
def get_generic_product_preview_data(context, relating_type):
|
||||
model_props = tool.Model.get_model_props()
|
||||
if relating_type.is_a("IfcDoorType"):
|
||||
rl = float(model_props.rl1)
|
||||
elif relating_type.is_a("IfcWindowType"):
|
||||
rl = float(model_props.rl2)
|
||||
else:
|
||||
rl = 0
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
default_container_elevation = tool.Root.get_default_container_elevation()
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||
rot_mat = Quaternion()
|
||||
if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"):
|
||||
layers = tool.Model.get_material_layer_parameters(snap_element)
|
||||
axes = tool.Model.get_wall_axis(snap_obj, layers=layers)
|
||||
axis_base = axes["base"]
|
||||
axis_side = axes["side"]
|
||||
point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base)
|
||||
point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side)
|
||||
if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared:
|
||||
# mouse is snapped to the base axis, the preview looks exactly like the placed door / window
|
||||
rot_mat = snap_obj.matrix_world.to_quaternion()
|
||||
else:
|
||||
# mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later
|
||||
rot_mat = snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180))
|
||||
|
||||
mouse_point.z = snap_obj.matrix_world.translation.z
|
||||
|
||||
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
mouse_point.z = container_obj.location.z
|
||||
|
||||
obj_type = tool.Ifc.get_object(relating_type)
|
||||
|
||||
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
|
||||
if not subcontexts:
|
||||
subcontexts = [("Model", "Body", "MODEL_VIEW")]
|
||||
|
||||
active_context = tool.Geometry.get_active_representation_context(obj_type)
|
||||
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
|
||||
for subcontext in subcontexts:
|
||||
if subcontext == active_context_params:
|
||||
break
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
|
||||
if representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj_type,
|
||||
representation,
|
||||
)
|
||||
context.view_layer.update()
|
||||
break
|
||||
|
||||
if obj_type.data:
|
||||
data = ItemDecorator.get_obj_data(obj_type)
|
||||
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
|
||||
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class PolylineOperator:
|
||||
# TODO Fill doc strings
|
||||
""" """
|
||||
@@ -987,37 +436,6 @@ class PolylineOperator:
|
||||
tool.Blender.update_viewport()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
|
||||
if tool.Model.get_usage_type(relating_type) == "PROFILE":
|
||||
if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}:
|
||||
data = get_vertical_profile_preview_data(context, relating_type)
|
||||
else:
|
||||
data = get_horizontal_profile_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
data = get_wall_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
|
||||
data = get_slab_preview_data(context, relating_type)
|
||||
else:
|
||||
data = get_generic_product_preview_data(context, relating_type)
|
||||
|
||||
# Update properties so it can be used by the decorator
|
||||
props = tool.Model.get_product_preview_props()
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
if not data:
|
||||
return
|
||||
|
||||
for vert in data["verts"]:
|
||||
v = props.verts.add()
|
||||
v.value_3d = vert
|
||||
for edge in data["edges"]:
|
||||
e = props.edges.add()
|
||||
e.value_2d = edge
|
||||
for tri in data["tris"]:
|
||||
t = props.tris.add()
|
||||
t.value_3d = tri
|
||||
|
||||
def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
|
||||
props = tool.Model.get_model_props()
|
||||
direction_sense = props.direction_sense
|
||||
|
||||
@@ -249,8 +249,6 @@ class DrawOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
||||
self.create_occurrence(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -1189,11 +1189,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -1697,17 +1697,6 @@ class ProductPreviewItem(PropertyGroup):
|
||||
value_2d: tuple[float, float]
|
||||
|
||||
|
||||
class BIMProductPreviewProperties(PropertyGroup):
|
||||
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
verts: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
|
||||
edges: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
|
||||
tris: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
|
||||
|
||||
|
||||
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.is_editing:
|
||||
return
|
||||
|
||||
@@ -1135,11 +1135,8 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -691,9 +691,17 @@ class BIM_PT_external_parametric_geometry(bpy.types.Panel):
|
||||
# should find a way to update only on graph changes.
|
||||
res = tool.Model.update_mesh_from_sverchok(obj, props.sverchok_nodes)
|
||||
if res is not None:
|
||||
print(res)
|
||||
layout.label(text=f"Error Updating from Graph, See System Console", icon="ERROR")
|
||||
|
||||
layout.label(text="Parameters:")
|
||||
box = layout.box()
|
||||
|
||||
group_node = tool.Model.get_ifcsverchok_group_node(props.sverchok_nodes)
|
||||
node_tree = group_node.node_tree
|
||||
|
||||
for socket, interface_socket in zip(group_node.inputs, node_tree.sockets("INPUT")):
|
||||
socket.draw_group_property(box, socket.name, interface_socket)
|
||||
|
||||
|
||||
def draw_door_properties(layout: bpy.types.UILayout, props: module_prop.BIMDoorProperties) -> None:
|
||||
"""Draw door properties UI (shared between properties panel and preferences)."""
|
||||
|
||||
@@ -787,11 +787,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -319,7 +319,7 @@ class BIMProjectProperties(PropertyGroup):
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001)
|
||||
deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.05)
|
||||
angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5)
|
||||
void_limit: IntProperty(
|
||||
name="Void Limit",
|
||||
|
||||
@@ -445,20 +445,17 @@ class FilterValueSuggestions(Operator):
|
||||
if pset.HasProperties:
|
||||
for prop in pset.HasProperties:
|
||||
if hasattr(prop, "Name") and prop.Name == property_name:
|
||||
if hasattr(prop, "NominalValue") and prop.NominalValue:
|
||||
try:
|
||||
value = prop.NominalValue.wrappedValue
|
||||
if value is not None and value != "":
|
||||
if not hasattr(value, "is_a") and not isinstance(
|
||||
value, (tuple, list)
|
||||
):
|
||||
str_value = str(value)
|
||||
if not str_value.startswith("#") and not str_value.startswith(
|
||||
"("
|
||||
):
|
||||
property_values.add(str_value)
|
||||
except:
|
||||
continue
|
||||
if prop.is_a("IfcPropertyEnumeratedValue"):
|
||||
if hasattr(prop, "EnumerationReference") and prop.EnumerationReference:
|
||||
enum_reference = prop.EnumerationReference
|
||||
if hasattr(enum_reference, "EnumerationValues"):
|
||||
for enum_value in enum_reference.EnumerationValues:
|
||||
property_values.add(str(enum_value.wrappedValue))
|
||||
if hasattr(prop, "EnumerationValues") and prop.EnumerationValues:
|
||||
for enum_value in prop.EnumerationValues:
|
||||
property_values.add(str(enum_value.wrappedValue))
|
||||
elif hasattr(prop, "NominalValue") and prop.NominalValue:
|
||||
property_values.add(str(prop.NominalValue.wrappedValue))
|
||||
except:
|
||||
continue
|
||||
return property_values
|
||||
@@ -765,10 +762,7 @@ class Search(Operator):
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
|
||||
# Migrate old ! prefix filters to new filter_mode system when preferences are enabled
|
||||
if (
|
||||
preferences.chain_filter_with_set_operations
|
||||
or preferences.default_filter_with_set_operations_for_globalid_and_class
|
||||
):
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
for filter_group in props.filter_groups:
|
||||
for ifc_filter in filter_group.filters:
|
||||
if ifc_filter.type not in ["entity", "instance"]:
|
||||
|
||||
@@ -21,11 +21,13 @@ from . import ui, prop, operator, decorator
|
||||
|
||||
classes = (
|
||||
operator.AddPort,
|
||||
operator.AddRelatedPortConnection,
|
||||
operator.AddSystem,
|
||||
operator.AddZone,
|
||||
operator.AssignSystem,
|
||||
operator.AssignUnassignFlowControl,
|
||||
operator.ConnectPort,
|
||||
operator.CycleFlowDirection,
|
||||
operator.DisableEditingSystem,
|
||||
operator.DisableEditingZone,
|
||||
operator.DisableSystemEditingUI,
|
||||
|
||||
@@ -198,7 +198,16 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
# Ifc.Operator - as operator will sync object's position with IFC.
|
||||
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=tool.Ifc.get_entity(context.active_object))
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=element)
|
||||
|
||||
for port in tool.System.get_ports(element):
|
||||
connected_port = tool.System.get_connected_port(port)
|
||||
if connected_port:
|
||||
connected_port_obj = tool.Ifc.get_object(connected_port)
|
||||
if not connected_port_obj:
|
||||
parent_element = tool.System.get_port_relating_element(connected_port)
|
||||
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=parent_element)
|
||||
|
||||
|
||||
class HidePorts(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -217,7 +226,7 @@ class HidePorts(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class AddPort(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_port"
|
||||
bl_description = "Add port at current cursor position"
|
||||
bl_description = "Add USERDEFINED port at current cursor position"
|
||||
bl_label = "Add Port"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -246,7 +255,41 @@ class ConnectPort(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
obj1 = context.active_object
|
||||
obj2 = context.selected_objects[0] if context.selected_objects[1] == obj1 else context.selected_objects[1]
|
||||
core.connect_port(tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2))
|
||||
direction = tool.Ifc.get_entity(obj1).FlowDirection or "NOTDEFINED"
|
||||
core.connect_port(
|
||||
tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2), direction=direction
|
||||
)
|
||||
|
||||
|
||||
class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_related_port_connection"
|
||||
bl_label = "Connect Port"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
relating_port_id: bpy.props.IntProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
props = tool.System.get_system_props()
|
||||
self.layout.prop(props, "related_port", text="Select Port")
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.System.get_system_props()
|
||||
|
||||
if not props.related_port or props.related_port == "NONE":
|
||||
return {"CANCELLED"}
|
||||
|
||||
port_obj = bpy.data.objects.get(props.related_port)
|
||||
related_port = tool.Ifc.get_entity(port_obj)
|
||||
relating_port = tool.Ifc.get().by_id(self.relating_port_id)
|
||||
|
||||
direction = relating_port.FlowDirection or "NOTDEFINED"
|
||||
core.connect_port(tool.Ifc, port1=relating_port, port2=related_port, direction=direction)
|
||||
PortData.is_loaded = False
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -256,6 +299,9 @@ class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.element_id != 0:
|
||||
element = tool.Ifc.get().by_id(self.element_id)
|
||||
@@ -310,7 +356,8 @@ class MEPConnectElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ports_distance[(port1, port2)] = distance
|
||||
|
||||
closest_ports = min(ports_distance, key=lambda x: ports_distance[x])
|
||||
core.connect_port(tool.Ifc, *closest_ports)
|
||||
direction = closest_ports[0].FlowDirection or "NOTDEFINED"
|
||||
core.connect_port(tool.Ifc, *closest_ports, direction=direction)
|
||||
bpy.ops.bim.regenerate_distribution_element()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -379,6 +426,55 @@ class SetFlowDirection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cycle_flow_direction"
|
||||
bl_label = "Cycle Flow Direction"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
port_id: bpy.props.IntProperty()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
port = tool.Ifc.get().by_id(operator.port_id)
|
||||
if port and port.is_a("IfcDistributionPort"):
|
||||
current_direction = port.FlowDirection or "NOTDEFINED"
|
||||
return f"Current flow direction: {current_direction}. Click to cycle: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED"
|
||||
return "Cycle through flow directions: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED → SOURCE..."
|
||||
|
||||
def _execute(self, context):
|
||||
port = tool.Ifc.get().by_id(self.port_id)
|
||||
if not port or not port.is_a("IfcDistributionPort"):
|
||||
return {"CANCELLED"}
|
||||
|
||||
current_direction = port.FlowDirection or "NOTDEFINED"
|
||||
|
||||
flow_cycle_map = {
|
||||
"SOURCE": "SINK",
|
||||
"SINK": "SOURCEANDSINK",
|
||||
"SOURCEANDSINK": "NOTDEFINED",
|
||||
"NOTDEFINED": "SOURCE",
|
||||
}
|
||||
next_direction = flow_cycle_map.get(current_direction, "SOURCE")
|
||||
|
||||
tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction})
|
||||
|
||||
connected_port = tool.System.get_connected_port(port)
|
||||
if connected_port:
|
||||
connected_direction_map = {
|
||||
"SOURCE": "SINK",
|
||||
"SINK": "SOURCE",
|
||||
"SOURCEANDSINK": "SOURCEANDSINK",
|
||||
"NOTDEFINED": "NOTDEFINED",
|
||||
}
|
||||
connected_direction = connected_direction_map.get(next_direction, "NOTDEFINED")
|
||||
tool.Ifc.run(
|
||||
"attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction}
|
||||
)
|
||||
|
||||
PortData.is_loaded = False
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadZones(bpy.types.Operator):
|
||||
bl_idname = "bim.load_zones"
|
||||
bl_label = "Load Zones"
|
||||
|
||||
@@ -92,6 +92,28 @@ def toggle_decorations(self: "BIMSystemProperties", context: bpy.types.Context)
|
||||
decorator.SystemDecorator.uninstall()
|
||||
|
||||
|
||||
def get_available_ports_for_connection(
|
||||
self: "BIMSystemProperties", context: bpy.types.Context
|
||||
) -> list[tuple[str, str, str]]:
|
||||
items = []
|
||||
active_object_ports = set(tool.System.get_ports(tool.Ifc.get_entity(context.active_object)))
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
for ifc_port in ifc_file.by_type("IfcDistributionPort"):
|
||||
port = tool.Ifc.get_object(ifc_port)
|
||||
if not port:
|
||||
continue
|
||||
|
||||
if tool.System.get_connected_port(ifc_port) is not None or ifc_port in active_object_ports:
|
||||
continue
|
||||
|
||||
port_object = tool.Ifc.get_object(tool.System.get_port_relating_element(ifc_port))
|
||||
suggestion = f"{port_object.name} > {port.name}"
|
||||
items.append((port.name, suggestion, ""))
|
||||
|
||||
return items if items else [("NONE", "Ports are hidden or not available", "")]
|
||||
|
||||
|
||||
class BIMSystemProperties(PropertyGroup):
|
||||
system_attributes: CollectionProperty(name="System Attributes", type=Attribute)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
@@ -107,6 +129,11 @@ class BIMSystemProperties(PropertyGroup):
|
||||
should_draw_decorations: BoolProperty(
|
||||
name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations
|
||||
)
|
||||
related_port: EnumProperty(
|
||||
name="Connect To Port",
|
||||
description="Select a port to connect to",
|
||||
items=get_available_ports_for_connection,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
system_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
@@ -119,6 +146,7 @@ class BIMSystemProperties(PropertyGroup):
|
||||
edited_system_id: int
|
||||
system_class: str
|
||||
should_draw_decorations: bool
|
||||
related_port: str
|
||||
|
||||
@property
|
||||
def active_system_ui_item(self) -> Union[System, None]:
|
||||
|
||||
@@ -30,10 +30,10 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
FLOW_DIRECTION_TO_ICON = {
|
||||
"SOURCE": "FORWARD",
|
||||
"SINK": "BACK",
|
||||
"SOURCEANDSINK": "ARROW_LEFTRIGHT",
|
||||
"NOTDEFINED": "CHECKBOX_DEHLT",
|
||||
"SOURCE": "FULLSCREEN_ENTER",
|
||||
"SINK": "FULLSCREEN_EXIT",
|
||||
"SOURCEANDSINK": "CHECKBOX_DEHLT",
|
||||
"NOTDEFINED": "QUESTION",
|
||||
}
|
||||
|
||||
|
||||
@@ -163,44 +163,87 @@ class BIM_PT_ports(Panel):
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Change Flow Direction:")
|
||||
|
||||
current_flow_direction = PortData.data["selected_objects_flow_direction"]
|
||||
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
|
||||
row.operator(
|
||||
"bim.set_flow_direction",
|
||||
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
|
||||
depress=flow_direction == current_flow_direction,
|
||||
text="",
|
||||
).direction = flow_direction
|
||||
row.enabled = len(context.selected_objects) == 2
|
||||
row.label(text=f"Ports located in: {context.active_object.name} and connected Port/Objects:")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Ports located on object and connected objects:")
|
||||
row = self.layout.row(align=True)
|
||||
cols = [row.column(align=True) for i in range(6)]
|
||||
cols = [row.column(align=True) for i in range(9)]
|
||||
cols[3].scale_x = 1.0
|
||||
cols[6].scale_x = 1.0
|
||||
cols[8].scale_x = 1.33
|
||||
|
||||
for port_data in PortData.data["located_ports_data"]:
|
||||
flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"]
|
||||
if port_data["port_obj_name"]:
|
||||
cols[0].label(text="", icon=flow_direction_icon)
|
||||
cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"]
|
||||
cols[2].label(text=port_data["port_obj_name"])
|
||||
|
||||
if port_data["connected_obj_name"]:
|
||||
cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"]
|
||||
op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
|
||||
op.port_id = port_data["id"]
|
||||
else:
|
||||
cols[0].label(text="", icon=flow_direction_icon)
|
||||
cols[1].label(text="", icon="HIDE_ON")
|
||||
cols[2].label(text="Port is hidden")
|
||||
op = cols[0].operator("bim.add_related_port_connection", text="", icon="PLUGIN")
|
||||
op.relating_port_id = port_data["id"]
|
||||
op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
|
||||
op.port_id = port_data["id"]
|
||||
|
||||
if port_data["port_obj_name"]:
|
||||
cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"]
|
||||
cols[3].label(text=port_data["port_obj_name"])
|
||||
else:
|
||||
cols[2].label(text="", icon="HIDE_ON")
|
||||
cols[3].label(text="Port is hidden")
|
||||
|
||||
if port_data["connected_obj_name"]:
|
||||
connected_obj = bpy.data.objects[port_data["connected_obj_name"]]
|
||||
cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"]
|
||||
|
||||
port = tool.Ifc.get().by_id(port_data["id"])
|
||||
connected_port = tool.System.get_connected_port(port)
|
||||
if connected_port:
|
||||
connected_port_obj = tool.Ifc.get_object(connected_port)
|
||||
if connected_port_obj:
|
||||
connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"]
|
||||
op = cols[4].operator(
|
||||
"bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True
|
||||
)
|
||||
op.port_id = connected_port.id()
|
||||
cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = (
|
||||
connected_port.id()
|
||||
)
|
||||
cols[6].label(text=connected_port_obj.name)
|
||||
else:
|
||||
blank4 = cols[4].column(align=True)
|
||||
blank4.scale_x = 0.1
|
||||
blank4.label(text="", icon="BLANK1")
|
||||
blank5 = cols[5].column(align=True)
|
||||
blank5.scale_x = 0.1
|
||||
blank5.label(text="", icon="BLANK1")
|
||||
cols[6].label(text="Port is hidden")
|
||||
else:
|
||||
blank4 = cols[4].column(align=True)
|
||||
blank4.scale_x = 0.1
|
||||
blank4.label(text="", icon="BLANK1")
|
||||
blank5 = cols[5].column(align=True)
|
||||
blank5.scale_x = 0.1
|
||||
blank5.label(text="", icon="BLANK1")
|
||||
cols[6].label(text="")
|
||||
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(connected_obj)
|
||||
cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
|
||||
cols[5].label(text=port_data["connected_obj_name"])
|
||||
cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
|
||||
cols[8].label(text=port_data["connected_obj_name"])
|
||||
else:
|
||||
cols[3].label(text="", icon="UNLINKED")
|
||||
cols[4].label(text="", icon="BLANK1")
|
||||
cols[5].label(text="Port is disconnected")
|
||||
blank4 = cols[4].column(align=True)
|
||||
blank4.scale_x = 0.1
|
||||
blank4.label(text="", icon="BLANK1")
|
||||
blank5 = cols[5].column(align=True)
|
||||
blank5.scale_x = 0.1
|
||||
blank5.label(text="", icon="BLANK1")
|
||||
|
||||
cols[6].label(text="Port is disconnected")
|
||||
|
||||
blank7 = cols[7].column(align=True)
|
||||
blank7.scale_x = 0.1
|
||||
blank7.label(text="", icon="BLANK1")
|
||||
blank8 = cols[8].column(align=True)
|
||||
blank8.scale_x = 0.1
|
||||
blank8.label(text="", icon="BLANK1")
|
||||
|
||||
|
||||
class BIM_PT_port(Panel):
|
||||
@@ -223,14 +266,8 @@ class BIM_PT_port(Panel):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
self.props = tool.System.get_system_props()
|
||||
|
||||
layout = self.layout
|
||||
row = layout.row(align=True)
|
||||
row.label(text="IfcDistributionPort")
|
||||
row.operator("bim.connect_port", icon="PLUGIN", text="")
|
||||
row.operator("bim.disconnect_port", icon="UNLINKED", text="")
|
||||
row.operator("bim.remove_port", icon="X", text="")
|
||||
|
||||
if not PortData.is_loaded:
|
||||
PortData.load()
|
||||
@@ -238,42 +275,56 @@ class BIM_PT_port(Panel):
|
||||
if not PortData.data["is_port"]:
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
current_flow_direction = str(element.FlowDirection)
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Flow Direction:")
|
||||
row.label(text=current_flow_direction)
|
||||
|
||||
# port located on
|
||||
row = layout.row(align=True)
|
||||
relating_object_name = PortData.data["port_relating_object_name"]
|
||||
relating_object = bpy.data.objects[relating_object_name]
|
||||
row.label(text="Port located on:")
|
||||
row.label(text=relating_object_name)
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(relating_object)
|
||||
row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
|
||||
row.label(text=f"IfcDistributionPort located in: {relating_object_name}")
|
||||
row.operator("bim.remove_port", icon="X", text="")
|
||||
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
|
||||
# object connected to the port
|
||||
row = layout.row(align=True)
|
||||
connected_object_name = PortData.data["port_connected_object_name"]
|
||||
if connected_object_name:
|
||||
connected_object = bpy.data.objects[connected_object_name]
|
||||
row.label(text="Port connected to:")
|
||||
row.label(text=connected_object_name)
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(connected_object)
|
||||
row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
|
||||
cols = [row.column(align=True) for i in range(9)]
|
||||
cols[3].scale_x = 1.0
|
||||
cols[6].scale_x = 1.0
|
||||
cols[8].scale_x = 1.33
|
||||
|
||||
flow_direction_icon = FLOW_DIRECTION_TO_ICON[element.FlowDirection or "NOTDEFINED"]
|
||||
connected_port = tool.System.get_connected_port(element)
|
||||
|
||||
if connected_port:
|
||||
cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED")
|
||||
op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
|
||||
op.port_id = element.id()
|
||||
else:
|
||||
row.label(text="Port is not connected to any element")
|
||||
cols[0].operator("bim.connect_port", icon="PLUGIN", text="")
|
||||
op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
|
||||
op.port_id = element.id()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Change Flow Direction:")
|
||||
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
|
||||
row.operator(
|
||||
"bim.set_flow_direction",
|
||||
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
|
||||
depress=flow_direction == current_flow_direction,
|
||||
text="",
|
||||
).direction = flow_direction
|
||||
cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = element.id()
|
||||
cols[3].label(text=context.active_object.name)
|
||||
|
||||
if connected_port:
|
||||
connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"]
|
||||
op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True)
|
||||
op.port_id = connected_port.id()
|
||||
cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id()
|
||||
connected_port_obj = tool.Ifc.get_object(connected_port)
|
||||
cols[6].label(text=connected_port_obj.name if connected_port_obj else "Hidden Port")
|
||||
|
||||
connected_object_name = PortData.data["port_connected_object_name"]
|
||||
if connected_object_name:
|
||||
connected_obj = bpy.data.objects[connected_object_name]
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(connected_obj)
|
||||
cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
|
||||
cols[8].label(text=connected_object_name)
|
||||
else:
|
||||
cols[7].label(text="", icon="BLANK1")
|
||||
cols[8].label(text="")
|
||||
else:
|
||||
cols[4].label(text="", icon="BLANK1")
|
||||
cols[5].label(text="", icon="BLANK1")
|
||||
cols[6].label(text="Port is disconnected")
|
||||
cols[7].label(text="", icon="BLANK1")
|
||||
cols[8].label(text="")
|
||||
|
||||
|
||||
class BIM_PT_flow_controls(Panel):
|
||||
|
||||
@@ -724,15 +724,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
chain_filter_with_set_operations: BoolProperty(
|
||||
name="NEW filter mode: Enable chained filters with set operations",
|
||||
name="NEW Filter mode: Enable chained filters with set operations",
|
||||
description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values",
|
||||
default=False,
|
||||
)
|
||||
default_filter_with_set_operations_for_globalid_and_class: BoolProperty(
|
||||
name="DEFAULT filter mode: Enable set operations for GlobalId/Class",
|
||||
description="Enable ADD/SUBTRACT/FILTER toggle buttons on entity (Class) and instance (GlobalId) filters for the DEFAULT filter mode",
|
||||
default=False,
|
||||
)
|
||||
|
||||
save_metadata_blend_file: BoolProperty(
|
||||
name="Save non ifc data to metadata blend File",
|
||||
@@ -783,7 +778,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
container_hide_show_isolate: bool
|
||||
mass_time_units_in_wizard: bool
|
||||
chain_filter_with_set_operations: bool
|
||||
default_filter_with_set_operations_for_globalid_and_class: bool
|
||||
save_metadata_blend_file: bool
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
@@ -979,14 +973,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "container_hide_show_isolate")
|
||||
layout.prop(self, "mass_time_units_in_wizard")
|
||||
layout.label(text="Filtering modes:")
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "chain_filter_with_set_operations")
|
||||
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
|
||||
row = box.row(align=True)
|
||||
row.prop(self, "default_filter_with_set_operations_for_globalid_and_class")
|
||||
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/comment/27030"
|
||||
layout.prop(self, "save_metadata_blend_file")
|
||||
if self.save_metadata_blend_file:
|
||||
row = layout.row()
|
||||
|
||||
@@ -123,9 +123,8 @@ def hide_ports(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopens
|
||||
|
||||
def add_port(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopenshell.entity_instance) -> None:
|
||||
system.load_ports(element, system.get_ports(element))
|
||||
obj = system.create_empty_at_cursor_with_element_orientation(element)
|
||||
port = system.run_root_assign_class(obj=obj, ifc_class="IfcDistributionPort", should_add_representation=False)
|
||||
ifc.run("system.assign_port", element=element, port=port)
|
||||
port = system.create_port_at_cursor(element)
|
||||
system.load_ports(element, [port])
|
||||
|
||||
|
||||
def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshell.entity_instance) -> None:
|
||||
@@ -133,8 +132,13 @@ def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshe
|
||||
ifc.run("root.remove_product", product=port)
|
||||
|
||||
|
||||
def connect_port(ifc: type[tool.Ifc], port1: ifcopenshell.entity_instance, port2: ifcopenshell.entity_instance) -> None:
|
||||
ifc.run("system.connect_port", port1=port1, port2=port2)
|
||||
def connect_port(
|
||||
ifc: type[tool.Ifc],
|
||||
port1: ifcopenshell.entity_instance,
|
||||
port2: ifcopenshell.entity_instance,
|
||||
direction: str = "NOTDEFINED",
|
||||
) -> None:
|
||||
ifc.run("system.connect_port", port1=port1, port2=port2, direction=direction)
|
||||
|
||||
|
||||
def disconnect_port(ifc: type[tool.Ifc], port: ifcopenshell.entity_instance) -> None:
|
||||
|
||||
@@ -1082,6 +1082,7 @@ class Surveyor:
|
||||
@interface
|
||||
class System:
|
||||
def create_empty_at_cursor_with_element_orientation(cls, element): pass
|
||||
def create_port_at_cursor(cls, system): pass
|
||||
def delete_element_objects(cls, elements): pass
|
||||
def disable_editing_system(cls): pass
|
||||
def disable_system_editing_ui(cls): pass
|
||||
|
||||
@@ -50,12 +50,12 @@ class Collector(bonsai.core.tool.Collector):
|
||||
tool.Geometry.lock_object(obj)
|
||||
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
if not tool.Spatial.get_grid_props().is_visible:
|
||||
obj.hide_set(True)
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcGrid"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
tool.Geometry.lock_object(obj)
|
||||
if not tool.Spatial.get_grid_props().is_visible:
|
||||
obj.hide_set(True)
|
||||
obj.hide_viewport = True
|
||||
|
||||
if element.is_a("IfcProject"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
@@ -71,6 +71,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
tool.Geometry.lock_object(obj)
|
||||
collection = cls._create_project_child_collection("IfcSpace")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcStructuralItem"):
|
||||
collection = cls._create_project_child_collection("IfcStructuralItem")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
|
||||
@@ -66,8 +66,8 @@ if TYPE_CHECKING:
|
||||
BIMRailingProperties,
|
||||
BIMExternalParametricGeometryProperties,
|
||||
BIMPolylineProperties,
|
||||
BIMProductPreviewProperties,
|
||||
)
|
||||
from sverchok.core.node_group import SvGroupTreeNode
|
||||
|
||||
|
||||
class Model(bonsai.core.tool.Model):
|
||||
@@ -108,11 +108,6 @@ class Model(bonsai.core.tool.Model):
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMPolylineProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_product_preview_props(cls) -> BIMProductPreviewProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMProductPreviewProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def convert_si_to_unit(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
@@ -2571,13 +2566,21 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
return [s for s in group_node.inputs if s.type != "GEOMETRY"]
|
||||
|
||||
@classmethod
|
||||
def get_ifcsverchok_group_node(cls, node_tree: sverchok.node_tree.SverchCustomTree) -> SvGroupTreeNode:
|
||||
from sverchok.core.node_group import SvGroupTreeNode
|
||||
|
||||
return next(n for n in node_tree.nodes if isinstance(n, SvGroupTreeNode) and n.label == "BBIM_EPG")
|
||||
|
||||
@classmethod
|
||||
def get_ifcsverchok_shape_output(
|
||||
cls, node_tree: sverchok.node_tree.SverchCustomTree
|
||||
) -> ifcsverchok.nodes.ifc.shape_builder.shape_output.SvSbShapeOutput:
|
||||
from ifcsverchok.nodes.ifc.shape_builder.shape_output import SvSbShapeOutput
|
||||
|
||||
return next(n for n in node_tree.nodes if isinstance(n, SvSbShapeOutput))
|
||||
group_node = cls.get_ifcsverchok_group_node(node_tree)
|
||||
subtree = group_node.node_tree
|
||||
return next(n for n in subtree.nodes if isinstance(n, SvSbShapeOutput))
|
||||
|
||||
@classmethod
|
||||
def update_mesh_from_sverchok(
|
||||
@@ -2750,3 +2753,20 @@ class Model(bonsai.core.tool.Model):
|
||||
list(nodes_to_update)
|
||||
finally:
|
||||
SvIfcStore.use_bonsai_file = False
|
||||
|
||||
@classmethod
|
||||
def create_bmesh_from_vertices(cls, vertices, is_closed=False):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in vertices]
|
||||
if is_closed:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
new_edges.append(
|
||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||
) # Add an edge between the last an first point to make it closed.
|
||||
else:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
return bm
|
||||
|
||||
@@ -132,10 +132,7 @@ class Search(bonsai.core.tool.Search):
|
||||
else:
|
||||
value = ifc_filter.value
|
||||
|
||||
if (
|
||||
preferences.chain_filter_with_set_operations
|
||||
or preferences.default_filter_with_set_operations_for_globalid_and_class
|
||||
):
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
value = value.lstrip("!")
|
||||
if ifc_filter.filter_mode == "SUBTRACT":
|
||||
value = f"!{value}"
|
||||
@@ -144,10 +141,7 @@ class Search(bonsai.core.tool.Search):
|
||||
elif ifc_filter.type == "entity":
|
||||
value = ifc_filter.value
|
||||
|
||||
if (
|
||||
preferences.chain_filter_with_set_operations
|
||||
or preferences.default_filter_with_set_operations_for_globalid_and_class
|
||||
):
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
value = value.lstrip("!")
|
||||
if ifc_filter.filter_mode == "SUBTRACT":
|
||||
value = f"!{value}"
|
||||
@@ -215,19 +209,10 @@ class Search(bonsai.core.tool.Search):
|
||||
if filter_index == 0:
|
||||
mode = "ADD"
|
||||
else:
|
||||
if ifc_filter.type in ["entity", "instance"]:
|
||||
if (
|
||||
preferences.chain_filter_with_set_operations
|
||||
or preferences.default_filter_with_set_operations_for_globalid_and_class
|
||||
):
|
||||
mode = ifc_filter.filter_mode
|
||||
else:
|
||||
mode = "FILTER" if group_results else "ADD"
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
mode = ifc_filter.filter_mode
|
||||
else:
|
||||
if preferences.chain_filter_with_set_operations:
|
||||
mode = ifc_filter.filter_mode
|
||||
else:
|
||||
mode = "FILTER" if group_results else "ADD"
|
||||
mode = "FILTER" if group_results else "ADD"
|
||||
|
||||
if mode == "ADD":
|
||||
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
|
||||
|
||||
@@ -1213,18 +1213,18 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
for element in elements:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if obj.hide_viewport is True and is_visible:
|
||||
obj.hide_set(False)
|
||||
obj.hide_viewport = False
|
||||
elif obj.hide_viewport is False and not is_visible:
|
||||
obj.hide_set(True)
|
||||
obj.hide_viewport = True
|
||||
|
||||
@classmethod
|
||||
def set_grid_visibility(cls, is_visible: bool) -> None:
|
||||
for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if obj.hide_viewport is True and is_visible:
|
||||
obj.hide_set(False)
|
||||
obj.hide_viewport = False
|
||||
elif obj.hide_viewport is False and not is_visible:
|
||||
obj.hide_set(True)
|
||||
obj.hide_viewport = True
|
||||
|
||||
@classmethod
|
||||
def toggle_spaces_visibility_wired_and_textured(cls, spaces: list[ifcopenshell.entity_instance]) -> None:
|
||||
|
||||
@@ -100,6 +100,7 @@ class System(bonsai.core.tool.System):
|
||||
|
||||
@classmethod
|
||||
def create_empty_at_cursor_with_element_orientation(cls, element: ifcopenshell.entity_instance) -> bpy.types.Object:
|
||||
# Is this necessary anymore?
|
||||
element_obj = tool.Ifc.get_object(element)
|
||||
obj = bpy.data.objects.new("Port", None)
|
||||
obj.matrix_world = element_obj.matrix_world
|
||||
@@ -107,6 +108,26 @@ class System(bonsai.core.tool.System):
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def create_port_at_cursor(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
ifc_file = tool.Ifc.get()
|
||||
element_obj = tool.Ifc.get_object(element)
|
||||
|
||||
port = ifcopenshell.api.system.add_port(ifc_file, element=element)
|
||||
port.FlowDirection = "NOTDEFINED"
|
||||
port.PredefinedType = "USERDEFINED"
|
||||
|
||||
systems = ifcopenshell.util.system.get_element_systems(element)
|
||||
system = systems[0] if systems else None
|
||||
port.SystemType = getattr(system, "PredefinedType", None) or "USERDEFINED"
|
||||
|
||||
matrix = element_obj.matrix_world.copy()
|
||||
matrix.translation = bpy.context.scene.cursor.matrix.translation
|
||||
|
||||
ifcopenshell.api.geometry.edit_object_placement(ifc_file, product=port, matrix=matrix, is_si=True)
|
||||
|
||||
return port
|
||||
|
||||
@classmethod
|
||||
def delete_element_objects(cls, elements: list[ifcopenshell.entity_instance]) -> None:
|
||||
for element in elements:
|
||||
@@ -192,11 +213,22 @@ class System(bonsai.core.tool.System):
|
||||
ifc_importer.process_context_filter()
|
||||
ifc_importer.create_generic_elements(set(ports_to_create))
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection
|
||||
ifc_importer.collections[container.GlobalId] = collection
|
||||
ifc_importer.place_objects_in_collections()
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
target_collection = None
|
||||
for collection in obj.users_collection:
|
||||
target_collection = collection
|
||||
break
|
||||
|
||||
if target_collection:
|
||||
for port_obj in ifc_importer.added_data.values():
|
||||
if isinstance(port_obj, bpy.types.Object):
|
||||
tool.Collector.link_collection_object_safe(target_collection, port_obj)
|
||||
else:
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection
|
||||
ifc_importer.collections[container.GlobalId] = collection
|
||||
ifc_importer.place_objects_in_collections()
|
||||
|
||||
for port_obj in ifc_importer.added_data.values():
|
||||
assert isinstance(port_obj, bpy.types.Object)
|
||||
|
||||
@@ -337,7 +337,7 @@ def an_untestable_scenario():
|
||||
def an_empty_blender_session():
|
||||
IfcStore.purge()
|
||||
if not PYTEST_BLENDER_NO_BACKGROUND:
|
||||
bpy.ops.wm.read_homefile(app_template="")
|
||||
bpy.ops.wm.read_homefile(app_template="", use_factory_startup=True)
|
||||
if len(bpy.data.objects) > 0:
|
||||
bpy.data.batch_remove(bpy.data.objects)
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
@@ -153,11 +153,10 @@ class TestAddPort:
|
||||
def test_run(self, ifc, system):
|
||||
system.get_ports("element").should_be_called().will_return(["port"])
|
||||
system.load_ports("element", ["port"]).should_be_called()
|
||||
system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj")
|
||||
system.run_root_assign_class(
|
||||
obj="obj", ifc_class="IfcDistributionPort", should_add_representation=False
|
||||
).should_be_called().will_return("port")
|
||||
ifc.run("system.assign_port", element="element", port="port").should_be_called()
|
||||
#system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj")
|
||||
#system.run_root_assign_class(obj="obj", ifc_class="IfcDistributionPort", should_add_representation=False).should_be_called().will_return("port")
|
||||
system.create_port_at_cursor("element").should_be_called().will_return("port")
|
||||
system.load_ports("element", ["port"]).should_be_called()
|
||||
subject.add_port(ifc, system, element="element")
|
||||
|
||||
|
||||
|
||||
@@ -103,6 +103,19 @@ class TestCreateEmptyAtCursorWithElementOrientation(NewFile):
|
||||
obj = subject.create_empty_at_cursor_with_element_orientation(element)
|
||||
assert obj.matrix_world == bpy.context.scene.cursor.matrix
|
||||
|
||||
class TestCreatePortAtCursor(NewFile):
|
||||
def test_run(self):
|
||||
assert bpy.context.scene
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc().set(ifc)
|
||||
system = ifcopenshell.api.system.add_system(ifc)
|
||||
element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcDuctSegment")
|
||||
ifcopenshell.api.system.assign_system(ifc, products=[element], system=system)
|
||||
obj = tool.Ifc.link(element, bpy.data.objects.new("Object", None))
|
||||
port = subject.create_port_at_cursor(element)
|
||||
assert port.is_a("IfcDistributionPort")
|
||||
assert ifcopenshell.util.system.get_ports(element) == [port]
|
||||
|
||||
|
||||
class TestDeleteElementObjects(NewFile):
|
||||
def test_run(self):
|
||||
|
||||
@@ -184,6 +184,78 @@ class ClassPropertyContractV1(TypedDict):
|
||||
qudtCodes: NotRequired[list[str]]
|
||||
|
||||
|
||||
class ClassRelationItemContractV1(TypedDict):
|
||||
relationType: str
|
||||
classUri: str
|
||||
className: NotRequired[str]
|
||||
fraction: NotRequired[float]
|
||||
dictionaryUri: NotRequired[str]
|
||||
|
||||
|
||||
class ClassRelationsContractV1(TypedDict):
|
||||
totalCount: NotRequired[int]
|
||||
offset: NotRequired[int]
|
||||
count: NotRequired[int]
|
||||
classUri: NotRequired[str]
|
||||
areReversedRelations: NotRequired[bool]
|
||||
classRelations: NotRequired[list[ClassRelationItemContractV1]]
|
||||
|
||||
|
||||
class ClassPropertiesContractV1(TypedDict):
|
||||
classUri: NotRequired[str]
|
||||
totalCount: NotRequired[int]
|
||||
offset: NotRequired[int]
|
||||
count: NotRequired[int]
|
||||
classProperties: list[ClassPropertyContractV1]
|
||||
|
||||
|
||||
class ClassPropertyItemContractV1(TypedDict):
|
||||
name: str
|
||||
propertySet: str
|
||||
uri: str
|
||||
description: NotRequired[str]
|
||||
definition: NotRequired[str]
|
||||
dataType: NotRequired[str]
|
||||
dimension: NotRequired[str]
|
||||
dimensionLength: NotRequired[int]
|
||||
dimensionMass: NotRequired[int]
|
||||
dimensionTime: NotRequired[int]
|
||||
dimensionElectricCurrent: NotRequired[int]
|
||||
dimensionThermodynamicTemperature: NotRequired[int]
|
||||
dimensionAmountOfSubstance: NotRequired[int]
|
||||
dimensionLuminousIntensity: NotRequired[int]
|
||||
dynamicParameterPropertyCodes: NotRequired[list[str]]
|
||||
example: NotRequired[str]
|
||||
isDynamic: NotRequired[bool]
|
||||
isRequired: NotRequired[bool]
|
||||
isWritable: NotRequired[bool]
|
||||
maxExclusive: NotRequired[float]
|
||||
maxInclusive: NotRequired[float]
|
||||
minExclusive: NotRequired[float]
|
||||
minInclusive: NotRequired[float]
|
||||
pattern: NotRequired[str]
|
||||
physicalQuantity: NotRequired[str]
|
||||
allowedValues: NotRequired[list[ClassPropertyValueItemContractV1]]
|
||||
predefinedValue: NotRequired[str]
|
||||
propertyCode: NotRequired[str]
|
||||
propertyDictionaryName: NotRequired[str]
|
||||
propertyDictionaryUri: NotRequired[str]
|
||||
propertyUri: NotRequired[str]
|
||||
propertyStatus: NotRequired[str]
|
||||
propertyValueKind: NotRequired[str]
|
||||
symbol: NotRequired[str]
|
||||
units: NotRequired[list[str]]
|
||||
qudtCodes: NotRequired[list[str]]
|
||||
|
||||
|
||||
class ClassPropertyValueItemContractV1(TypedDict):
|
||||
uri: NotRequired[str]
|
||||
code: NotRequired[str]
|
||||
value: str
|
||||
description: NotRequired[str]
|
||||
sortNumber: NotRequired[int]
|
||||
|
||||
|
||||
class PropertyContractV5(TypedDict):
|
||||
dictionaryUri: NotRequired[str]
|
||||
activationDateUtc: str
|
||||
@@ -710,6 +782,56 @@ class Client:
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
return self.get(endpoint, params)
|
||||
|
||||
def get_class_relations(
|
||||
self,
|
||||
class_uri: str,
|
||||
get_reverse_relations: bool = False,
|
||||
search_text: str = "",
|
||||
offset: int = 0,
|
||||
limit: int = 1000,
|
||||
language_code: str = "",
|
||||
version=1,
|
||||
) -> ClassRelationsContractV1:
|
||||
"""
|
||||
Get class relations or reverse relations (paginated)
|
||||
"""
|
||||
endpoint = f"Class/Relations/v{version}"
|
||||
params = {
|
||||
"ClassUri": class_uri,
|
||||
"GetReverseRelations": get_reverse_relations,
|
||||
"SearchText": search_text,
|
||||
"Offset": offset,
|
||||
"Limit": limit,
|
||||
"languageCode": language_code,
|
||||
}
|
||||
return self.get(endpoint, params)
|
||||
|
||||
def get_class_properties(
|
||||
self,
|
||||
class_uri: str,
|
||||
property_set: str = "",
|
||||
property_code: str = "",
|
||||
search_text: str = "",
|
||||
offset: int = 0,
|
||||
limit: int = 1000,
|
||||
language_code: str = "",
|
||||
version=1,
|
||||
) -> ClassPropertiesContractV1:
|
||||
"""
|
||||
Get class properties (paginated)
|
||||
"""
|
||||
endpoint = f"Class/Properties/v{version}"
|
||||
params = {
|
||||
"ClassUri": class_uri,
|
||||
"PropertySet": property_set,
|
||||
"PropertyCode": property_code,
|
||||
"SearchText": search_text,
|
||||
"Offset": offset,
|
||||
"Limit": limit,
|
||||
"languageCode": language_code,
|
||||
}
|
||||
return self.get(endpoint, params)
|
||||
|
||||
def get_property(self, uri, language_code="", version: int = 5) -> PropertyContractV5:
|
||||
"""
|
||||
Get Property details.
|
||||
|
||||
@@ -37,6 +37,22 @@ def test_get_class():
|
||||
]
|
||||
|
||||
|
||||
def test_get_class_relations():
|
||||
uri_light_fixture = next(l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"])["uri"]
|
||||
ifc4x3_light_fixture_relations = client.get_class_properties(uri_light_fixture, True)
|
||||
assert "Electrical unit for light-line system" and "Tubelight system" in [
|
||||
r["className"] for r in ifc4x3_light_fixture_relations["classRelations"]
|
||||
]
|
||||
|
||||
|
||||
def test_get_class_properties():
|
||||
uri_light_fixture = next(l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"])["uri"]
|
||||
ifc4x3_light_fixture_properties = client.get_class_properties(uri_light_fixture)
|
||||
assert "Maintenance Factor" and "Light Fixture Mounting Type" in [
|
||||
l["name"] for l in ifc4x3_light_fixture_properties["classProperties"]
|
||||
]
|
||||
|
||||
|
||||
def test_search_class():
|
||||
ss_heat_pump_sys = client.search_class("Ss_60_40_36", [nbs_uri])
|
||||
li = [l + "source heat pump systems" for l in ["Air ", "Ground ", "Water "]]
|
||||
|
||||
@@ -340,8 +340,12 @@ In IFC, meshes may be stored as **Faceted BReps**, **Tessellations**, or
|
||||
|
||||
# These vertices and faces represent a 2m square 1m high pyramid in SI units.
|
||||
# Note how they are nested lists. Each nested list represents a "mesh". There may be multiple meshes.
|
||||
vertices = [[(0.,0.,0.), (0.,2.,0.), (2.,2.,0.), (2.,0.,0.), (1.,1.,1.)]]
|
||||
faces = [[(0,1,2,3), (0,4,1), (1,4,2), (2,4,3), (3,4,0)]]
|
||||
vertices = [
|
||||
[(0.,0.,0.), (0.,2.,0.), (2.,2.,0.), (2.,0.,0.), (1.,1.,1.)] # A single mesh
|
||||
]
|
||||
faces = [
|
||||
[(0,1,2,3), (0,4,1), (1,4,2), (2,4,3), (3,4,0)] # A single mesh
|
||||
]
|
||||
representation = ifcopenshell.api.geometry.add_mesh_representation(model, context=body, vertices=vertices, faces=faces)
|
||||
|
||||
.. image:: images/mesh-representation.png
|
||||
|
||||
@@ -381,6 +381,7 @@ class Usecase:
|
||||
def append_type_product(self):
|
||||
self.whitelisted_inverse_attributes = {
|
||||
"IfcObjectDefinition": ["HasAssociations"],
|
||||
"IfcDistributionElementType": ["IsNestedBy"],
|
||||
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
|
||||
"IfcRepresentationItem": ["StyledByItem", "LayerAssignment"],
|
||||
"IfcRepresentation": ["LayerAssignments"],
|
||||
@@ -397,6 +398,7 @@ class Usecase:
|
||||
"IfcObjectDefinition": ["HasAssociations"],
|
||||
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
|
||||
"IfcElement": ["HasOpenings"],
|
||||
"IfcDistributionElement": ["IsNestedBy"],
|
||||
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
|
||||
"IfcRepresentationItem": [
|
||||
"StyledByItem",
|
||||
@@ -569,6 +571,8 @@ class Usecase:
|
||||
return False
|
||||
elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None:
|
||||
return False
|
||||
elif element.is_a("IfcDistributionPort"):
|
||||
return False
|
||||
elif element.is_a(self.target_class):
|
||||
return True
|
||||
elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"):
|
||||
|
||||
@@ -65,8 +65,6 @@ def disconnect_port(file: ifcopenshell.file, port: ifcopenshell.entity_instance)
|
||||
rels += port.ConnectedFrom or ()
|
||||
|
||||
for rel in rels:
|
||||
rel.RelatingPort.FlowDirection = None
|
||||
rel.RelatedPort.FlowDirection = None
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
if history:
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Literal
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from sverchok.core.sockets import SvVerticesSocket
|
||||
from sverchok.data_structure import updateNode
|
||||
from sverchok.node_tree import SverchCustomTreeNode
|
||||
|
||||
@@ -62,6 +63,7 @@ class SvIfcSbExtrude(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.Sv
|
||||
self.inputs,
|
||||
"Position",
|
||||
data_type="list[list[tuple[float, float, float]]]",
|
||||
socket_type=SvVerticesSocket,
|
||||
)
|
||||
helper.create_socket(
|
||||
self.outputs,
|
||||
|
||||
@@ -300,23 +300,15 @@ class Specification:
|
||||
if not is_applicable:
|
||||
continue
|
||||
self.applicable_entities.append(element)
|
||||
for facet in self.requirements:
|
||||
result = facet(element)
|
||||
is_pass = bool(result)
|
||||
if self.maxOccurs != 0: # This is a required or optional specification
|
||||
if is_pass:
|
||||
if self.maxOccurs != 0: # Requirements are skipped for prohibited applicability
|
||||
for facet in self.requirements:
|
||||
result = facet(element)
|
||||
if bool(result):
|
||||
self.passed_entities.add(element)
|
||||
facet.passed_entities.add(element)
|
||||
else:
|
||||
self.failed_entities.add(element)
|
||||
facet.failures.append(FacetFailure(element=element, reason=str(result)))
|
||||
else: # This is a prohibited specification
|
||||
if is_pass:
|
||||
self.failed_entities.add(element)
|
||||
facet.failures.append(FacetFailure(element=element, reason=str(result)))
|
||||
else:
|
||||
self.passed_entities.add(element)
|
||||
facet.passed_entities.add(element)
|
||||
|
||||
self.status = True
|
||||
for facet in self.requirements:
|
||||
|
||||
@@ -81,10 +81,12 @@ class ResultsSpecification(TypedDict):
|
||||
description: str
|
||||
instructions: str
|
||||
status: bool
|
||||
is_skipped: bool
|
||||
is_ifc_version: bool
|
||||
total_applicable: int
|
||||
total_applicable_pass: int
|
||||
total_applicable_fail: int
|
||||
applicable_entities: list[ResultsEntity]
|
||||
percent_applicable_pass: ResultsPercent
|
||||
total_checks: int
|
||||
total_checks_pass: int
|
||||
@@ -367,16 +369,24 @@ class Json(Reporter):
|
||||
cardinality = "optional"
|
||||
elif specification.minOccurs == 0 and specification.maxOccurs == 0:
|
||||
cardinality = "prohibited"
|
||||
elif specification.minOccurs >= 1:
|
||||
# Any minimum occurrence >= 1 means the specification is required
|
||||
cardinality = "required"
|
||||
else:
|
||||
# minOccurs == 0 with any other maxOccurs value means optional
|
||||
cardinality = "optional"
|
||||
|
||||
return ResultsSpecification(
|
||||
name=specification.name,
|
||||
description=specification.description,
|
||||
instructions=specification.instructions,
|
||||
status=specification.status,
|
||||
is_skipped=cardinality == "optional" and total_checks == 0,
|
||||
is_ifc_version=specification.is_ifc_version,
|
||||
total_applicable=total_applicable,
|
||||
total_applicable_pass=total_applicable_pass,
|
||||
total_applicable_fail=total_applicable - total_applicable_pass,
|
||||
applicable_entities=self.report_applicable_entities(specification),
|
||||
percent_applicable_pass=percent_applicable_pass,
|
||||
total_checks=total_checks,
|
||||
total_checks_pass=total_checks_pass,
|
||||
@@ -387,6 +397,24 @@ class Json(Reporter):
|
||||
requirements=requirements,
|
||||
)
|
||||
|
||||
def report_applicable_entities(self, specification: Specification) -> list[ResultsEntity]:
|
||||
return [
|
||||
ResultsEntity(
|
||||
{
|
||||
"element": e,
|
||||
"element_type": ifcopenshell.util.element.get_type(e),
|
||||
"class": e.is_a(),
|
||||
"predefined_type": ifcopenshell.util.element.get_predefined_type(e),
|
||||
"name": getattr(e, "Name", None),
|
||||
"description": getattr(e, "Description", None),
|
||||
"id": e.id(),
|
||||
"global_id": getattr(e, "GlobalId", None),
|
||||
"tag": getattr(e, "Tag", None),
|
||||
}
|
||||
)
|
||||
for e in specification.applicable_entities
|
||||
]
|
||||
|
||||
def report_passed_entities(self, requirement: Facet) -> list[ResultsEntity]:
|
||||
return [
|
||||
ResultsEntity(
|
||||
@@ -448,11 +476,13 @@ class Html(Json):
|
||||
def report(self) -> None:
|
||||
super().report()
|
||||
for spec in self.results["specifications"]:
|
||||
if spec["cardinality"] == "optional" and spec["total_checks"] == 0:
|
||||
spec["is_skipped"] = True
|
||||
spec["is_prohibited"] = spec["cardinality"] == "prohibited"
|
||||
spec["cardinality"] = spec["cardinality"].capitalize()
|
||||
spec["has_requirements"] = bool(spec["requirements"])
|
||||
total_applicable_entities = len(spec["applicable_entities"])
|
||||
spec["applicable_entities"] = self.limit_entities(spec["applicable_entities"])
|
||||
spec["has_omitted_applicable"] = total_applicable_entities > self.entity_limit
|
||||
spec["total_omitted_applicable"] = total_applicable_entities - self.entity_limit
|
||||
for requirement in spec["requirements"]:
|
||||
total_passed_entities = len(requirement["passed_entities"])
|
||||
total_failed_entities = len(requirement["failed_entities"])
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
<p>
|
||||
<strong>Requirements</strong>
|
||||
</p>
|
||||
{{/has_requirements}}
|
||||
<ol>
|
||||
{{#requirements}}
|
||||
<li class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}}">
|
||||
@@ -252,6 +251,45 @@
|
||||
</li>
|
||||
{{/requirements}}
|
||||
</ol>
|
||||
{{/has_requirements}}
|
||||
{{#is_prohibited}}
|
||||
{{#total_applicable}}
|
||||
<table class="fail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>PredefinedType</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>GlobalId</th>
|
||||
<th>Tag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#applicable_entities}}
|
||||
<tr>
|
||||
<td>{{class}}</td>
|
||||
<td>{{predefined_type}}</td>
|
||||
<td>{{name}}</td>
|
||||
<td>{{description}}</td>
|
||||
<td>{{global_id}}</td>
|
||||
<td>{{tag}}</td>
|
||||
</tr>
|
||||
{{#extra_of_type}}
|
||||
<tr>
|
||||
<td colspan="7">... {{extra_of_type}} more of the same element type ({{type_name}} with Tag {{type_tag}} and GlobalId {{type_global_id}}) not shown ...</td>
|
||||
</tr>
|
||||
{{/extra_of_type}}
|
||||
{{/applicable_entities}}
|
||||
{{#has_omitted_applicable}}
|
||||
<tr>
|
||||
<td colspan="7"> ... {{total_omitted_applicable}} more failing elements not shown out of {{total_applicable}} total ...</td>
|
||||
</tr>
|
||||
{{/has_omitted_applicable}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/total_applicable}}
|
||||
{{/is_prohibited}}
|
||||
</div>
|
||||
</section>
|
||||
{{/specifications}}
|
||||
|
||||
Generated
+8
-25
@@ -1955,9 +1955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
||||
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1967,22 +1967,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "dist/cjs/src/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
|
||||
@@ -2867,17 +2851,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
|
||||
"integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.0.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -50,13 +50,7 @@
|
||||
function getSpecificationStatus(specIndex, auditData) {
|
||||
const spec = auditData.specifications[specIndex];
|
||||
if (!spec) return null;
|
||||
|
||||
// If no applicable elements and no checks, and it passed, it's actually skipped
|
||||
if (spec.total_applicable === 0 && spec.total_checks === 0 && spec.status === true) {
|
||||
return 'skipped';
|
||||
}
|
||||
|
||||
return spec.status;
|
||||
return spec.is_skipped ? 'skipped' : spec.status;
|
||||
}
|
||||
|
||||
function getSpecificationStats(specIndex, auditData) {
|
||||
@@ -79,11 +73,13 @@
|
||||
const status = getSpecificationStatus(specIndex, auditData);
|
||||
|
||||
if (status === 'skipped') {
|
||||
return "Skipped because no applicable entities were found and the cardinality is OPTIONAL or PROHIBITED";
|
||||
return "Skipped because no applicable entities were found and the cardinality is OPTIONAL";
|
||||
}
|
||||
|
||||
if (status === false) { // Failed
|
||||
if (spec.total_applicable === 0) {
|
||||
if (spec.cardinality === 'prohibited') {
|
||||
return `Failed because ${spec.total_applicable} prohibited entities were found`;
|
||||
} else if (spec.total_applicable === 0) {
|
||||
return "Failed because no applicable entities were found but the cardinality is REQUIRED";
|
||||
} else {
|
||||
const failedChecks = spec.total_checks - spec.total_checks_pass;
|
||||
@@ -240,19 +236,30 @@
|
||||
{#if "@description" in spec}
|
||||
<p class="spec-description">{spec["@description"]}</p>
|
||||
{/if}
|
||||
<div class="spec-stats">
|
||||
{#if spec.applicability["@minOccurs"] === 1 && spec.applicability["@maxOccurs"] === 'unbounded'}
|
||||
<span class="stat-item">Required</span>
|
||||
{/if}
|
||||
{#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 'unbounded'}
|
||||
<span class="stat-item">Optional</span>
|
||||
{/if}
|
||||
{#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 0}
|
||||
<span class="stat-item">Prohibited</span>
|
||||
{/if}
|
||||
{#if auditReport}
|
||||
{@const stats = getSpecificationStats(index, auditReport.data)}
|
||||
{@const status = getSpecificationStatus(index, auditReport.data)}
|
||||
{#if stats && spec.applicability["@maxOccurs"] !== 0 && status !== 'skipped'}
|
||||
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
|
||||
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if auditReport}
|
||||
{@const reason = getSpecificationReason(index, auditReport.data)}
|
||||
{#if reason}
|
||||
<p class="spec-reason">{reason}</p>
|
||||
{/if}
|
||||
{@const stats = getSpecificationStats(index, auditReport.data)}
|
||||
{@const status = getSpecificationStatus(index, auditReport.data)}
|
||||
{#if stats && status !== 'skipped'}
|
||||
<div class="spec-stats">
|
||||
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
|
||||
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="spec-actions">
|
||||
@@ -292,9 +299,105 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if auditReport}
|
||||
{@const status = getSpecificationStatus(index, auditReport.data)}
|
||||
{#if ! status && spec.applicability["@maxOccurs"] == 0}
|
||||
{@const specReport = auditReport.data.specifications[index]}
|
||||
<div class="entity-tables">
|
||||
{#if specReport.applicable_entities && specReport.applicable_entities.length > 0}
|
||||
<div class="entity-table-section fail">
|
||||
<h4>Failed Elements ({specReport.applicable_entities.length})</h4>
|
||||
<div class="entity-table-container">
|
||||
<Tooltip.Provider>
|
||||
<table class="entity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>PredefinedType</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Warning</th>
|
||||
<th>GlobalId</th>
|
||||
<th>Tag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each specReport.applicable_entities.slice(0, 10) as entity}
|
||||
<tr>
|
||||
<td>{entity.class}</td>
|
||||
<td>{entity.predefined_type || '-'}</td>
|
||||
<td>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<div class="truncated-text">{entity.name || '-'}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{entity.name || '-'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div class="truncated-text">{entity.description || '-'}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{entity.description || '-'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div class="truncated-text">{entity.reason || '-'}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{entity.reason || '-'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<div class="truncated-text">{entity.global_id || '-'}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{entity.global_id || '-'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<div class="truncated-text">{entity.tag || '-'}</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{entity.tag || '-'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if specReport.applicable_entities.length > 10}
|
||||
<tr class="more-row">
|
||||
<td colspan="7">... {specReport.applicable_entities.length - 10} more failing elements not shown ...</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Requirements Section -->
|
||||
{#if Array.isArray(spec.requirements) && spec.requirements.length > 0}
|
||||
<div class="facet-section">
|
||||
<h3>Requirements</h3>
|
||||
|
||||
@@ -497,6 +600,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1128,4 +1232,4 @@
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user