Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-01-23 07:24:49 +01:00
42 changed files with 1443 additions and 922 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ dependencies = [
"black==25.12",
"ruff==0.14.13",
"poethepoet",
"gersemi==0.24",
"gersemi==0.25.1",
]
[tool.black]
+20 -27
View File
@@ -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(
+1 -10
View File
@@ -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()
+681 -59
View File
@@ -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:
+1 -583
View File
@@ -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()
+9 -1
View File
@@ -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()
+1 -1
View File
@@ -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",
+12 -18
View File
@@ -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,
+100 -4
View File
@@ -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]:
+119 -68
View File
@@ -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):
+2 -13
View File
@@ -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()
+9 -5
View File
@@ -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:
+1
View File
@@ -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
+4 -2
View File
@@ -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)
+27 -7
View File
@@ -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
+5 -20
View File
@@ -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)
+4 -4
View File
@@ -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:
+37 -5
View File
@@ -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)
+1 -1
View File
@@ -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)
+4 -5
View File
@@ -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")
+13
View File
@@ -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):
+122
View File
@@ -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.
+16
View File
@@ -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,
+4 -12
View File
@@ -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:
+32 -2
View File
@@ -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"])
+39 -1
View File
@@ -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}}
+8 -25
View File
@@ -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>