mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -51,9 +51,11 @@ from bonsai.bim.module.model.decorator import (
|
|||||||
BoundingBoxDecorator,
|
BoundingBoxDecorator,
|
||||||
DoorSwingReadonlyDecorator,
|
DoorSwingReadonlyDecorator,
|
||||||
MEPSegmentExtendPreviewDecorator,
|
MEPSegmentExtendPreviewDecorator,
|
||||||
|
MEPSystemPathDecorator,
|
||||||
SlabDirectionDecorator,
|
SlabDirectionDecorator,
|
||||||
WallAxisDecorator,
|
WallAxisDecorator,
|
||||||
WallFilletPreviewDecorator,
|
WallFilletPreviewDecorator,
|
||||||
|
WallSystemPathDecorator,
|
||||||
)
|
)
|
||||||
from bonsai.bim.module.model.wall import WallGizmoPreviewDecorator
|
from bonsai.bim.module.model.wall import WallGizmoPreviewDecorator
|
||||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||||
@@ -513,6 +515,8 @@ def _install_viewport_overlays() -> None:
|
|||||||
NestDecorator.uninstall()
|
NestDecorator.uninstall()
|
||||||
WallAxisDecorator.uninstall()
|
WallAxisDecorator.uninstall()
|
||||||
SlabDirectionDecorator.uninstall()
|
SlabDirectionDecorator.uninstall()
|
||||||
|
MEPSystemPathDecorator.uninstall()
|
||||||
|
WallSystemPathDecorator.uninstall()
|
||||||
WallFilletPreviewDecorator.uninstall()
|
WallFilletPreviewDecorator.uninstall()
|
||||||
BendPreviewDecorator.uninstall()
|
BendPreviewDecorator.uninstall()
|
||||||
MEPSegmentExtendPreviewDecorator.uninstall()
|
MEPSegmentExtendPreviewDecorator.uninstall()
|
||||||
@@ -532,6 +536,9 @@ def _install_viewport_overlays() -> None:
|
|||||||
WallAxisDecorator.install(bpy.context)
|
WallAxisDecorator.install(bpy.context)
|
||||||
if model_props.show_slab_direction:
|
if model_props.show_slab_direction:
|
||||||
SlabDirectionDecorator.install(bpy.context)
|
SlabDirectionDecorator.install(bpy.context)
|
||||||
|
if model_props.show_paths:
|
||||||
|
MEPSystemPathDecorator.install(bpy.context)
|
||||||
|
WallSystemPathDecorator.install(bpy.context)
|
||||||
if model_props.show_bounding_box:
|
if model_props.show_bounding_box:
|
||||||
BoundingBoxDecorator.install(bpy.context)
|
BoundingBoxDecorator.install(bpy.context)
|
||||||
# Always-installed: draw() self-polls on Scene.BIMPreviewProperties.
|
# Always-installed: draw() self-polls on Scene.BIMPreviewProperties.
|
||||||
|
|||||||
@@ -378,6 +378,11 @@ def unregister():
|
|||||||
# half-unloaded module state.
|
# half-unloaded module state.
|
||||||
opening.DecorationsHandler.uninstall()
|
opening.DecorationsHandler.uninstall()
|
||||||
|
|
||||||
|
# Network path overlays attach SpaceView3D draw handlers on toggle;
|
||||||
|
# uninstall here so addon disable / Blender shutdown doesn't leak them.
|
||||||
|
decorator.MEPSystemPathDecorator.uninstall()
|
||||||
|
decorator.WallSystemPathDecorator.uninstall()
|
||||||
|
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
for tool_data in reversed(tools):
|
for tool_data in reversed(tools):
|
||||||
bpy.utils.unregister_tool(tool_data.tool)
|
bpy.utils.unregister_tool(tool_data.tool)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
from collections.abc import Sequence
|
||||||
from math import cos, pi, radians, sin, tan
|
from math import cos, pi, radians, sin, tan
|
||||||
from typing import Any, Literal, NamedTuple
|
from typing import Any, Literal, NamedTuple
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ from mathutils import Matrix, Quaternion, Vector
|
|||||||
|
|
||||||
import bonsai.core.geometry
|
import bonsai.core.geometry
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
from bonsai.bim.decorator_cache import TokenCache
|
||||||
from bonsai.bim.module.drawing.gizmos import (
|
from bonsai.bim.module.drawing.gizmos import (
|
||||||
ARC_SEGMENTS,
|
ARC_SEGMENTS,
|
||||||
DOOR_SWING_ANGLE_MAX,
|
DOOR_SWING_ANGLE_MAX,
|
||||||
@@ -62,6 +64,41 @@ def highlight_color(color, alpha=0.1):
|
|||||||
return color
|
return color
|
||||||
|
|
||||||
|
|
||||||
|
def _stroke_lines_alpha(
|
||||||
|
context: bpy.types.Context,
|
||||||
|
segments: list[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
color_rgb: tuple[float, float, float],
|
||||||
|
line_width: float,
|
||||||
|
line_alpha: float,
|
||||||
|
) -> None:
|
||||||
|
"""Render ``segments`` (a list of (start, end) tuples) as one anti-aliased
|
||||||
|
LINES batch in world space. Early-returns when ``context.region`` is
|
||||||
|
unavailable (e.g. when called from a ``_RestrictContext``)."""
|
||||||
|
if not segments:
|
||||||
|
return
|
||||||
|
verts: list[tuple[float, float, float]] = []
|
||||||
|
indices: list[tuple[int, int]] = []
|
||||||
|
for start, end in segments:
|
||||||
|
base = len(verts)
|
||||||
|
verts.append(tuple(start))
|
||||||
|
verts.append(tuple(end))
|
||||||
|
indices.append((base, base + 1))
|
||||||
|
if not tool.Blender.validate_shader_batch_data(verts, indices):
|
||||||
|
return
|
||||||
|
region = getattr(context, "region", None)
|
||||||
|
if region is None:
|
||||||
|
return
|
||||||
|
shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("viewportSize", (region.width, region.height))
|
||||||
|
shader.uniform_float("lineWidth", line_width)
|
||||||
|
shader.uniform_float("color", (*color_rgb, line_alpha))
|
||||||
|
batch = batch_for_shader(shader, "LINES", {"pos": verts}, indices=indices)
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
batch.draw(shader)
|
||||||
|
gpu.state.blend_set("NONE")
|
||||||
|
|
||||||
|
|
||||||
class ProfileDecorator:
|
class ProfileDecorator:
|
||||||
installed = None
|
installed = None
|
||||||
|
|
||||||
@@ -2529,3 +2566,461 @@ def draw_polyline_segments(
|
|||||||
|
|
||||||
_BBOX_HIGHLIGHT_LINE_WIDTH = 1.8
|
_BBOX_HIGHLIGHT_LINE_WIDTH = 1.8
|
||||||
_BBOX_HIGHLIGHT_LINE_ALPHA = 0.8
|
_BBOX_HIGHLIGHT_LINE_ALPHA = 0.8
|
||||||
|
|
||||||
|
|
||||||
|
class _ConnectedNetworkPathDecorator(tool.Blender.ViewportDecorator):
|
||||||
|
"""Shared scaffolding for "BFS-walk a connected IFC network from a selected
|
||||||
|
seed and overlay its schematic path" viewport decorators.
|
||||||
|
|
||||||
|
Subclasses implement three hooks:
|
||||||
|
|
||||||
|
``_is_seed_element(element)``: True if ``element`` can seed a walk
|
||||||
|
``_walk(start_element)``: list of network elements reachable from the seed
|
||||||
|
``_build_geometry(connected)``: ``(lines, free_points, connection_points)``
|
||||||
|
for one walk pass; free dots render in the base selected color,
|
||||||
|
connection dots in the "special" slot so junctions stand out
|
||||||
|
|
||||||
|
Lifecycle each redraw: gate on ``BIMModelProperties.show_paths`` (the
|
||||||
|
shared toggle for all network-path overlays), find the first selected
|
||||||
|
seed element, walk the network (cached per seed-GUID per IFC file), and
|
||||||
|
render lines + connection-node dots. Geometry is memoised through a
|
||||||
|
``TokenCache`` keyed on the decorator-cache token, so depsgraph / undo /
|
||||||
|
redo / load all invalidate the resolved world-space pass without
|
||||||
|
re-walking.
|
||||||
|
|
||||||
|
Install / uninstall is driven by the central addon-load handler and
|
||||||
|
by the toggle's ``update`` callback, so flipping the property takes
|
||||||
|
effect immediately without a Blender restart."""
|
||||||
|
|
||||||
|
# Network-path lines + junction dots render in ``decorator_color_selected``
|
||||||
|
# (Bonsai's palette slot for "what the user is currently inspecting"); free
|
||||||
|
# endpoints (dangling chain tips) switch to ``decorator_color_special`` so
|
||||||
|
# the end of the line stands apart from interior junctions at a glance.
|
||||||
|
LINE_WIDTH = 1.3
|
||||||
|
LINE_ALPHA = 0.85
|
||||||
|
# Sized larger than LINE_WIDTH so connection nodes read as discrete
|
||||||
|
# points rather than line thickenings.
|
||||||
|
DOT_SIZE = 4.0
|
||||||
|
# Squared distance under which two emitted dots are treated as the same
|
||||||
|
# connection node. In Blender units (typically meters), 1e-4 m ≈ 0.1 mm
|
||||||
|
# — below the precision at which two IFC reference-line endpoints would
|
||||||
|
# ever be authored as "the same join" but not so tight that float drift
|
||||||
|
# from coordinate composition misses a real coincidence.
|
||||||
|
CONNECTION_EPS_SQ = 1e-4 * 1e-4
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# Two-tier cache. Walk cache keyed on (start_guid, ifc_file): re-walk
|
||||||
|
# only on selection change or file reload. Compare ``ifc_file`` with
|
||||||
|
# ``is`` (not id()) so a GC-recycled id() can't produce a false hit.
|
||||||
|
self._cached_start_guid: str | None = None
|
||||||
|
self._cached_ifc_file: Any = None
|
||||||
|
self._cached_walk: list[Any] = []
|
||||||
|
# Geometry cache: shared TokenCache so resolved world-space lines +
|
||||||
|
# dots re-build on every depsgraph / undo / redo / load.
|
||||||
|
self._geom_cache: TokenCache[
|
||||||
|
tuple[
|
||||||
|
list[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
]
|
||||||
|
] = TokenCache()
|
||||||
|
# One-shot guards so a corrupted walk or build surfaces in the console
|
||||||
|
# once per decorator instance instead of every redraw.
|
||||||
|
self._walk_failure_logged: bool = False
|
||||||
|
self._build_failure_logged: bool = False
|
||||||
|
# Short-circuit re-running a known-broken walk or build for the same
|
||||||
|
# seed every frame; cleared the moment the user picks a different seed.
|
||||||
|
self._failed_seed_guid: str | None = None
|
||||||
|
|
||||||
|
_ABSTRACT_HOOKS = ("_is_seed_element", "_walk", "_build_geometry")
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
# Pin the template-method contract at class-definition time, mirroring
|
||||||
|
# ViewportDecorator's draw_method check: a subclass that forgets to
|
||||||
|
# override one of the three hooks would otherwise pass class creation
|
||||||
|
# and only raise NotImplementedError on the first walk — deferred long
|
||||||
|
# past the offending declaration.
|
||||||
|
missing = [
|
||||||
|
name for name in cls._ABSTRACT_HOOKS if getattr(cls, name) is getattr(_ConnectedNetworkPathDecorator, name)
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise TypeError(f"{cls.__name__}: must override abstract hook(s) {sorted(missing)}")
|
||||||
|
|
||||||
|
def _is_seed_element(self, element: Any) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _walk(self, start_element: Any) -> list[Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _build_geometry(
|
||||||
|
self,
|
||||||
|
connected: list[Any],
|
||||||
|
) -> tuple[
|
||||||
|
list[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
]:
|
||||||
|
"""Resolve world-space line segments + dots for one walk pass. Returns
|
||||||
|
``(lines, free_points, connection_points)`` — free dots get the base
|
||||||
|
selected color, connection dots get the special color so junctions
|
||||||
|
between two consecutive elements pop out. Never raises; skips
|
||||||
|
degenerate elements."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _partition_points_by_coincidence(
|
||||||
|
cls,
|
||||||
|
points: list[tuple[float, float, float]],
|
||||||
|
lines: Sequence[tuple[tuple[float, float, float], tuple[float, float, float]]] = (),
|
||||||
|
) -> tuple[list[tuple[float, float, float]], list[tuple[float, float, float]]]:
|
||||||
|
"""Split ``points`` into ``(free, connection)``. A point is "connection"
|
||||||
|
when (a) at least one other point in the list lies within
|
||||||
|
``CONNECTION_EPS_SQ`` (corner / end-to-end joins), or (b) it lies within
|
||||||
|
``CONNECTION_EPS_SQ`` of the interior of any segment in ``lines``
|
||||||
|
(T-junctions / ATPATH joins, where one wall's end lands on another
|
||||||
|
wall's axis interior rather than its endpoint). Connection points
|
||||||
|
dedupe to one representative each so coincident dots don't stack the
|
||||||
|
same color."""
|
||||||
|
eps_sq = cls.CONNECTION_EPS_SQ
|
||||||
|
n = len(points)
|
||||||
|
shared = [False] * n
|
||||||
|
for i in range(n):
|
||||||
|
xi, yi, zi = points[i]
|
||||||
|
for j in range(i + 1, n):
|
||||||
|
xj, yj, zj = points[j]
|
||||||
|
dx, dy, dz = xi - xj, yi - yj, zi - zj
|
||||||
|
if dx * dx + dy * dy + dz * dz <= eps_sq:
|
||||||
|
shared[i] = True
|
||||||
|
shared[j] = True
|
||||||
|
for i, point in enumerate(points):
|
||||||
|
if shared[i]:
|
||||||
|
continue
|
||||||
|
if cls._point_touches_any_segment_interior(point, lines, eps_sq):
|
||||||
|
shared[i] = True
|
||||||
|
free: list[tuple[float, float, float]] = []
|
||||||
|
connection: list[tuple[float, float, float]] = []
|
||||||
|
seen_connection: list[tuple[float, float, float]] = []
|
||||||
|
for i, point in enumerate(points):
|
||||||
|
if not shared[i]:
|
||||||
|
free.append(point)
|
||||||
|
continue
|
||||||
|
for existing in seen_connection:
|
||||||
|
dx, dy, dz = point[0] - existing[0], point[1] - existing[1], point[2] - existing[2]
|
||||||
|
if dx * dx + dy * dy + dz * dz <= eps_sq:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
seen_connection.append(point)
|
||||||
|
connection.append(point)
|
||||||
|
return free, connection
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _point_touches_any_segment_interior(
|
||||||
|
point: tuple[float, float, float],
|
||||||
|
lines: Sequence[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
eps_sq: float,
|
||||||
|
) -> bool:
|
||||||
|
"""True iff ``point`` lies within ``sqrt(eps_sq)`` of the interior of
|
||||||
|
any segment in ``lines``. Endpoints are excluded so a point cannot
|
||||||
|
match its own owning segment via either of that segment's tips — the
|
||||||
|
endpoint-coincidence pass already handles those cases. The qualifying
|
||||||
|
projection must land strictly inside the segment (``0 < t < 1``) AND
|
||||||
|
sit further than ``eps`` from either tip, catching ATPATH/T-junction
|
||||||
|
joins without false-flagging walls that share a corner."""
|
||||||
|
px, py, pz = point
|
||||||
|
for (ax, ay, az), (bx, by, bz) in lines:
|
||||||
|
dxa, dya, dza = px - ax, py - ay, pz - az
|
||||||
|
if dxa * dxa + dya * dya + dza * dza <= eps_sq:
|
||||||
|
continue
|
||||||
|
dxb, dyb, dzb = px - bx, py - by, pz - bz
|
||||||
|
if dxb * dxb + dyb * dyb + dzb * dzb <= eps_sq:
|
||||||
|
continue
|
||||||
|
ex, ey, ez = bx - ax, by - ay, bz - az
|
||||||
|
seg_len_sq = ex * ex + ey * ey + ez * ez
|
||||||
|
if seg_len_sq <= eps_sq:
|
||||||
|
continue
|
||||||
|
t = (dxa * ex + dya * ey + dza * ez) / seg_len_sq
|
||||||
|
if t <= 0.0 or t >= 1.0:
|
||||||
|
continue
|
||||||
|
qx, qy, qz = ax + t * ex, ay + t * ey, az + t * ez
|
||||||
|
dx, dy, dz = px - qx, py - qy, pz - qz
|
||||||
|
if dx * dx + dy * dy + dz * dz <= eps_sq:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def draw(self, context: bpy.types.Context) -> None:
|
||||||
|
model_props = tool.Model.get_model_props()
|
||||||
|
if not getattr(model_props, "show_paths", False):
|
||||||
|
return
|
||||||
|
ifc_file = tool.Ifc.get()
|
||||||
|
if ifc_file is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
start_element = None
|
||||||
|
active = context.active_object
|
||||||
|
if active is not None:
|
||||||
|
element = tool.Ifc.get_entity(active)
|
||||||
|
if element is not None and self._is_seed_element(element):
|
||||||
|
start_element = element
|
||||||
|
if start_element is None:
|
||||||
|
for obj in context.selected_objects or []:
|
||||||
|
if obj is active:
|
||||||
|
continue
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if element is None or not self._is_seed_element(element):
|
||||||
|
continue
|
||||||
|
start_element = element
|
||||||
|
break
|
||||||
|
if start_element is None:
|
||||||
|
self._cached_start_guid = None
|
||||||
|
self._cached_walk = []
|
||||||
|
return
|
||||||
|
|
||||||
|
start_guid = start_element.GlobalId
|
||||||
|
if start_guid == self._failed_seed_guid:
|
||||||
|
return
|
||||||
|
if start_guid == self._cached_start_guid and ifc_file is self._cached_ifc_file and self._cached_walk:
|
||||||
|
connected = self._cached_walk
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
connected = self._walk(start_element)
|
||||||
|
except Exception:
|
||||||
|
if not self._walk_failure_logged:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
self._walk_failure_logged = True
|
||||||
|
self._cached_walk = []
|
||||||
|
self._failed_seed_guid = start_guid
|
||||||
|
return
|
||||||
|
self._cached_start_guid = start_guid
|
||||||
|
self._cached_ifc_file = ifc_file
|
||||||
|
self._cached_walk = connected
|
||||||
|
if not connected:
|
||||||
|
return
|
||||||
|
|
||||||
|
prefs = tool.Blender.get_addon_preferences()
|
||||||
|
line_color = tuple(prefs.decorator_color_selected[:3])
|
||||||
|
# Junction dots get the "selected" palette slot (green by default) so
|
||||||
|
# they read as the currently-inspected network's spine; free endpoints
|
||||||
|
# get the "special" slot (blue by default) so dangling line ends stand
|
||||||
|
# apart from junctions at a glance.
|
||||||
|
connection_color = line_color
|
||||||
|
free_color = tuple(prefs.decorator_color_special[:3])
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines, free_points, connection_points = self._geom_cache.get_or_compute(
|
||||||
|
(start_guid, id(ifc_file)),
|
||||||
|
lambda: self._build_geometry(connected),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
if not self._build_failure_logged:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
self._build_failure_logged = True
|
||||||
|
self._failed_seed_guid = start_guid
|
||||||
|
return
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
_stroke_lines_alpha(context, lines, line_color, self.LINE_WIDTH, self.LINE_ALPHA)
|
||||||
|
|
||||||
|
if free_points or connection_points:
|
||||||
|
# POINTS via UNIFORM_COLOR; point_size_set only affects the next batch.
|
||||||
|
point_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||||
|
point_shader.bind()
|
||||||
|
gpu.state.point_size_set(self.DOT_SIZE)
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
if free_points:
|
||||||
|
point_shader.uniform_float("color", (*free_color, self.LINE_ALPHA))
|
||||||
|
batch = batch_for_shader(point_shader, "POINTS", {"pos": free_points})
|
||||||
|
batch.draw(point_shader)
|
||||||
|
if connection_points:
|
||||||
|
point_shader.uniform_float("color", (*connection_color, self.LINE_ALPHA))
|
||||||
|
batch = batch_for_shader(point_shader, "POINTS", {"pos": connection_points})
|
||||||
|
batch.draw(point_shader)
|
||||||
|
gpu.state.blend_set("NONE")
|
||||||
|
|
||||||
|
|
||||||
|
class MEPSystemPathDecorator(_ConnectedNetworkPathDecorator):
|
||||||
|
"""Schematic-path overlay for the selected MEP element's connected
|
||||||
|
distribution system.
|
||||||
|
|
||||||
|
Walk: BFS through ``IfcRelConnectsPorts`` from the first selected MEP
|
||||||
|
element. Segments render as one axis line + endpoint dots. Fittings
|
||||||
|
render as:
|
||||||
|
|
||||||
|
- 2-port (transition, coupler, bend): one line port-to-port, keeping
|
||||||
|
the schematic continuous through the fitting. The "spider from
|
||||||
|
origin" pattern produces V-shaped flares when the fitting's local
|
||||||
|
origin is offset from its ports.
|
||||||
|
- 3+-port (tee, cross, branching): spider from origin to each port.
|
||||||
|
Drawing all N*(N-1)/2 port pairs would clutter the view at high N
|
||||||
|
(N=4 → 6 lines); the spider gives one line per port.
|
||||||
|
- 0-port / 1-port: degenerate, no lines (dots still emit)."""
|
||||||
|
|
||||||
|
def _is_seed_element(self, element: Any) -> bool:
|
||||||
|
return tool.System.is_mep_element(element)
|
||||||
|
|
||||||
|
def _walk(self, start_element: Any) -> list[Any]:
|
||||||
|
return tool.System.walk_connected_mep_elements(start_element)
|
||||||
|
|
||||||
|
def _build_geometry(
|
||||||
|
self,
|
||||||
|
connected: list[Any],
|
||||||
|
) -> tuple[
|
||||||
|
list[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
]:
|
||||||
|
lines: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = []
|
||||||
|
port_positions: list[tuple[float, float, float]] = []
|
||||||
|
for element in connected:
|
||||||
|
if element.is_a("IfcFlowSegment"):
|
||||||
|
if not tool.Geometry.has_axis_representation(element):
|
||||||
|
continue
|
||||||
|
obj = tool.Ifc.get_object(element)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
start_world, end_world = tool.Model.get_flow_segment_axis(obj)
|
||||||
|
lines.append((tuple(start_world), tuple(end_world)))
|
||||||
|
# Segment ports sit at the two axis endpoints — emit dots so
|
||||||
|
# the connection node is visible whether the neighbour is a
|
||||||
|
# fitting (also emits) or another segment (doesn't).
|
||||||
|
port_positions.append(tuple(start_world))
|
||||||
|
port_positions.append(tuple(end_world))
|
||||||
|
elif element.is_a("IfcFlowFitting"):
|
||||||
|
obj = tool.Ifc.get_object(element)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
ports = tool.System.get_ports(element)
|
||||||
|
port_world_positions = [tool.System.get_port_world_position(p) for p in ports]
|
||||||
|
if len(port_world_positions) == 2:
|
||||||
|
lines.append((tuple(port_world_positions[0]), tuple(port_world_positions[1])))
|
||||||
|
elif len(port_world_positions) >= 3:
|
||||||
|
origin = obj.matrix_world.translation
|
||||||
|
for port_pos in port_world_positions:
|
||||||
|
lines.append((tuple(origin), tuple(port_pos)))
|
||||||
|
for port_pos in port_world_positions:
|
||||||
|
port_positions.append(tuple(port_pos))
|
||||||
|
free_points, connection_points = self._partition_points_by_coincidence(port_positions)
|
||||||
|
return lines, free_points, connection_points
|
||||||
|
|
||||||
|
|
||||||
|
class WallSystemPathDecorator(_ConnectedNetworkPathDecorator):
|
||||||
|
"""Schematic-path overlay for the selected wall's connected wall network.
|
||||||
|
|
||||||
|
Walk: BFS through ``IfcRelConnectsPathElements`` from the first selected
|
||||||
|
wall. Each wall renders as one reference-line segment + a dot at each
|
||||||
|
axis endpoint. Endpoints are classified by IFC topology — every wall in
|
||||||
|
the walked set inspects its ``IfcRelConnectsPathElements`` rels filtered
|
||||||
|
to walls in the same set, and uses ``Relating*``/``Related*ConnectionType``
|
||||||
|
(ATSTART / ATEND / ATPATH) to decide which endpoint participates. ATPATH
|
||||||
|
rels also emit a connection dot at the canonical join location (a T-meets
|
||||||
|
point sits on the through-wall's interior, not at any endpoint). The
|
||||||
|
framework's geometric classifier is bypassed for walls because authoring
|
||||||
|
tolerance and post-edit float drift commonly exceed the 0.1 mm coincidence
|
||||||
|
threshold, so T-junctions otherwise fell into the free bucket."""
|
||||||
|
|
||||||
|
def _is_seed_element(self, element: Any) -> bool:
|
||||||
|
return element.is_a("IfcWall") and tool.Geometry.has_axis_representation(element)
|
||||||
|
|
||||||
|
def _walk(self, start_element: Any) -> list[Any]:
|
||||||
|
return tool.Wall.walk_connected_walls(start_element)
|
||||||
|
|
||||||
|
def _build_geometry(
|
||||||
|
self,
|
||||||
|
connected: list[Any],
|
||||||
|
) -> tuple[
|
||||||
|
list[tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
list[tuple[float, float, float]],
|
||||||
|
]:
|
||||||
|
lines: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = []
|
||||||
|
refs: dict[int, tuple[tuple[float, float, float], tuple[float, float, float]]] = {}
|
||||||
|
for element in connected:
|
||||||
|
obj = tool.Ifc.get_object(element)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
ref = tool.Wall.get_world_reference_line(obj)
|
||||||
|
if ref is None:
|
||||||
|
continue
|
||||||
|
p1, p2 = tuple(ref[0]), tuple(ref[1])
|
||||||
|
refs[element.id()] = (p1, p2)
|
||||||
|
lines.append((p1, p2))
|
||||||
|
|
||||||
|
free_points, connection_points = self._classify_endpoints_from_rels(connected, refs)
|
||||||
|
connection_points = self._dedupe_close_points(connection_points, self.CONNECTION_EPS_SQ)
|
||||||
|
return lines, free_points, connection_points
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _classify_endpoints_from_rels(
|
||||||
|
connected: Sequence[Any],
|
||||||
|
refs: dict[int, tuple[tuple[float, float, float], tuple[float, float, float]]],
|
||||||
|
) -> tuple[list[tuple[float, float, float]], list[tuple[float, float, float]]]:
|
||||||
|
"""For each wall in ``connected`` with a reference line in ``refs``,
|
||||||
|
classify its endpoints by walking its ``IfcRelConnectsPathElements``
|
||||||
|
rels filtered to walls also in ``refs``. ATSTART side present →
|
||||||
|
reference-line start is a connection; ATEND side present → reference-
|
||||||
|
line end is a connection; otherwise free. ATPATH side present → emit
|
||||||
|
an extra connection dot at the canonical join via
|
||||||
|
``tool.Wall.path_connection_location_world``. Returns
|
||||||
|
``(free, connection)`` un-deduped."""
|
||||||
|
free_points: list[tuple[float, float, float]] = []
|
||||||
|
connection_points: list[tuple[float, float, float]] = []
|
||||||
|
for element in connected:
|
||||||
|
self_seg = refs.get(element.id())
|
||||||
|
if self_seg is None:
|
||||||
|
continue
|
||||||
|
sides: set[str] = set()
|
||||||
|
atpath_dots: list[tuple[float, float, float]] = []
|
||||||
|
for rel in getattr(element, "ConnectedTo", []) or ():
|
||||||
|
if not rel.is_a("IfcRelConnectsPathElements"):
|
||||||
|
continue
|
||||||
|
other = rel.RelatedElement
|
||||||
|
other_seg = refs.get(other.id()) if other is not None else None
|
||||||
|
if other_seg is None:
|
||||||
|
continue
|
||||||
|
self_type = rel.RelatingConnectionType
|
||||||
|
other_type = rel.RelatedConnectionType
|
||||||
|
sides.add(self_type)
|
||||||
|
if self_type == "ATPATH":
|
||||||
|
join = tool.Wall.path_connection_location_world(self_seg, self_type, other_seg, other_type)
|
||||||
|
atpath_dots.append(tuple(join))
|
||||||
|
for rel in getattr(element, "ConnectedFrom", []) or ():
|
||||||
|
if not rel.is_a("IfcRelConnectsPathElements"):
|
||||||
|
continue
|
||||||
|
other = rel.RelatingElement
|
||||||
|
other_seg = refs.get(other.id()) if other is not None else None
|
||||||
|
if other_seg is None:
|
||||||
|
continue
|
||||||
|
self_type = rel.RelatedConnectionType
|
||||||
|
other_type = rel.RelatingConnectionType
|
||||||
|
sides.add(self_type)
|
||||||
|
if self_type == "ATPATH":
|
||||||
|
join = tool.Wall.path_connection_location_world(self_seg, self_type, other_seg, other_type)
|
||||||
|
atpath_dots.append(tuple(join))
|
||||||
|
p1, p2 = self_seg
|
||||||
|
(connection_points if "ATSTART" in sides else free_points).append(p1)
|
||||||
|
(connection_points if "ATEND" in sides else free_points).append(p2)
|
||||||
|
connection_points.extend(atpath_dots)
|
||||||
|
return free_points, connection_points
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _dedupe_close_points(
|
||||||
|
points: Sequence[tuple[float, float, float]],
|
||||||
|
eps_sq: float,
|
||||||
|
) -> list[tuple[float, float, float]]:
|
||||||
|
"""Drop later occurrences of points within ``sqrt(eps_sq)`` of an
|
||||||
|
earlier one. Used to collapse overlapping connection dots so an ATPATH
|
||||||
|
join computed at the same point as a neighbour's wall endpoint
|
||||||
|
renders once."""
|
||||||
|
result: list[tuple[float, float, float]] = []
|
||||||
|
for point in points:
|
||||||
|
for existing in result:
|
||||||
|
dx, dy, dz = point[0] - existing[0], point[1] - existing[1], point[2] - existing[2]
|
||||||
|
if dx * dx + dy * dy + dz * dz <= eps_sq:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result.append(point)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ from bonsai.bim.module.drawing.decoration import CutDecorator
|
|||||||
from bonsai.bim.module.model.data import AuthoringData
|
from bonsai.bim.module.model.data import AuthoringData
|
||||||
from bonsai.bim.module.model.decorator import (
|
from bonsai.bim.module.model.decorator import (
|
||||||
BoundingBoxDecorator,
|
BoundingBoxDecorator,
|
||||||
|
MEPSystemPathDecorator,
|
||||||
SlabDirectionDecorator,
|
SlabDirectionDecorator,
|
||||||
WallAxisDecorator,
|
WallAxisDecorator,
|
||||||
|
WallSystemPathDecorator,
|
||||||
)
|
)
|
||||||
from bonsai.bim.module.model.door import update_door_modifier_bmesh
|
from bonsai.bim.module.model.door import update_door_modifier_bmesh
|
||||||
from bonsai.bim.module.model.window import update_window_modifier_bmesh
|
from bonsai.bim.module.model.window import update_window_modifier_bmesh
|
||||||
@@ -132,6 +134,19 @@ def update_slab_direction_decorator(self: "BIMModelProperties", context: bpy.typ
|
|||||||
SlabDirectionDecorator.uninstall()
|
SlabDirectionDecorator.uninstall()
|
||||||
|
|
||||||
|
|
||||||
|
def update_paths_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None:
|
||||||
|
"""Unified toggle for connected-element path overlays. Drives both the
|
||||||
|
MEP and wall path decorators — each decorator's ``draw`` short-circuits
|
||||||
|
when its kind of element isn't selected, so leaving both installed is
|
||||||
|
cheap and lets one toggle cover any connected-element family."""
|
||||||
|
if self.show_paths:
|
||||||
|
MEPSystemPathDecorator.install(bpy.context)
|
||||||
|
WallSystemPathDecorator.install(bpy.context)
|
||||||
|
else:
|
||||||
|
MEPSystemPathDecorator.uninstall()
|
||||||
|
WallSystemPathDecorator.uninstall()
|
||||||
|
|
||||||
|
|
||||||
def update_measure_xyz(self: "BIMModelProperties", context: bpy.types.Context) -> None:
|
def update_measure_xyz(self: "BIMModelProperties", context: bpy.types.Context) -> None:
|
||||||
if self.show_bounding_box:
|
if self.show_bounding_box:
|
||||||
BoundingBoxDecorator.install(context)
|
BoundingBoxDecorator.install(context)
|
||||||
@@ -354,6 +369,19 @@ class BIMModelProperties(PropertyGroup):
|
|||||||
default=False,
|
default=False,
|
||||||
update=update_slab_direction_decorator,
|
update=update_slab_direction_decorator,
|
||||||
)
|
)
|
||||||
|
show_paths: bpy.props.BoolProperty(
|
||||||
|
name="Show Paths",
|
||||||
|
default=False,
|
||||||
|
update=update_paths_decorator,
|
||||||
|
description=(
|
||||||
|
"Trace the connected element path from the selected element. For "
|
||||||
|
"walls, follows IfcRelConnectsPathElements and draws each "
|
||||||
|
"connected wall's reference axis with endpoint dots. For MEP "
|
||||||
|
"elements, follows IfcRelConnectsPorts and draws each segment's "
|
||||||
|
"axis plus a port-to-port spider for each fitting. Toggle off to "
|
||||||
|
"skip the BFS traversal entirely."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
prev_transform_orientation_slot_type: bpy.props.StringProperty(name="Previous Gizmo Orientation Type")
|
prev_transform_orientation_slot_type: bpy.props.StringProperty(name="Previous Gizmo Orientation Type")
|
||||||
prev_show_gizmo_object_translate: bpy.props.BoolProperty(name="Previous Gizmo Translate")
|
prev_show_gizmo_object_translate: bpy.props.BoolProperty(name="Previous Gizmo Translate")
|
||||||
@@ -401,6 +429,7 @@ class BIMModelProperties(PropertyGroup):
|
|||||||
offset: float
|
offset: float
|
||||||
show_wall_axis: bool
|
show_wall_axis: bool
|
||||||
show_slab_direction: bool
|
show_slab_direction: bool
|
||||||
|
show_paths: bool
|
||||||
|
|
||||||
prev_transform_orientation_slot_type: str
|
prev_transform_orientation_slot_type: str
|
||||||
prev_show_gizmo_object_translate: bool
|
prev_show_gizmo_object_translate: bool
|
||||||
|
|||||||
@@ -162,8 +162,8 @@ class ProjectLibraryData:
|
|||||||
library_file = IfcStore.library_file
|
library_file = IfcStore.library_file
|
||||||
if library_file is None or library_file.schema == "IFC2X3":
|
if library_file is None or library_file.schema == "IFC2X3":
|
||||||
return results
|
return results
|
||||||
project = library_file.by_type("IfcProject")[0]
|
root = tool.Project.get_root_context(library_file)
|
||||||
results.append((str(project.id()), f"IfcProject {project.Name or 'Unnamed'}", project.Description or ""))
|
results.append((str(root.id()), f"{root.is_a()} {root.Name or 'Unnamed'}", root.Description or ""))
|
||||||
for library_id, data in cls.data["project_libraries"].items():
|
for library_id, data in cls.data["project_libraries"].items():
|
||||||
results.append((str(library_id), data["Name"] or "Unnamed", data["Description"] or ""))
|
results.append((str(library_id), data["Name"] or "Unnamed", data["Description"] or ""))
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -281,9 +281,9 @@ class RefreshLibrary(bpy.types.Operator):
|
|||||||
elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)}
|
elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)}
|
||||||
self.props.add_library_project_library("Unassigned", len(elements), 0, False)
|
self.props.add_library_project_library("Unassigned", len(elements), 0, False)
|
||||||
|
|
||||||
ifc_project = library_file.by_type("IfcProject")[0]
|
root_context = tool.Project.get_root_context(library_file)
|
||||||
hierarchy = tool.Project.get_project_hierarchy(library_file)
|
hierarchy = tool.Project.get_project_hierarchy(library_file)
|
||||||
tool.Project.load_project_libraries_to_ui(ifc_project, hierarchy)
|
tool.Project.load_project_libraries_to_ui(root_context, hierarchy)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -763,7 +763,10 @@ class EditProjectLibrary(bpy.types.Operator):
|
|||||||
previous_parent_library = tool.Project.get_parent_library(project_library)
|
previous_parent_library = tool.Project.get_parent_library(project_library)
|
||||||
new_parent_library = library_file.by_id(int(props.parent_library))
|
new_parent_library = library_file.by_id(int(props.parent_library))
|
||||||
if previous_parent_library != new_parent_library:
|
if previous_parent_library != new_parent_library:
|
||||||
if previous_parent_library.is_a("IfcProject"):
|
if previous_parent_library is None:
|
||||||
|
# Edited library was a root in a library-only file; nest it under the new parent.
|
||||||
|
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
||||||
|
elif previous_parent_library.is_a("IfcProject"):
|
||||||
# Then new one is IfcProjectLibrary.
|
# Then new one is IfcProjectLibrary.
|
||||||
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
||||||
else: # Previous is IfcProjectLibrary.
|
else: # Previous is IfcProjectLibrary.
|
||||||
@@ -804,9 +807,12 @@ class AddProjectLibrary(bpy.types.Operator):
|
|||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
library_file = IfcStore.library_file
|
library_file = IfcStore.library_file
|
||||||
assert library_file
|
assert library_file
|
||||||
project = library_file.by_type("IfcProject")[0]
|
root_context = tool.Project.get_root_context(library_file)
|
||||||
project_library = ifcopenshell.api.root.create_entity(library_file, "IfcProjectLibrary")
|
project_library = ifcopenshell.api.root.create_entity(library_file, "IfcProjectLibrary")
|
||||||
ifcopenshell.api.project.assign_declaration(library_file, [project_library], project)
|
if root_context.is_a("IfcProject"):
|
||||||
|
ifcopenshell.api.project.assign_declaration(library_file, [project_library], root_context)
|
||||||
|
else:
|
||||||
|
ifcopenshell.api.nest.assign_object(library_file, [project_library], root_context)
|
||||||
ProjectLibraryData.load() # Update enum.
|
ProjectLibraryData.load() # Update enum.
|
||||||
props.selected_project_library = str(project_library.id())
|
props.selected_project_library = str(project_library.id())
|
||||||
props.is_editing_project_library = True
|
props.is_editing_project_library = True
|
||||||
@@ -1113,6 +1119,14 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
f"Error loading IFC file from filepath '{filepath}'. See logs above in the system console for the details.",
|
f"Error loading IFC file from filepath '{filepath}'. See logs above in the system console for the details.",
|
||||||
)
|
)
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
if not tool.Ifc.get().by_type("IfcProject"):
|
||||||
|
self.report(
|
||||||
|
{"ERROR"},
|
||||||
|
"This file contains no IfcProject. It is likely an IFC project library — "
|
||||||
|
"load it via Project Setup → Project Library → Select Library File instead.",
|
||||||
|
)
|
||||||
|
IfcStore.purge()
|
||||||
|
return {"CANCELLED"}
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
props.is_loading = True
|
props.is_loading = True
|
||||||
props.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
props.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ def is_editing_project_library_update(self: "BIMProjectProperties", context: bpy
|
|||||||
project_library = library_file.by_id(int(self.selected_project_library))
|
project_library = library_file.by_id(int(self.selected_project_library))
|
||||||
self.project_library_attributes.clear()
|
self.project_library_attributes.clear()
|
||||||
bonsai.bim.helper.import_attributes(project_library, self.project_library_attributes)
|
bonsai.bim.helper.import_attributes(project_library, self.project_library_attributes)
|
||||||
self.parent_library = str(tool.Project.get_parent_library(project_library).id())
|
if parent_library := tool.Project.get_parent_library(project_library):
|
||||||
|
self.parent_library = str(parent_library.id())
|
||||||
ProjectLibraryData.load() # Show edit icon in enum.
|
ProjectLibraryData.load() # Show edit icon in enum.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1925,6 +1925,7 @@ class BIM_PT_decorators_overlay(Panel):
|
|||||||
aggregate_props = tool.Aggregate.get_aggregate_props()
|
aggregate_props = tool.Aggregate.get_aggregate_props()
|
||||||
nest_props = tool.Nest.get_nest_props()
|
nest_props = tool.Nest.get_nest_props()
|
||||||
model_props = tool.Model.get_model_props()
|
model_props = tool.Model.get_model_props()
|
||||||
|
system_props = tool.System.get_system_props()
|
||||||
display_all = overlay.show_overlays
|
display_all = overlay.show_overlays
|
||||||
|
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
@@ -1942,6 +1943,9 @@ class BIM_PT_decorators_overlay(Panel):
|
|||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(model_props, "show_slab_direction", text="Slab Direction")
|
row.prop(model_props, "show_slab_direction", text="Slab Direction")
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
|
row.prop(model_props, "show_paths", text="Element Paths")
|
||||||
|
row.prop(system_props, "should_draw_decorations", text="System Decorations")
|
||||||
|
row = col.row(align=True)
|
||||||
row.prop(model_props, "show_bounding_box", text="Bounding Box Dimensions")
|
row.prop(model_props, "show_bounding_box", text="Bounding Box Dimensions")
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(model_props, "show_cut_decorator", text="Cut Decorator")
|
row.prop(model_props, "show_cut_decorator", text="Cut Decorator")
|
||||||
|
|||||||
@@ -38,15 +38,16 @@ if TYPE_CHECKING:
|
|||||||
PlaneTuple = tuple[float, float, float, float]
|
PlaneTuple = tuple[float, float, float, float]
|
||||||
PlaneSet = tuple[PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple]
|
PlaneSet = tuple[PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple]
|
||||||
|
|
||||||
# Outward margin (world units) so the empty's CUBE display edges sit
|
# Outward margins so the empty's CUBE display edges sit safely INSIDE
|
||||||
# safely INSIDE the clip volume. Absolute (not relative-to-scale)
|
# the clip volume. A fixed absolute margin fails under rotation: the
|
||||||
# because a relative multiplier balloons with scale and produces a
|
# float error in computing each column's length and in per-vertex dot
|
||||||
# visibly-wrong gap between the wireframe and the clipped geometry.
|
# products at GPU rasterisation scales with the axis's world
|
||||||
# Sub-mesh-precision value: visually invisible at any reasonable IFC
|
# half-extent, so once the box is spawned at any non-trivial scale it
|
||||||
# scale yet large enough to keep the empty's own wireframe edges off
|
# can exceed an absolute floor. The relative term tracks that drift;
|
||||||
# the clip planes when float-precision accumulation pushes a corner
|
# the absolute term catches sub-unit boxes where the relative term
|
||||||
# a fractional epsilon outward.
|
# shrinks below float precision.
|
||||||
_CLIP_EXPAND_ABS = 1e-6
|
_CLIP_EXPAND_ABS = 1e-6
|
||||||
|
_CLIP_EXPAND_REL = 1e-5
|
||||||
|
|
||||||
|
|
||||||
class ClipBox:
|
class ClipBox:
|
||||||
@@ -63,6 +64,11 @@ class ClipBox:
|
|||||||
|
|
||||||
_owned: set[int] = set()
|
_owned: set[int] = set()
|
||||||
_region_by_key: dict[int, tuple[Any, Any]] = {}
|
_region_by_key: dict[int, tuple[Any, Any]] = {}
|
||||||
|
# View matrix per region at last clip_border arm. PRE_VIEW only
|
||||||
|
# updates clip_planes; without a snapshot, the C-side clip_bb stays
|
||||||
|
# aligned to the prior view and the edit-mode picker rejects verts
|
||||||
|
# inside the current clip_planes after orbit/pan/zoom.
|
||||||
|
_view_matrix_at_arm: dict[int, tuple] = {}
|
||||||
_refresh_pending: bool = False
|
_refresh_pending: bool = False
|
||||||
_last_seen_ifc_id: int = 0
|
_last_seen_ifc_id: int = 0
|
||||||
# Tracks the last matrix we persisted to the pset, keyed by Blender
|
# Tracks the last matrix we persisted to the pset, keyed by Blender
|
||||||
@@ -152,7 +158,9 @@ class ClipBox:
|
|||||||
prevents the cube's own wireframe from being clipped by its own
|
prevents the cube's own wireframe from being clipped by its own
|
||||||
planes.
|
planes.
|
||||||
"""
|
"""
|
||||||
return tool.Cad.obb_clip_planes_from_matrix(obj.matrix_world, expand=_CLIP_EXPAND_ABS)
|
return tool.Cad.obb_clip_planes_from_matrix(
|
||||||
|
obj.matrix_world, expand=_CLIP_EXPAND_ABS, expand_rel=_CLIP_EXPAND_REL
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compute_planes_from_matrix(cls, matrix: Any) -> PlaneSet:
|
def compute_planes_from_matrix(cls, matrix: Any) -> PlaneSet:
|
||||||
@@ -163,7 +171,7 @@ class ClipBox:
|
|||||||
transform offset, while ``obj.matrix_world`` stays at the
|
transform offset, while ``obj.matrix_world`` stays at the
|
||||||
pre-transform value until the operator commits on release.
|
pre-transform value until the operator commits on release.
|
||||||
"""
|
"""
|
||||||
return tool.Cad.obb_clip_planes_from_matrix(matrix, expand=_CLIP_EXPAND_ABS)
|
return tool.Cad.obb_clip_planes_from_matrix(matrix, expand=_CLIP_EXPAND_ABS, expand_rel=_CLIP_EXPAND_REL)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_clip_planes(cls, planes: PlaneSet) -> None:
|
def apply_clip_planes(cls, planes: PlaneSet) -> None:
|
||||||
@@ -183,6 +191,7 @@ class ClipBox:
|
|||||||
cls._owned.add(key)
|
cls._owned.add(key)
|
||||||
cls._region_by_key[key] = (area, region)
|
cls._region_by_key[key] = (area, region)
|
||||||
cls._arm_region(area, region, region_3d, planes)
|
cls._arm_region(area, region, region_3d, planes)
|
||||||
|
cls._view_matrix_at_arm[key] = tuple(tuple(row) for row in region_3d.view_matrix)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _arm_region(cls, area: Any, region: Any, region_3d: Any, planes: PlaneSet) -> None:
|
def _arm_region(cls, area: Any, region: Any, region_3d: Any, planes: PlaneSet) -> None:
|
||||||
@@ -271,6 +280,7 @@ class ClipBox:
|
|||||||
"""Drop the ownership table without touching any region. Used on register/reload."""
|
"""Drop the ownership table without touching any region. Used on register/reload."""
|
||||||
cls._owned.clear()
|
cls._owned.clear()
|
||||||
cls._region_by_key.clear()
|
cls._region_by_key.clear()
|
||||||
|
cls._view_matrix_at_arm.clear()
|
||||||
|
|
||||||
PSET_NAME = "BBIM_ClipBoxes"
|
PSET_NAME = "BBIM_ClipBoxes"
|
||||||
COLLECTION_NAME = "BBIM_ClipBoxes"
|
COLLECTION_NAME = "BBIM_ClipBoxes"
|
||||||
@@ -533,10 +543,16 @@ class ClipBox:
|
|||||||
cls._last_seen_ifc_id = ifc_id
|
cls._last_seen_ifc_id = ifc_id
|
||||||
cls._owned.clear()
|
cls._owned.clear()
|
||||||
cls._region_by_key.clear()
|
cls._region_by_key.clear()
|
||||||
|
cls._view_matrix_at_arm.clear()
|
||||||
cls._persisted_matrices.clear()
|
cls._persisted_matrices.clear()
|
||||||
cls._last_seen_object_matrices.clear()
|
cls._last_seen_object_matrices.clear()
|
||||||
if ifc_file is not None:
|
if ifc_file is not None:
|
||||||
cls.load_from_project_pset(scene)
|
cls.load_from_project_pset(scene)
|
||||||
|
# .blend carries use_clip_planes / clip_bb forward; the
|
||||||
|
# C-side picker is armed for the prior session's view.
|
||||||
|
# Re-arm against the current view so click-select matches
|
||||||
|
# what the user sees.
|
||||||
|
cls.schedule_refresh()
|
||||||
# Orphan-empty adoption is deferred while a transform modal is
|
# Orphan-empty adoption is deferred while a transform modal is
|
||||||
# dragging so the active-index change on adoption can't disrupt
|
# dragging so the active-index change on adoption can't disrupt
|
||||||
# the move.
|
# the move.
|
||||||
@@ -557,6 +573,12 @@ class ClipBox:
|
|||||||
# one save on release, not N saves per frame.
|
# one save on release, not N saves per frame.
|
||||||
if ifc_file is not None and prev_matrix is not None:
|
if ifc_file is not None and prev_matrix is not None:
|
||||||
cls.mark_dirty_for_save(obj.name)
|
cls.mark_dirty_for_save(obj.name)
|
||||||
|
# clip_bb is stale once the box settles elsewhere. The modal
|
||||||
|
# gate suppresses per-tick re-arms during a live drag and
|
||||||
|
# fires once on release (or on external sets — Python, undo,
|
||||||
|
# constraint).
|
||||||
|
if prev_matrix is not None and not tool.Blender.is_transform_modal_active(bpy.context):
|
||||||
|
cls.schedule_refresh()
|
||||||
cls.flush_pending_saves(scene)
|
cls.flush_pending_saves(scene)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -594,6 +616,17 @@ class ClipBox:
|
|||||||
return
|
return
|
||||||
region_3d.clip_planes = cls.compute_planes_from_matrix(matrix)
|
region_3d.clip_planes = cls.compute_planes_from_matrix(matrix)
|
||||||
region_3d.update()
|
region_3d.update()
|
||||||
|
# clip_bb captured by view3d.clip_border is view-aligned, so an
|
||||||
|
# orbit/pan/zoom leaves the picker testing against the old
|
||||||
|
# frustum even after clip_planes refresh. Re-arm so the picker
|
||||||
|
# matches the current view.
|
||||||
|
region = getattr(bpy.context, "region", None)
|
||||||
|
if region is not None:
|
||||||
|
key = region.as_pointer()
|
||||||
|
prev_view = cls._view_matrix_at_arm.get(key)
|
||||||
|
current_view = tuple(tuple(row) for row in region_3d.view_matrix)
|
||||||
|
if prev_view is not None and prev_view != current_view:
|
||||||
|
cls.schedule_refresh()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Cross-section caps
|
# Cross-section caps
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from typing import (
|
|||||||
NotRequired,
|
NotRequired,
|
||||||
Optional,
|
Optional,
|
||||||
TypedDict,
|
TypedDict,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -376,12 +377,31 @@ class Project(bonsai.core.tool.Project):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_parent_library(cls, project_library: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
def get_parent_library(
|
||||||
|
cls, project_library: ifcopenshell.entity_instance
|
||||||
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
|
"""Return the IfcContext that declares or nests ``project_library``.
|
||||||
|
|
||||||
|
Returns ``None`` when ``project_library`` is itself the root of a
|
||||||
|
library-only file (no IfcRelNests, no IfcRelDeclares).
|
||||||
|
"""
|
||||||
if nests := project_library.Nests:
|
if nests := project_library.Nests:
|
||||||
# IfcProjectLibrary.
|
|
||||||
return nests[0].RelatingObject
|
return nests[0].RelatingObject
|
||||||
# IfcProject.
|
if has_context := project_library.HasContext:
|
||||||
return project_library.HasContext[0].RelatingContext
|
return has_context[0].RelatingContext
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_root_context(cls, ifc_file: ifcopenshell.file) -> ifcopenshell.entity_instance:
|
||||||
|
"""Return the file's root IfcContext.
|
||||||
|
|
||||||
|
Prefers IfcProject if present, otherwise falls back to IfcProjectLibrary —
|
||||||
|
library-only files are valid per IFC4+ and contain no IfcProject. Caller is
|
||||||
|
responsible for the IFC2X3 guard; IfcContext does not exist in that schema.
|
||||||
|
"""
|
||||||
|
if projects := ifc_file.by_type("IfcProject"):
|
||||||
|
return projects[0]
|
||||||
|
return ifc_file.by_type("IfcProjectLibrary")[0]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict:
|
def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict:
|
||||||
@@ -401,6 +421,8 @@ class Project(bonsai.core.tool.Project):
|
|||||||
return hierarchy
|
return hierarchy
|
||||||
for project_library in ifc_file.by_type("IfcProjectLibrary"):
|
for project_library in ifc_file.by_type("IfcProjectLibrary"):
|
||||||
parent_library = cls.get_parent_library(project_library)
|
parent_library = cls.get_parent_library(project_library)
|
||||||
|
if parent_library is None:
|
||||||
|
continue
|
||||||
hierarchy[parent_library][project_library] = hierarchy[project_library]
|
hierarchy[parent_library][project_library] = hierarchy[project_library]
|
||||||
return hierarchy
|
return hierarchy
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,33 @@ class TestComputePlanes(NewFile):
|
|||||||
planes = tool.ClipBox.compute_planes(host)
|
planes = tool.ClipBox.compute_planes(host)
|
||||||
assert tool.Cad.point_is_inside_clip_planes(planes, Vector((5, 7, 0)))
|
assert tool.Cad.point_is_inside_clip_planes(planes, Vector((5, 7, 0)))
|
||||||
|
|
||||||
|
def test_margin_grows_with_scale(self):
|
||||||
|
# The empty's CUBE display lives at local +-1; the GPU dot
|
||||||
|
# product that tests each wireframe vertex against the clip
|
||||||
|
# planes has float error that scales with the axis's world
|
||||||
|
# half-extent. A fixed absolute margin gets eaten by that drift
|
||||||
|
# once the box is spawned at non-trivial scale, so the half-
|
||||||
|
# extent the planes encode must include a relative term — the
|
||||||
|
# margin between the wireframe edge and the plane must grow
|
||||||
|
# with the scale.
|
||||||
|
bpy.ops.bim.add_clip_box()
|
||||||
|
host = tool.ClipBox.get_active_clip_box()
|
||||||
|
|
||||||
|
host.matrix_world = Matrix.Identity(4)
|
||||||
|
planes_unit = tool.ClipBox.compute_planes(host)
|
||||||
|
# +X plane: normal (-1, 0, 0), d = half_x. Read half from d.
|
||||||
|
half_unit = planes_unit[0][3]
|
||||||
|
margin_unit = half_unit - 1.0
|
||||||
|
|
||||||
|
host.matrix_world = Matrix.Diagonal((100.0, 100.0, 100.0, 1.0))
|
||||||
|
planes_scaled = tool.ClipBox.compute_planes(host)
|
||||||
|
half_scaled = planes_scaled[0][3]
|
||||||
|
margin_scaled = half_scaled - 100.0
|
||||||
|
|
||||||
|
assert margin_scaled > margin_unit * 10, (
|
||||||
|
f"margin must scale with extent: unit={margin_unit:g}, " f"scale-100={margin_scaled:g}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestToggleEnabled(NewFile):
|
class TestToggleEnabled(NewFile):
|
||||||
def test_flips_scene_enabled_flag(self):
|
def test_flips_scene_enabled_flag(self):
|
||||||
@@ -690,3 +717,84 @@ class TestCapRebuildDebounce(NewFile):
|
|||||||
):
|
):
|
||||||
tool.ClipBox.on_depsgraph_update_caps(bpy.context.scene, None)
|
tool.ClipBox.on_depsgraph_update_caps(bpy.context.scene, None)
|
||||||
mock_handle.assert_not_called()
|
mock_handle.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestClipBbReArmTriggers(NewFile):
|
||||||
|
"""The C-side clip_bb captured by view3d.clip_border for edit-mode
|
||||||
|
click-select is view-aligned and tied to the box pose at arm time,
|
||||||
|
so it goes stale on either a clip-box transform commit, an external
|
||||||
|
matrix mutation, or an IFC reload that rehydrates from pset. The
|
||||||
|
depsgraph handler schedules a full re-arm at those events; the
|
||||||
|
modal gate suppresses per-tick re-arms during a live drag.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
tool.ClipBox._persisted_matrices.clear()
|
||||||
|
tool.ClipBox._last_seen_ifc_id = 0
|
||||||
|
tool.ClipBox._refresh_pending = False
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
tool.ClipBox._persisted_matrices.clear()
|
||||||
|
tool.ClipBox._last_seen_ifc_id = 0
|
||||||
|
tool.ClipBox._refresh_pending = False
|
||||||
|
|
||||||
|
def test_matrix_change_outside_modal_re_arms(self):
|
||||||
|
bpy.ops.bim.add_clip_box()
|
||||||
|
host = tool.ClipBox.get_active_clip_box()
|
||||||
|
# Seed a stale baseline so prev_matrix != current_matrix.
|
||||||
|
stale = tuple(tuple(row) for row in Matrix.Translation((-99.0, 0.0, 0.0)))
|
||||||
|
tool.ClipBox._persisted_matrices[host.name] = stale
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(tool.Blender, "is_transform_modal_active", return_value=False),
|
||||||
|
patch.object(tool.ClipBox, "schedule_refresh") as mock_refresh,
|
||||||
|
):
|
||||||
|
tool.ClipBox.on_depsgraph_update(bpy.context.scene, bpy.context.evaluated_depsgraph_get())
|
||||||
|
mock_refresh.assert_called_once()
|
||||||
|
|
||||||
|
def test_matrix_change_during_modal_skips_re_arm(self):
|
||||||
|
bpy.ops.bim.add_clip_box()
|
||||||
|
host = tool.ClipBox.get_active_clip_box()
|
||||||
|
stale = tuple(tuple(row) for row in Matrix.Translation((-99.0, 0.0, 0.0)))
|
||||||
|
tool.ClipBox._persisted_matrices[host.name] = stale
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(tool.Blender, "is_transform_modal_active", return_value=True),
|
||||||
|
patch.object(tool.ClipBox, "schedule_refresh") as mock_refresh,
|
||||||
|
):
|
||||||
|
tool.ClipBox.on_depsgraph_update(bpy.context.scene, bpy.context.evaluated_depsgraph_get())
|
||||||
|
mock_refresh.assert_not_called()
|
||||||
|
|
||||||
|
def test_first_matrix_sighting_does_not_re_arm(self):
|
||||||
|
# No prior persisted-matrix entry: the branch records the
|
||||||
|
# baseline and exits without re-arming. The add path already
|
||||||
|
# armed once; a per-tick re-arm on first sight would double-arm.
|
||||||
|
bpy.ops.bim.add_clip_box()
|
||||||
|
host = tool.ClipBox.get_active_clip_box()
|
||||||
|
tool.ClipBox._persisted_matrices.pop(host.name, None)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(tool.Blender, "is_transform_modal_active", return_value=False),
|
||||||
|
patch.object(tool.ClipBox, "schedule_refresh") as mock_refresh,
|
||||||
|
):
|
||||||
|
tool.ClipBox.on_depsgraph_update(bpy.context.scene, bpy.context.evaluated_depsgraph_get())
|
||||||
|
mock_refresh.assert_not_called()
|
||||||
|
|
||||||
|
def test_ifc_reload_re_arms(self):
|
||||||
|
bpy.ops.bim.create_project()
|
||||||
|
bpy.ops.bim.add_clip_box()
|
||||||
|
# Force an ifc-id mismatch so the rehydrate-from-pset branch
|
||||||
|
# fires. The .blend carries the prior session's clip_bb forward;
|
||||||
|
# the picker is armed for the OLD view until this re-arms.
|
||||||
|
tool.ClipBox._last_seen_ifc_id = 0
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(tool.Blender, "is_transform_modal_active", return_value=False),
|
||||||
|
patch.object(tool.ClipBox, "schedule_refresh") as mock_refresh,
|
||||||
|
):
|
||||||
|
tool.ClipBox.on_depsgraph_update(bpy.context.scene, bpy.context.evaluated_depsgraph_get())
|
||||||
|
# IFC-load triggers a re-arm. (Reading the show_caps pset entry
|
||||||
|
# writes scene_props.show_caps via its update callback, which
|
||||||
|
# is a separate pre-existing re-arm path; the test pins the
|
||||||
|
# invariant "ifc-load arms at least once".)
|
||||||
|
assert mock_refresh.call_count >= 1
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2026
|
||||||
|
#
|
||||||
|
# This file is part of Bonsai.
|
||||||
|
#
|
||||||
|
# Bonsai is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Bonsai is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# This file was generated with the assistance of an AI coding tool.
|
||||||
|
|
||||||
|
"""Pin the template-method contract for the connected-network-path decorator
|
||||||
|
base. The base class defines three abstract hooks (`_is_seed_element`,
|
||||||
|
`_walk`, `_build_geometry`) and an `__init_subclass__` that rejects any
|
||||||
|
subclass which leaves a hook un-overridden. Without this guard, a forgotten
|
||||||
|
override would only surface as `NotImplementedError` on the first redraw
|
||||||
|
that hit the missing hook — long after the class declaration."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.model
|
||||||
|
|
||||||
|
|
||||||
|
_GOOD_HOOKS = {
|
||||||
|
"_is_seed_element": lambda self, element: False,
|
||||||
|
"_walk": lambda self, start_element: [],
|
||||||
|
"_build_geometry": lambda self, connected: ([], [], []),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_subclass(name, omit=()):
|
||||||
|
from bonsai.bim.module.model.decorator import _ConnectedNetworkPathDecorator
|
||||||
|
|
||||||
|
namespace = {name: fn for name, fn in _GOOD_HOOKS.items() if name not in omit}
|
||||||
|
return type(name, (_ConnectedNetworkPathDecorator,), namespace)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("missing_hook", sorted(_GOOD_HOOKS))
|
||||||
|
def test_subclass_missing_any_single_hook_raises(missing_hook):
|
||||||
|
with pytest.raises(TypeError, match="must override abstract hook"):
|
||||||
|
_build_subclass(f"DecoratorMissing_{missing_hook}", omit=(missing_hook,))
|
||||||
|
|
||||||
|
|
||||||
|
def test_subclass_missing_all_hooks_raises_naming_each():
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
_build_subclass("DecoratorMissingEverything", omit=tuple(_GOOD_HOOKS))
|
||||||
|
message = str(excinfo.value)
|
||||||
|
for hook in _GOOD_HOOKS:
|
||||||
|
assert hook in message, f"missing-hook error must name {hook!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fully_overridden_subclass_is_accepted():
|
||||||
|
cls = _build_subclass("DecoratorWithAllHooks")
|
||||||
|
assert cls.__name__ == "DecoratorWithAllHooks"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pure-geometry classifier contract.
|
||||||
|
#
|
||||||
|
# Pins the free/connection split that drives the dot colors. The classifier
|
||||||
|
# is plain Python (no bpy / no ifcopenshell), so it runs unconditionally —
|
||||||
|
# the autouse Blender skip in conftest still applies but doesn't bite here.
|
||||||
|
|
||||||
|
_EPS = 1e-5 # well under CONNECTION_EPS_SQ's sqrt (1e-4)
|
||||||
|
|
||||||
|
|
||||||
|
def _cls():
|
||||||
|
from bonsai.bim.module.model.decorator import _ConnectedNetworkPathDecorator
|
||||||
|
|
||||||
|
return _ConnectedNetworkPathDecorator
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_empty_input_returns_two_empty_lists():
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence([])
|
||||||
|
assert free == []
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_single_point_is_free():
|
||||||
|
p = (1.0, 2.0, 3.0)
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence([p])
|
||||||
|
assert free == [p]
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_coincident_pair_dedupes_to_one_connection():
|
||||||
|
p = (1.0, 2.0, 3.0)
|
||||||
|
near = (1.0 + _EPS, 2.0, 3.0)
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence([p, near])
|
||||||
|
assert free == []
|
||||||
|
assert len(conn) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_far_points_stay_free():
|
||||||
|
p1 = (0.0, 0.0, 0.0)
|
||||||
|
p2 = (10.0, 0.0, 0.0)
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence([p1, p2])
|
||||||
|
assert sorted(free) == sorted([p1, p2])
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_t_junction_point_on_segment_interior_is_connection():
|
||||||
|
a1, a2 = (0.0, 0.0, 0.0), (5.0, 0.0, 0.0) # wall A endpoints (own segment)
|
||||||
|
b1, b2 = (2.5, -2.0, 0.0), (2.5, 0.0, 0.0) # wall B: T-meets A's midpoint
|
||||||
|
points = [a1, a2, b1, b2]
|
||||||
|
lines = [(a1, a2), (b1, b2)]
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence(points, lines)
|
||||||
|
assert b2 in conn, "T-junction interior touch must be flagged as a connection"
|
||||||
|
assert a1 in free and a2 in free, "wall A free endpoints must stay free"
|
||||||
|
assert b1 in free, "wall B's far endpoint must stay free"
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_endpoint_of_own_segment_is_not_a_t_junction():
|
||||||
|
"""A free endpoint sits exactly on its own segment's tip; the interior
|
||||||
|
check must exclude segment endpoints, not just the line interior."""
|
||||||
|
a1, a2 = (0.0, 0.0, 0.0), (5.0, 0.0, 0.0)
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence([a1, a2], [(a1, a2)])
|
||||||
|
assert conn == [], "own-segment endpoints must not self-classify as connection"
|
||||||
|
assert sorted(free) == sorted([a1, a2])
|
||||||
|
|
||||||
|
|
||||||
|
def test_classifier_zero_length_segment_does_not_match():
|
||||||
|
"""A segment whose two endpoints coincide has no interior; the interior
|
||||||
|
check must skip it rather than divide by a near-zero seg_len_sq."""
|
||||||
|
a = (0.0, 0.0, 0.0)
|
||||||
|
p_far = (1.0, 1.0, 1.0)
|
||||||
|
free, conn = _cls()._partition_points_by_coincidence([p_far], [(a, a)])
|
||||||
|
assert free == [p_far]
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Wall topology classifier — IFC-rel-driven endpoint classification.
|
||||||
|
#
|
||||||
|
# Pins the rule "an endpoint is a connection iff an IfcRelConnectsPathElements
|
||||||
|
# rel says so", independent of geometric coincidence. Replaces the geometric
|
||||||
|
# classifier on the wall path because authoring tolerance routinely exceeds
|
||||||
|
# the 0.1 mm epsilon, leaving T-junction dots mis-coloured.
|
||||||
|
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
|
||||||
|
def _stub_wall(wid, connected_to=(), connected_from=()):
|
||||||
|
e = Mock()
|
||||||
|
e.id.return_value = wid
|
||||||
|
e.is_a = lambda kind: kind == "IfcWall"
|
||||||
|
e.ConnectedTo = list(connected_to)
|
||||||
|
e.ConnectedFrom = list(connected_from)
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
def _stub_rel(relating, related, relating_type, related_type):
|
||||||
|
r = Mock()
|
||||||
|
r.is_a = lambda kind: kind == "IfcRelConnectsPathElements"
|
||||||
|
r.RelatingElement = relating
|
||||||
|
r.RelatedElement = related
|
||||||
|
r.RelatingConnectionType = relating_type
|
||||||
|
r.RelatedConnectionType = related_type
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def _wall_cls():
|
||||||
|
from bonsai.bim.module.model.decorator import WallSystemPathDecorator
|
||||||
|
|
||||||
|
return WallSystemPathDecorator
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_topology_single_wall_no_rels_both_endpoints_free():
|
||||||
|
a = _stub_wall(1)
|
||||||
|
refs = {1: ((0.0, 0.0, 0.0), (5.0, 0.0, 0.0))}
|
||||||
|
free, conn = _wall_cls()._classify_endpoints_from_rels([a], refs)
|
||||||
|
assert sorted(free) == sorted([(0.0, 0.0, 0.0), (5.0, 0.0, 0.0)])
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_topology_l_corner_atend_to_atstart_flags_both_endpoints():
|
||||||
|
"""Two walls meeting at a corner: A's ATEND joins B's ATSTART. Each wall's
|
||||||
|
join-side endpoint flips to connection; the far endpoints stay free."""
|
||||||
|
a = _stub_wall(1)
|
||||||
|
b = _stub_wall(2)
|
||||||
|
rel = _stub_rel(relating=a, related=b, relating_type="ATEND", related_type="ATSTART")
|
||||||
|
a.ConnectedTo = [rel]
|
||||||
|
b.ConnectedFrom = [rel]
|
||||||
|
refs = {
|
||||||
|
1: ((0.0, 0.0, 0.0), (5.0, 0.0, 0.0)),
|
||||||
|
2: ((5.0, 0.0, 0.0), (5.0, 5.0, 0.0)),
|
||||||
|
}
|
||||||
|
free, conn = _wall_cls()._classify_endpoints_from_rels([a, b], refs)
|
||||||
|
assert (5.0, 0.0, 0.0) in conn, "A's ATEND endpoint at the corner must be connection"
|
||||||
|
assert (5.0, 0.0, 0.0) in conn, "B's ATSTART endpoint at the corner must be connection"
|
||||||
|
assert (0.0, 0.0, 0.0) in free, "A's far end must stay free"
|
||||||
|
assert (5.0, 5.0, 0.0) in free, "B's far end must stay free"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_topology_t_junction_atpath_emits_canonical_join_dot():
|
||||||
|
"""B's ATEND meets A's interior (ATPATH). A's two endpoints stay free,
|
||||||
|
B's ATSTART stays free, B's ATEND is connection, and an extra connection
|
||||||
|
dot is emitted at the T-meets point computed by
|
||||||
|
``tool.Wall.path_connection_location_world``."""
|
||||||
|
a = _stub_wall(1)
|
||||||
|
b = _stub_wall(2)
|
||||||
|
rel = _stub_rel(relating=b, related=a, relating_type="ATEND", related_type="ATPATH")
|
||||||
|
a.ConnectedFrom = [rel]
|
||||||
|
b.ConnectedTo = [rel]
|
||||||
|
refs = {
|
||||||
|
1: ((0.0, 0.0, 0.0), (5.0, 0.0, 0.0)),
|
||||||
|
2: ((2.5, -2.0, 0.0), (2.5, 0.0, 0.0)),
|
||||||
|
}
|
||||||
|
t_meets = (2.5, 0.0, 0.0)
|
||||||
|
with patch("bonsai.tool.Wall.path_connection_location_world", return_value=t_meets):
|
||||||
|
free, conn = _wall_cls()._classify_endpoints_from_rels([a, b], refs)
|
||||||
|
assert t_meets in conn, "T-meets canonical join must be a connection dot"
|
||||||
|
assert (2.5, 0.0, 0.0) in conn, "B's ATEND at the junction must also be a connection"
|
||||||
|
assert (0.0, 0.0, 0.0) in free and (5.0, 0.0, 0.0) in free, "A's endpoints stay free"
|
||||||
|
assert (2.5, -2.0, 0.0) in free, "B's ATSTART (far end) stays free"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_topology_rel_to_wall_outside_walked_set_is_ignored():
|
||||||
|
"""A rel pointing at a wall whose id is not in ``refs`` must not classify
|
||||||
|
the participating endpoint as connection — only intra-set joins count."""
|
||||||
|
a = _stub_wall(1)
|
||||||
|
outside = _stub_wall(99)
|
||||||
|
rel = _stub_rel(relating=a, related=outside, relating_type="ATEND", related_type="ATSTART")
|
||||||
|
a.ConnectedTo = [rel]
|
||||||
|
refs = {1: ((0.0, 0.0, 0.0), (5.0, 0.0, 0.0))}
|
||||||
|
free, conn = _wall_cls()._classify_endpoints_from_rels([a], refs)
|
||||||
|
assert sorted(free) == sorted([(0.0, 0.0, 0.0), (5.0, 0.0, 0.0)])
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_topology_non_path_rels_are_ignored():
|
||||||
|
"""``ConnectedTo`` can carry ``IfcRelConnectsElements`` (slab clip rels);
|
||||||
|
only ``IfcRelConnectsPathElements`` contribute to wall endpoint topology."""
|
||||||
|
a = _stub_wall(1)
|
||||||
|
non_path_rel = Mock()
|
||||||
|
non_path_rel.is_a = lambda kind: kind == "IfcRelConnectsElements"
|
||||||
|
a.ConnectedTo = [non_path_rel]
|
||||||
|
refs = {1: ((0.0, 0.0, 0.0), (5.0, 0.0, 0.0))}
|
||||||
|
free, conn = _wall_cls()._classify_endpoints_from_rels([a], refs)
|
||||||
|
assert sorted(free) == sorted([(0.0, 0.0, 0.0), (5.0, 0.0, 0.0)])
|
||||||
|
assert conn == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_topology_dedupe_collapses_overlapping_connection_dots():
|
||||||
|
"""Two connection dots at the same world point (within eps) collapse to
|
||||||
|
one — used by ``_build_geometry`` to keep ATPATH joins from stacking on
|
||||||
|
neighbour-wall endpoints."""
|
||||||
|
p = (1.0, 2.0, 3.0)
|
||||||
|
near = (1.0 + 1e-6, 2.0, 3.0)
|
||||||
|
far = (10.0, 0.0, 0.0)
|
||||||
|
result = _wall_cls()._dedupe_close_points([p, near, far], 1e-4 * 1e-4)
|
||||||
|
assert len(result) == 2
|
||||||
|
assert p in result and far in result
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2026
|
||||||
|
#
|
||||||
|
# This file is part of Bonsai.
|
||||||
|
#
|
||||||
|
# Bonsai is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Bonsai is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# This file was generated with the assistance of an AI coding tool.
|
||||||
|
|
||||||
|
import ifcopenshell
|
||||||
|
import ifcopenshell.api.nest
|
||||||
|
import ifcopenshell.api.project
|
||||||
|
import ifcopenshell.api.root
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import bonsai.tool as tool
|
||||||
|
from bonsai.bim.ifc import IfcStore
|
||||||
|
from bonsai.bim.module.project.data import ProjectLibraryData
|
||||||
|
from test.bim.bootstrap import NewIfc
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.project
|
||||||
|
|
||||||
|
|
||||||
|
def _make_library_only_file(*, with_child: bool = False) -> ifcopenshell.file:
|
||||||
|
"""Build a minimal IFC4 file containing only an IfcProjectLibrary (no IfcProject).
|
||||||
|
|
||||||
|
Per IFC4+, a file must contain at least one IfcContext; IfcProjectLibrary is a
|
||||||
|
valid root on its own. ``with_child=True`` nests a sub-library under the root via
|
||||||
|
IfcRelNests, mirroring real authored library files.
|
||||||
|
"""
|
||||||
|
library_file = ifcopenshell.api.project.create_file(version="IFC4")
|
||||||
|
root = ifcopenshell.api.root.create_entity(library_file, ifc_class="IfcProjectLibrary", name="RootLib")
|
||||||
|
if with_child:
|
||||||
|
child = ifcopenshell.api.root.create_entity(library_file, ifc_class="IfcProjectLibrary", name="ChildLib")
|
||||||
|
ifcopenshell.api.nest.assign_object(library_file, [child], root)
|
||||||
|
return library_file
|
||||||
|
|
||||||
|
|
||||||
|
class TestLibraryOnlyFile(NewIfc):
|
||||||
|
def test_get_root_context_returns_project_library_when_no_project(self):
|
||||||
|
library_file = _make_library_only_file()
|
||||||
|
assert not library_file.by_type("IfcProject")
|
||||||
|
|
||||||
|
root = tool.Project.get_root_context(library_file)
|
||||||
|
|
||||||
|
assert root.is_a("IfcProjectLibrary")
|
||||||
|
assert root.Name == "RootLib"
|
||||||
|
|
||||||
|
def test_get_parent_library_returns_none_for_root_library(self):
|
||||||
|
library_file = _make_library_only_file()
|
||||||
|
root = library_file.by_type("IfcProjectLibrary")[0]
|
||||||
|
|
||||||
|
assert tool.Project.get_parent_library(root) is None
|
||||||
|
|
||||||
|
def test_get_project_hierarchy_skips_root_library(self):
|
||||||
|
library_file = _make_library_only_file(with_child=True)
|
||||||
|
root = next(lib for lib in library_file.by_type("IfcProjectLibrary") if lib.Name == "RootLib")
|
||||||
|
child = next(lib for lib in library_file.by_type("IfcProjectLibrary") if lib.Name == "ChildLib")
|
||||||
|
|
||||||
|
hierarchy = tool.Project.get_project_hierarchy(library_file)
|
||||||
|
|
||||||
|
assert root in hierarchy
|
||||||
|
assert child in hierarchy[root]
|
||||||
|
|
||||||
|
def test_project_library_data_loads_without_crash(self):
|
||||||
|
IfcStore.library_file = _make_library_only_file()
|
||||||
|
try:
|
||||||
|
ProjectLibraryData.is_loaded = False
|
||||||
|
ProjectLibraryData.load()
|
||||||
|
assert ProjectLibraryData.is_loaded
|
||||||
|
enum = ProjectLibraryData.data["parent_libraries_enum"]
|
||||||
|
assert len(enum) == 1
|
||||||
|
assert enum[0][1].startswith("IfcProjectLibrary ")
|
||||||
|
finally:
|
||||||
|
IfcStore.library_file = None
|
||||||
|
ProjectLibraryData.is_loaded = False
|
||||||
|
|
||||||
|
def test_refresh_library_succeeds_on_library_only_file(self):
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
IfcStore.library_file = _make_library_only_file(with_child=True)
|
||||||
|
try:
|
||||||
|
result = bpy.ops.bim.refresh_library()
|
||||||
|
assert result == {"FINISHED"}
|
||||||
|
finally:
|
||||||
|
IfcStore.library_file = None
|
||||||
|
ProjectLibraryData.is_loaded = False
|
||||||
|
|
||||||
|
def test_add_project_library_nests_under_root_when_no_project(self):
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
IfcStore.library_file = _make_library_only_file()
|
||||||
|
library_file = IfcStore.library_file
|
||||||
|
try:
|
||||||
|
root = library_file.by_type("IfcProjectLibrary")[0]
|
||||||
|
before = set(library_file.by_type("IfcProjectLibrary"))
|
||||||
|
|
||||||
|
result = bpy.ops.bim.add_project_library()
|
||||||
|
|
||||||
|
assert result == {"FINISHED"}
|
||||||
|
after = set(library_file.by_type("IfcProjectLibrary"))
|
||||||
|
new_libraries = after - before
|
||||||
|
assert len(new_libraries) == 1
|
||||||
|
new_library = next(iter(new_libraries))
|
||||||
|
assert new_library.Nests
|
||||||
|
assert new_library.Nests[0].RelatingObject == root
|
||||||
|
assert not new_library.HasContext
|
||||||
|
finally:
|
||||||
|
IfcStore.library_file = None
|
||||||
|
ProjectLibraryData.is_loaded = False
|
||||||
@@ -65,6 +65,30 @@ static_assert(false, "A boost preprocessor sequence of schema identifiers is nee
|
|||||||
#include INCLUDE_SCHEMA_N(14)
|
#include INCLUDE_SCHEMA_N(14)
|
||||||
#include INCLUDE_SCHEMA_N(15)
|
#include INCLUDE_SCHEMA_N(15)
|
||||||
|
|
||||||
|
#undef INCLUDE_SCHEMA_N
|
||||||
|
|
||||||
|
#define INCLUDE_SCHEMA_N(n) \
|
||||||
|
BOOST_PP_IIF(BOOST_PP_GREATER(BOOST_PP_SEQ_SIZE(SCHEMA_SEQ), n), \
|
||||||
|
BOOST_PP_STRINGIZE(ifcparse/BOOST_PP_CAT(Ifc, BOOST_PP_SEQ_ELEM(BOOST_PP_MIN(n, BOOST_PP_SEQ_SIZE(BOOST_PP_SEQ_POP_BACK(SCHEMA_SEQ))), SCHEMA_SEQ))-definitions.h), \
|
||||||
|
"ifcgeom/empty.h")
|
||||||
|
|
||||||
|
#include INCLUDE_SCHEMA_N(0)
|
||||||
|
#include INCLUDE_SCHEMA_N(1)
|
||||||
|
#include INCLUDE_SCHEMA_N(2)
|
||||||
|
#include INCLUDE_SCHEMA_N(3)
|
||||||
|
#include INCLUDE_SCHEMA_N(4)
|
||||||
|
#include INCLUDE_SCHEMA_N(5)
|
||||||
|
#include INCLUDE_SCHEMA_N(6)
|
||||||
|
#include INCLUDE_SCHEMA_N(7)
|
||||||
|
#include INCLUDE_SCHEMA_N(8)
|
||||||
|
#include INCLUDE_SCHEMA_N(9)
|
||||||
|
#include INCLUDE_SCHEMA_N(10)
|
||||||
|
#include INCLUDE_SCHEMA_N(11)
|
||||||
|
#include INCLUDE_SCHEMA_N(12)
|
||||||
|
#include INCLUDE_SCHEMA_N(13)
|
||||||
|
#include INCLUDE_SCHEMA_N(14)
|
||||||
|
#include INCLUDE_SCHEMA_N(15)
|
||||||
|
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
|
||||||
#if USE_VLD
|
#if USE_VLD
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "../ifcgeom/ConversionSettings.h"
|
#include "../ifcgeom/ConversionSettings.h"
|
||||||
#include "../ifcgeom/taxonomy.h"
|
#include "../ifcgeom/taxonomy.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -293,8 +294,8 @@ namespace IfcGeom {
|
|||||||
virtual ConversionResultShape* intersect(ConversionResultShape*) = 0;
|
virtual ConversionResultShape* intersect(ConversionResultShape*) = 0;
|
||||||
virtual ConversionResultShape* concat(ConversionResultShape*) = 0;
|
virtual ConversionResultShape* concat(ConversionResultShape*) = 0;
|
||||||
|
|
||||||
virtual void map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to) = 0;
|
virtual std::size_t map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to) = 0;
|
||||||
virtual void map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to) = 0;
|
virtual std::size_t map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to) = 0;
|
||||||
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const = 0;
|
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const = 0;
|
||||||
|
|
||||||
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const = 0;
|
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const = 0;
|
||||||
|
|||||||
@@ -30,6 +30,165 @@ typedef Polyhedron::Facet_const_handle Facet_const_handle;
|
|||||||
typedef Polyhedron::Halfedge_around_facet_const_circulator Halfedge_around_facet_circulator;
|
typedef Polyhedron::Halfedge_around_facet_const_circulator Halfedge_around_facet_circulator;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
cgal_placement_t make_transform(const ifcopenshell::geometry::taxonomy::matrix4& place) {
|
||||||
|
const auto& m = place.ccomponents();
|
||||||
|
return cgal_placement_t(
|
||||||
|
m(0, 0), m(0, 1), m(0, 2), m(0, 3),
|
||||||
|
m(1, 0), m(1, 1), m(1, 2), m(1, 3),
|
||||||
|
m(2, 0), m(2, 1), m(2, 2), m(2, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
OpaqueCoordinate<3> opaque_point(const cgal_point_t& p) {
|
||||||
|
return OpaqueCoordinate<3>(
|
||||||
|
new NumberType(p.cartesian(0)),
|
||||||
|
new NumberType(p.cartesian(1)),
|
||||||
|
new NumberType(p.cartesian(2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
typename Kernel_::FT max_abs3(const typename Kernel_::FT& a, const typename Kernel_::FT& b, const typename Kernel_::FT& c) {
|
||||||
|
std::array<typename Kernel_::FT, 3> abc{ a, b, c };
|
||||||
|
auto minel = std::min_element(abc.begin(), abc.end());
|
||||||
|
auto maxel = std::max_element(abc.begin(), abc.end());
|
||||||
|
return ((-*minel) > *maxel) ? (-*minel) : *maxel;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpaqueCoordinate<3> opaque_axis(const cgal_vector_t& v) {
|
||||||
|
auto maxval = max_abs3(v.x(), v.y(), v.z());
|
||||||
|
if (maxval == 0) {
|
||||||
|
throw std::runtime_error("Invalid shape type");
|
||||||
|
}
|
||||||
|
return OpaqueCoordinate<3>(
|
||||||
|
new NumberType(v.x() / maxval),
|
||||||
|
new NumberType(v.y() / maxval),
|
||||||
|
new NumberType(v.z() / maxval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpaqueCoordinate<4> opaque_plane(const cgal_plane_t& p) {
|
||||||
|
auto maxval = max_abs3(p.a(), p.b(), p.c());
|
||||||
|
if (maxval == 0) {
|
||||||
|
throw std::runtime_error("Invalid shape type");
|
||||||
|
}
|
||||||
|
return OpaqueCoordinate<4>(
|
||||||
|
new NumberType(p.a() / maxval),
|
||||||
|
new NumberType(p.b() / maxval),
|
||||||
|
new NumberType(p.c() / maxval),
|
||||||
|
new NumberType(p.d() / maxval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cgal_plane_t plane_from_opaque(const OpaqueCoordinate<4>& p) {
|
||||||
|
#ifdef IFOPSH_SIMPLE_KERNEL
|
||||||
|
return cgal_plane_t(
|
||||||
|
p.get(0)->to_double(),
|
||||||
|
p.get(1)->to_double(),
|
||||||
|
p.get(2)->to_double(),
|
||||||
|
p.get(3)->to_double()
|
||||||
|
);
|
||||||
|
#else
|
||||||
|
return cgal_plane_t(
|
||||||
|
static_cast<NumberEpeck*>(p.get(0))->value(),
|
||||||
|
static_cast<NumberEpeck*>(p.get(1))->value(),
|
||||||
|
static_cast<NumberEpeck*>(p.get(2))->value(),
|
||||||
|
static_cast<NumberEpeck*>(p.get(3))->value()
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void insert_normalized_plane_map(plane_map<Kernel_>& mp, const OpaqueCoordinate<4>& from, const OpaqueCoordinate<4>& to) {
|
||||||
|
mp.insert({
|
||||||
|
normalized_plane_for_map<Kernel_>(plane_from_opaque(from)),
|
||||||
|
normalized_plane_for_map<Kernel_>(plane_from_opaque(to))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cgal_vector_t wire_normal(const cgal_wire_t& wire) {
|
||||||
|
typename Kernel_::FT a(0), b(0), c(0);
|
||||||
|
if (wire.size() < 3) {
|
||||||
|
return cgal_vector_t(a, b, c);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < wire.size(); ++i) {
|
||||||
|
const auto& curr = wire[i];
|
||||||
|
const auto& next = wire[(i + 1) % wire.size()];
|
||||||
|
a += (curr.y() - next.y()) * (curr.z() + next.z());
|
||||||
|
b += (curr.z() - next.z()) * (curr.x() + next.x());
|
||||||
|
c += (curr.x() - next.x()) * (curr.y() + next.y());
|
||||||
|
}
|
||||||
|
return cgal_vector_t(a, b, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
cgal_point_t wire_centroid(const cgal_wire_t& wire) {
|
||||||
|
if (wire.empty()) {
|
||||||
|
throw std::runtime_error("Invalid shape type");
|
||||||
|
}
|
||||||
|
std::array<Kernel_::FT, 3> p{ Kernel_::FT(0), Kernel_::FT(0), Kernel_::FT(0) };
|
||||||
|
for (const auto& point : wire) {
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
p[i] += point.cartesian(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kernel_::FT n(wire.size());
|
||||||
|
return cgal_point_t(p[0] / n, p[1] / n, p[2] / n);
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel_::FT wire_length(const cgal_wire_t& wire) {
|
||||||
|
Kernel_::FT len(0);
|
||||||
|
if (wire.size() < 2) {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 1; i < wire.size(); ++i) {
|
||||||
|
len += CGAL::approximate_sqrt(CGAL::Segment_3<Kernel_>(wire[i - 1], wire[i]).squared_length());
|
||||||
|
}
|
||||||
|
if (wire.size() > 2) {
|
||||||
|
len += CGAL::approximate_sqrt(CGAL::Segment_3<Kernel_>(wire.back(), wire.front()).squared_length());
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel_::FT wire_area(const cgal_wire_t& wire) {
|
||||||
|
Kernel_::FT area(0);
|
||||||
|
if (wire.size() < 3) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
const auto& origin = wire.front();
|
||||||
|
for (std::size_t i = 1; i + 1 < wire.size(); ++i) {
|
||||||
|
auto v1 = wire[i] - origin;
|
||||||
|
auto v2 = wire[i + 1] - origin;
|
||||||
|
area += CGAL::approximate_sqrt(CGAL::cross_product(v1, v2).squared_length()) / Kernel_::FT(2);
|
||||||
|
}
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
cgal_wire_t moved_wire(const cgal_wire_t& wire, const cgal_placement_t& trsf) {
|
||||||
|
cgal_wire_t result;
|
||||||
|
result.reserve(wire.size());
|
||||||
|
for (const auto& point : wire) {
|
||||||
|
result.push_back(point.transform(trsf));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_off_point(std::stringstream& sstream, const cgal_point_t& point) {
|
||||||
|
sstream << "OFF\n1 0 0\n";
|
||||||
|
sstream << point.x() << " " << point.y() << " " << point.z() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_off_wire(std::stringstream& sstream, const cgal_wire_t& wire) {
|
||||||
|
const bool face = wire.size() >= 3;
|
||||||
|
sstream << "OFF\n" << wire.size() << " " << (face ? 1 : 0) << " 0\n";
|
||||||
|
for (const auto& point : wire) {
|
||||||
|
sstream << point.x() << " " << point.y() << " " << point.z() << "\n";
|
||||||
|
}
|
||||||
|
if (face) {
|
||||||
|
sstream << wire.size();
|
||||||
|
for (std::size_t i = 0; i < wire.size(); ++i) {
|
||||||
|
sstream << " " << i;
|
||||||
|
}
|
||||||
|
sstream << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Facet>
|
template <typename Facet>
|
||||||
CGAL::Direction_3<Kernel_> newell(Facet& face) {
|
CGAL::Direction_3<Kernel_> newell(Facet& face) {
|
||||||
typename Kernel_::FT a(0), b(0), c(0);
|
typename Kernel_::FT a(0), b(0), c(0);
|
||||||
@@ -102,10 +261,11 @@ namespace {
|
|||||||
ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool convex, Logger& logger) {
|
ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool convex, Logger& logger) {
|
||||||
shape_ = shape;
|
shape_ = shape;
|
||||||
convex_tag_ = convex;
|
convex_tag_ = convex;
|
||||||
|
auto& poly = std::get<cgal_shape_t>(*shape_);
|
||||||
|
|
||||||
std::set<cgal_shape_t::Facet_handle> faces_to_remove;
|
std::set<cgal_shape_t::Facet_handle> faces_to_remove;
|
||||||
|
|
||||||
for (const auto& face : CGAL::faces(*shape_)) {
|
for (const auto& face : CGAL::faces(poly)) {
|
||||||
auto V = newell(*face).to_vector();
|
auto V = newell(*face).to_vector();
|
||||||
CGAL::Plane_3<Kernel_> plane(CGAL::Point_3<Kernel_>(), V);
|
CGAL::Plane_3<Kernel_> plane(CGAL::Point_3<Kernel_>(), V);
|
||||||
auto b1 = plane.base1();
|
auto b1 = plane.base1();
|
||||||
@@ -127,7 +287,7 @@ ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool con
|
|||||||
|
|
||||||
std::vector<CGAL::Point_2<Kernel_>> ps;
|
std::vector<CGAL::Point_2<Kernel_>> ps;
|
||||||
|
|
||||||
for (auto& he1 : CGAL::halfedges_around_face(face->halfedge(), *shape_)) {
|
for (auto& he1 : CGAL::halfedges_around_face(face->halfedge(), poly)) {
|
||||||
const auto& source = he1->vertex()->point();
|
const auto& source = he1->vertex()->point();
|
||||||
ps.push_back(transform_point(source));
|
ps.push_back(transform_point(source));
|
||||||
}
|
}
|
||||||
@@ -140,33 +300,41 @@ ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool con
|
|||||||
|
|
||||||
{
|
{
|
||||||
for (auto& face : faces_to_remove) {
|
for (auto& face : faces_to_remove) {
|
||||||
CGAL::Euler::remove_face(face->halfedge(), *shape_);
|
CGAL::Euler::remove_face(face->halfedge(), poly);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shape.size_of_facets() != 1) {
|
ifcopenshell::geometry::CgalShape::CgalShape(const cgal_point_t& point, bool convex) {
|
||||||
// the size_of_facets() == 1 check is for handling the specical case of
|
shape_ = point;
|
||||||
// storing a single point in a polyhedron as a degenerate triangle
|
convex_tag_ = convex;
|
||||||
//
|
}
|
||||||
// @todo come up with a proper variant for storing lower dimensional entities
|
|
||||||
|
ifcopenshell::geometry::CgalShape::CgalShape(const cgal_wire_t& wire, bool convex) {
|
||||||
// @todo we don't have access to settings here so we don't know whether we should triangulate
|
shape_ = wire;
|
||||||
// remove_degenerate_faces() is also called in the triangulate() call below though...
|
convex_tag_ = convex;
|
||||||
// CGAL::Polygon_mesh_processing::triangulate_faces(*shape_);
|
}
|
||||||
// CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_);
|
|
||||||
|
const cgal_shape_t& ifcopenshell::geometry::CgalShape::poly() const {
|
||||||
|
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||||
|
to_poly();
|
||||||
|
#endif
|
||||||
|
if (!shape_ || !std::holds_alternative<cgal_shape_t>(*shape_)) {
|
||||||
|
throw std::runtime_error("Invalid shape type");
|
||||||
}
|
}
|
||||||
|
return std::get<cgal_shape_t>(*shape_);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef IFOPSH_SIMPLE_KERNEL
|
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||||
void ifcopenshell::geometry::CgalShape::to_poly() const {
|
void ifcopenshell::geometry::CgalShape::to_poly() const {
|
||||||
if (!shape_) {
|
if (!shape_) {
|
||||||
shape_.emplace();
|
cgal_shape_t poly;
|
||||||
|
convert_to_polyhedron(*nef_, poly);
|
||||||
convert_to_polyhedron(*nef_, *shape_);
|
if (poly.size_of_vertices() > 0) {
|
||||||
if (shape_->size_of_vertices() > 0) {
|
|
||||||
// @todo why is this necessary? we have the mark of the volumes?
|
// @todo why is this necessary? we have the mark of the volumes?
|
||||||
CGAL::Polygon_mesh_processing::orient_to_bound_a_volume(*shape_);
|
CGAL::Polygon_mesh_processing::orient_to_bound_a_volume(poly);
|
||||||
}
|
}
|
||||||
|
shape_ = poly;
|
||||||
|
|
||||||
// nef_->convert_to_polyhedron(*shape_);
|
// nef_->convert_to_polyhedron(*shape_);
|
||||||
}
|
}
|
||||||
@@ -174,18 +342,23 @@ void ifcopenshell::geometry::CgalShape::to_poly() const {
|
|||||||
|
|
||||||
void ifcopenshell::geometry::CgalShape::to_nef() const {
|
void ifcopenshell::geometry::CgalShape::to_nef() const {
|
||||||
if (!nef_) {
|
if (!nef_) {
|
||||||
|
auto shp = poly();
|
||||||
if (!convex_tag_) {
|
if (!convex_tag_) {
|
||||||
if (CGAL::Polygon_mesh_processing::does_self_intersect(*shape_)) {
|
if (CGAL::Polygon_mesh_processing::does_self_intersect(shp)) {
|
||||||
throw std::runtime_error("Self-intersections detected, unable to proceed");
|
throw std::runtime_error("Self-intersections detected, unable to proceed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nef_ = utils::create_nef_polyhedron(*shape_);
|
nef_ = utils::create_nef_polyhedron(shp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Settings settings, const ifcopenshell::geometry::taxonomy::matrix4& place, IfcGeom::Representation::Triangulation* t, int item_id, int surface_style_id, Logger& logger) const {
|
void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Settings settings, const ifcopenshell::geometry::taxonomy::matrix4& place, IfcGeom::Representation::Triangulation* t, int item_id, int surface_style_id, Logger& logger) const {
|
||||||
const bool all_triangles = std::all_of(shape_->facets_begin(), shape_->facets_end(), [](auto f) { return f.is_triangle(); });
|
if (is_point() || is_wire()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& base_shape = poly();
|
||||||
|
const bool all_triangles = std::all_of(base_shape.facets_begin(), base_shape.facets_end(), [](auto f) { return f.is_triangle(); });
|
||||||
const bool has_iden_transform = place.is_identity();
|
const bool has_iden_transform = place.is_identity();
|
||||||
|
|
||||||
std::unique_ptr<cgal_shape_t> shape_copy_holder;
|
std::unique_ptr<cgal_shape_t> shape_copy_holder;
|
||||||
@@ -193,10 +366,10 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett
|
|||||||
|
|
||||||
if (!all_triangles || !has_iden_transform) {
|
if (!all_triangles || !has_iden_transform) {
|
||||||
// A copy is made when triangulate_faces() is required or when vertex positions need be transformed
|
// A copy is made when triangulate_faces() is required or when vertex positions need be transformed
|
||||||
shape_copy_holder.reset(new cgal_shape_t(*this));
|
shape_copy_holder.reset(new cgal_shape_t(base_shape));
|
||||||
shape_to_use = shape_copy_holder.get();
|
shape_to_use = shape_copy_holder.get();
|
||||||
} else {
|
} else {
|
||||||
shape_to_use = &*shape_;
|
shape_to_use = const_cast<cgal_shape_t*>(&base_shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool setting_use_original_edges = settings.get<ifcopenshell::geometry::settings::CgalEmitOriginalEdges>().get();
|
const bool setting_use_original_edges = settings.get<ifcopenshell::geometry::settings::CgalEmitOriginalEdges>().get();
|
||||||
@@ -403,25 +576,33 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShape::Serialize(const ifcopenshell::geometry::taxonomy::matrix4& place, std::string& r) const {
|
void ifcopenshell::geometry::CgalShape::Serialize(const ifcopenshell::geometry::taxonomy::matrix4& place, std::string& r) const {
|
||||||
cgal_shape_t s = *this;
|
|
||||||
|
|
||||||
if (!place.is_identity()) {
|
|
||||||
const auto& m = place.ccomponents();
|
|
||||||
|
|
||||||
// @todo check
|
|
||||||
const cgal_placement_t trsf(
|
|
||||||
m(0, 0), m(0, 1), m(0, 2), m(0, 3),
|
|
||||||
m(1, 0), m(1, 1), m(1, 2), m(1, 3),
|
|
||||||
m(2, 0), m(2, 1), m(2, 2), m(2, 3));
|
|
||||||
|
|
||||||
// Apply transformation
|
|
||||||
for (auto &vertex : s.vertex_handles()) {
|
|
||||||
vertex->point() = vertex->point().transform(trsf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::stringstream sstream;
|
std::stringstream sstream;
|
||||||
sstream << s;
|
if (is_point()) {
|
||||||
|
auto p = point();
|
||||||
|
if (!place.is_identity()) {
|
||||||
|
p = p.transform(make_transform(place));
|
||||||
|
}
|
||||||
|
write_off_point(sstream, p);
|
||||||
|
} else if (is_wire()) {
|
||||||
|
auto w = wire();
|
||||||
|
if (!place.is_identity()) {
|
||||||
|
w = moved_wire(w, make_transform(place));
|
||||||
|
}
|
||||||
|
write_off_wire(sstream, w);
|
||||||
|
} else {
|
||||||
|
cgal_shape_t s = poly();
|
||||||
|
|
||||||
|
if (!place.is_identity()) {
|
||||||
|
const auto trsf = make_transform(place);
|
||||||
|
|
||||||
|
// Apply transformation
|
||||||
|
for (auto &vertex : s.vertex_handles()) {
|
||||||
|
vertex->point() = vertex->point().transform(trsf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sstream << s;
|
||||||
|
}
|
||||||
r = sstream.str();
|
r = sstream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,12 +613,26 @@ double ifcopenshell::geometry::CgalShape::bounding_box(void *& b) const {
|
|||||||
b = new CGAL::Bbox_3;
|
b = new CGAL::Bbox_3;
|
||||||
}
|
}
|
||||||
auto& bb = (*((CGAL::Bbox_3*)b));
|
auto& bb = (*((CGAL::Bbox_3*)b));
|
||||||
bb += CGAL::Polygon_mesh_processing::bbox(static_cast<cgal_shape_t>(*this));
|
if (is_point()) {
|
||||||
|
bb += point().bbox();
|
||||||
|
} else if (is_wire()) {
|
||||||
|
for (const auto& point : wire()) {
|
||||||
|
bb += point.bbox();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bb += CGAL::Polygon_mesh_processing::bbox(poly());
|
||||||
|
}
|
||||||
return (bb.xmax() - bb.xmin()) * (bb.ymax() - bb.ymin()) * (bb.zmax() - bb.zmin());
|
return (bb.xmax() - bb.xmin()) * (bb.ymax() - bb.ymin()) * (bb.zmax() - bb.zmin());
|
||||||
}
|
}
|
||||||
|
|
||||||
int ifcopenshell::geometry::CgalShape::num_vertices() const {
|
int ifcopenshell::geometry::CgalShape::num_vertices() const {
|
||||||
return (int) static_cast<cgal_shape_t>(*this).size_of_vertices();
|
if (is_point()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (is_wire()) {
|
||||||
|
return (int) wire().size();
|
||||||
|
}
|
||||||
|
return (int) poly().size_of_vertices();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShape::set_box(void * b) {
|
void ifcopenshell::geometry::CgalShape::set_box(void * b) {
|
||||||
@@ -448,10 +643,13 @@ void ifcopenshell::geometry::CgalShape::set_box(void * b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ifcopenshell::geometry::CgalShape::surface_genus() const {
|
int ifcopenshell::geometry::CgalShape::surface_genus() const {
|
||||||
to_poly();
|
if (is_point() || is_wire()) {
|
||||||
auto nv = shape_->size_of_vertices();
|
return 0;
|
||||||
auto ne = shape_->size_of_halfedges() / 2;
|
}
|
||||||
auto nf = shape_->size_of_facets();
|
const auto& shp = poly();
|
||||||
|
auto nv = shp.size_of_vertices();
|
||||||
|
auto ne = shp.size_of_halfedges() / 2;
|
||||||
|
auto nf = shp.size_of_facets();
|
||||||
|
|
||||||
auto euler = nv - ne + nf;
|
auto euler = nv - ne + nf;
|
||||||
auto genus = (2 - euler) / 2;
|
auto genus = (2 - euler) / 2;
|
||||||
@@ -461,14 +659,22 @@ int ifcopenshell::geometry::CgalShape::surface_genus() const {
|
|||||||
|
|
||||||
bool ifcopenshell::geometry::CgalShape::is_manifold() const {
|
bool ifcopenshell::geometry::CgalShape::is_manifold() const {
|
||||||
// @todo ?
|
// @todo ?
|
||||||
to_poly();
|
return (is_point() || is_wire()) ? true : poly().is_valid();
|
||||||
return shape_->is_valid();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ifcopenshell::geometry::CgalShape::num_edges() const
|
int ifcopenshell::geometry::CgalShape::num_edges() const
|
||||||
{
|
{
|
||||||
to_poly();
|
if (is_point()) {
|
||||||
return (int) shape_->size_of_halfedges() / 2;
|
return 0;
|
||||||
|
}
|
||||||
|
if (is_wire()) {
|
||||||
|
const auto n = wire().size();
|
||||||
|
if (n < 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)(n == 2 ? 1 : n);
|
||||||
|
}
|
||||||
|
return (int) poly().size_of_halfedges() / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ifcopenshell::geometry::CgalShape::num_faces() const
|
int ifcopenshell::geometry::CgalShape::num_faces() const
|
||||||
@@ -479,7 +685,13 @@ int ifcopenshell::geometry::CgalShape::num_faces() const
|
|||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
if (shape_) {
|
if (shape_) {
|
||||||
return (int) shape_->size_of_facets();
|
if (is_poly()) {
|
||||||
|
return (int) poly().size_of_facets();
|
||||||
|
}
|
||||||
|
if (is_wire() && wire().size() >= 3) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -487,46 +699,63 @@ int ifcopenshell::geometry::CgalShape::num_faces() const
|
|||||||
|
|
||||||
OpaqueNumber* ifcopenshell::geometry::CgalShape::CgalShape::length()
|
OpaqueNumber* ifcopenshell::geometry::CgalShape::CgalShape::length()
|
||||||
{
|
{
|
||||||
to_poly();
|
|
||||||
Kernel_::FT len = 0;
|
Kernel_::FT len = 0;
|
||||||
for (auto it = shape_->edges_begin(); it != shape_->edges_end(); ++it) {
|
if (is_wire()) {
|
||||||
len += CGAL::approximate_sqrt(CGAL::Segment_3<Kernel_>(
|
len = wire_length(wire());
|
||||||
it->vertex()->point(),
|
} else if (!is_point()) {
|
||||||
it->next()->vertex()->point()
|
const auto& shp = poly();
|
||||||
).squared_length());
|
for (auto it = shp.edges_begin(); it != shp.edges_end(); ++it) {
|
||||||
|
len += CGAL::approximate_sqrt(CGAL::Segment_3<Kernel_>(
|
||||||
|
it->vertex()->point(),
|
||||||
|
it->opposite()->vertex()->point()
|
||||||
|
).squared_length());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new NumberType(len);
|
return new NumberType(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
OpaqueNumber* ifcopenshell::geometry::CgalShape::area()
|
OpaqueNumber* ifcopenshell::geometry::CgalShape::area()
|
||||||
{
|
{
|
||||||
to_poly();
|
if (is_wire()) {
|
||||||
auto s = *shape_;
|
return new NumberType(wire_area(wire()));
|
||||||
|
}
|
||||||
|
if (is_point()) {
|
||||||
|
return new NumberType(Kernel_::FT(0));
|
||||||
|
}
|
||||||
|
auto s = poly();
|
||||||
CGAL::Polygon_mesh_processing::triangulate_faces(s);
|
CGAL::Polygon_mesh_processing::triangulate_faces(s);
|
||||||
return new NumberType(CGAL::Polygon_mesh_processing::area(s));
|
return new NumberType(CGAL::Polygon_mesh_processing::area(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
OpaqueNumber* ifcopenshell::geometry::CgalShape::volume()
|
OpaqueNumber* ifcopenshell::geometry::CgalShape::volume()
|
||||||
{
|
{
|
||||||
to_poly();
|
if (is_point() || is_wire()) {
|
||||||
auto s = *shape_;
|
return new NumberType(Kernel_::FT(0));
|
||||||
|
}
|
||||||
|
auto s = poly();
|
||||||
CGAL::Polygon_mesh_processing::triangulate_faces(s);
|
CGAL::Polygon_mesh_processing::triangulate_faces(s);
|
||||||
return new NumberType(CGAL::Polygon_mesh_processing::volume(s));
|
return new NumberType(CGAL::Polygon_mesh_processing::volume(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::position()
|
OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::position()
|
||||||
{
|
{
|
||||||
to_poly();
|
if (is_point()) {
|
||||||
if (shape_->size_of_facets() == 1) {
|
return opaque_point(point());
|
||||||
|
}
|
||||||
|
if (is_wire()) {
|
||||||
|
return opaque_point(wire_centroid(wire()));
|
||||||
|
}
|
||||||
|
const auto& shp = poly();
|
||||||
|
if (shp.size_of_facets() == 1) {
|
||||||
// return centroid;
|
// return centroid;
|
||||||
// CGAL::Vector_3<Kernel_> p;
|
// CGAL::Vector_3<Kernel_> p;
|
||||||
std::array<Kernel_::FT, 3> p;
|
std::array<Kernel_::FT, 3> p{ Kernel_::FT(0), Kernel_::FT(0), Kernel_::FT(0) };
|
||||||
for (auto it = shape_->points_begin(); it != shape_->points_end(); ++it) {
|
for (auto it = shp.points_begin(); it != shp.points_end(); ++it) {
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
p[i] += it->cartesian(i);
|
p[i] += it->cartesian(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kernel_::FT N(std::distance(shape_->points_begin(), shape_->points_end()));
|
Kernel_::FT N(std::distance(shp.points_begin(), shp.points_end()));
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
p[i] /= N;
|
p[i] /= N;
|
||||||
}
|
}
|
||||||
@@ -542,19 +771,19 @@ OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::position()
|
|||||||
|
|
||||||
OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::axis()
|
OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::axis()
|
||||||
{
|
{
|
||||||
to_poly();
|
if (is_wire()) {
|
||||||
if (shape_->size_of_facets() == 1) {
|
if (wire().size() == 2) {
|
||||||
auto pl = Plane_equation()(*shape_->facets_begin());
|
return opaque_axis(wire()[1] - wire()[0]);
|
||||||
std::array<typename Kernel_::FT, 3> abc{ pl.a(), pl.b(), pl.c() };
|
}
|
||||||
auto minel = std::min_element(abc.begin(), abc.end());
|
if (wire().size() >= 3) {
|
||||||
auto maxel = std::max_element(abc.begin(), abc.end());
|
return opaque_axis(wire_normal(wire()));
|
||||||
auto maxval = ((-*minel) > *maxel) ? (-*minel) : *maxel;
|
}
|
||||||
|
throw std::runtime_error("Invalid shape type");
|
||||||
return OpaqueCoordinate<3>(
|
}
|
||||||
new NumberType(pl.a() / maxval),
|
auto shp = poly();
|
||||||
new NumberType(pl.b() / maxval),
|
if (shp.size_of_facets() == 1) {
|
||||||
new NumberType(pl.c() / maxval)
|
auto pl = Plane_equation()(*shp.facets_begin());
|
||||||
);
|
return opaque_axis(cgal_vector_t(pl.a(), pl.b(), pl.c()));
|
||||||
} else {
|
} else {
|
||||||
throw std::runtime_error("Invalid shape type");
|
throw std::runtime_error("Invalid shape type");
|
||||||
}
|
}
|
||||||
@@ -562,6 +791,14 @@ OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::axis()
|
|||||||
|
|
||||||
OpaqueCoordinate<4> ifcopenshell::geometry::CgalShape::plane_equation()
|
OpaqueCoordinate<4> ifcopenshell::geometry::CgalShape::plane_equation()
|
||||||
{
|
{
|
||||||
|
if (is_wire() && wire().size() >= 3) {
|
||||||
|
auto normal = wire_normal(wire());
|
||||||
|
return opaque_plane(cgal_plane_t(wire().front(), CGAL::Direction_3<Kernel_>(normal)));
|
||||||
|
}
|
||||||
|
auto shp = poly();
|
||||||
|
if (shp.size_of_facets() == 1) {
|
||||||
|
return opaque_plane(Plane_equation()(*shp.facets_begin()));
|
||||||
|
}
|
||||||
throw std::runtime_error("Invalid shape type");
|
throw std::runtime_error("Invalid shape type");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,75 +849,71 @@ ConversionResultShape * ifcopenshell::geometry::CgalShape::box()
|
|||||||
|
|
||||||
ConversionResultShape* ifcopenshell::geometry::CgalShape::wrap_in_compound()
|
ConversionResultShape* ifcopenshell::geometry::CgalShape::wrap_in_compound()
|
||||||
{
|
{
|
||||||
return new CgalShape(poly(), convex_tag_);
|
return clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::vertices()
|
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::vertices()
|
||||||
{
|
{
|
||||||
// @todo this is ridiculous
|
|
||||||
to_poly();
|
|
||||||
std::vector<ConversionResultShape*> result;
|
std::vector<ConversionResultShape*> result;
|
||||||
for (auto& p : shape_->points()) {
|
if (is_point()) {
|
||||||
std::vector<cgal_point_t> ps = {
|
result.push_back(new CgalShape(point()));
|
||||||
p, p, p
|
return result;
|
||||||
};
|
}
|
||||||
|
if (is_wire()) {
|
||||||
std::vector<std::vector<size_t>> ids(1);
|
for (const auto& p : wire()) {
|
||||||
ids.front().push_back(0);
|
result.push_back(new CgalShape(p));
|
||||||
ids.front().push_back(1);
|
}
|
||||||
ids.front().push_back(2);
|
return result;
|
||||||
|
}
|
||||||
cgal_shape_t poly;
|
for (const auto& p : poly().points()) {
|
||||||
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(ps, ids, poly);
|
result.push_back(new CgalShape(p));
|
||||||
|
|
||||||
result.push_back(new CgalShape(poly));
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::edges()
|
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::edges()
|
||||||
{
|
{
|
||||||
// @todo this is ridiculous
|
|
||||||
to_poly();
|
|
||||||
std::vector<ConversionResultShape*> result;
|
std::vector<ConversionResultShape*> result;
|
||||||
for (auto& ed : shape_->edges()) {
|
if (is_point()) {
|
||||||
std::vector<cgal_point_t> ps = {
|
return result;
|
||||||
ed.vertex()->point(),
|
}
|
||||||
ed.vertex()->point(),
|
if (is_wire()) {
|
||||||
ed.next()->vertex()->point()
|
const auto& w = wire();
|
||||||
};
|
for (std::size_t i = 1; i < w.size(); ++i) {
|
||||||
|
result.push_back(new CgalShape(cgal_wire_t{ w[i - 1], w[i] }));
|
||||||
std::vector<std::vector<size_t>> ids(1);
|
}
|
||||||
ids.front().push_back(0);
|
if (w.size() > 2) {
|
||||||
ids.front().push_back(1);
|
result.push_back(new CgalShape(cgal_wire_t{ w.back(), w.front() }));
|
||||||
ids.front().push_back(2);
|
}
|
||||||
|
return result;
|
||||||
cgal_shape_t poly;
|
}
|
||||||
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(ps, ids, poly);
|
for (auto ed : poly().edges()) {
|
||||||
|
result.push_back(new CgalShape(cgal_wire_t{ ed.vertex()->point(), ed.opposite()->vertex()->point() }));
|
||||||
result.push_back(new CgalShape(poly));
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::facets()
|
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::facets()
|
||||||
{
|
{
|
||||||
to_poly();
|
|
||||||
std::vector<ConversionResultShape*> result;
|
std::vector<ConversionResultShape*> result;
|
||||||
for (auto &face : faces(*shape_)) {
|
if (is_point()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (is_wire()) {
|
||||||
|
if (wire().size() >= 3) {
|
||||||
|
result.push_back(new CgalShape(wire()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (auto face : faces(poly())) {
|
||||||
std::vector<cgal_point_t> ps;
|
std::vector<cgal_point_t> ps;
|
||||||
std::vector<std::vector<size_t>> ids(1);
|
|
||||||
|
|
||||||
auto it = face->facet_begin();
|
auto it = face->facet_begin();
|
||||||
do {
|
do {
|
||||||
ps.push_back(it->vertex()->point());
|
ps.push_back(it->vertex()->point());
|
||||||
ids.front().push_back(ids.front().size());
|
|
||||||
} while (++it != face->facet_begin());
|
} while (++it != face->facet_begin());
|
||||||
|
|
||||||
cgal_shape_t poly;
|
result.push_back(new CgalShape(ps));
|
||||||
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(ps, ids, poly);
|
|
||||||
|
|
||||||
result.push_back(new CgalShape(poly));
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -756,31 +989,31 @@ std::pair<OpaqueCoordinate<3>, OpaqueCoordinate<3>> ifcopenshell::geometry::Cgal
|
|||||||
|
|
||||||
ConversionResultShape* ifcopenshell::geometry::CgalShape::moved(ifcopenshell::geometry::taxonomy::matrix4::ptr place) const
|
ConversionResultShape* ifcopenshell::geometry::CgalShape::moved(ifcopenshell::geometry::taxonomy::matrix4::ptr place) const
|
||||||
{
|
{
|
||||||
cgal_shape_t s = *this;
|
if (place->is_identity()) {
|
||||||
|
return clone();
|
||||||
|
}
|
||||||
|
|
||||||
if (!place->is_identity()) {
|
const auto trsf = make_transform(*place);
|
||||||
const auto& m = place->ccomponents();
|
if (is_point()) {
|
||||||
|
return new CgalShape(point().transform(trsf), convex_tag_);
|
||||||
|
}
|
||||||
|
if (is_wire()) {
|
||||||
|
return new CgalShape(moved_wire(wire(), trsf), convex_tag_);
|
||||||
|
}
|
||||||
|
|
||||||
// @todo check
|
cgal_shape_t s = poly();
|
||||||
const cgal_placement_t trsf(
|
for (auto &vertex : s.vertex_handles()) {
|
||||||
m(0, 0), m(0, 1), m(0, 2), m(0, 3),
|
vertex->point() = vertex->point().transform(trsf);
|
||||||
m(1, 0), m(1, 1), m(1, 2), m(1, 3),
|
|
||||||
m(2, 0), m(2, 1), m(2, 2), m(2, 3));
|
|
||||||
|
|
||||||
// Apply transformation
|
|
||||||
for (auto &vertex : s.vertex_handles()) {
|
|
||||||
vertex->point() = vertex->point().transform(trsf);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CgalShape(s, convex_tag_);
|
return new CgalShape(s, convex_tag_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShape::map(OpaqueCoordinate<4>&, OpaqueCoordinate<4>&) {
|
std::size_t ifcopenshell::geometry::CgalShape::map(OpaqueCoordinate<4>&, OpaqueCoordinate<4>&) {
|
||||||
throw std::runtime_error("Not implemented");
|
throw std::runtime_error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShape::map(const std::vector<OpaqueCoordinate<4>>&, const std::vector<OpaqueCoordinate<4>>&) {
|
std::size_t ifcopenshell::geometry::CgalShape::map(const std::vector<OpaqueCoordinate<4>>&, const std::vector<OpaqueCoordinate<4>>&) {
|
||||||
throw std::runtime_error("Not implemented");
|
throw std::runtime_error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,27 +1190,16 @@ ConversionResultShape* ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::
|
|||||||
throw std::runtime_error("Not implemented");
|
throw std::runtime_error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to) {
|
std::size_t ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to) {
|
||||||
plane_map<Kernel_> mp;
|
plane_map<Kernel_> mp;
|
||||||
mp.insert({
|
insert_normalized_plane_map(mp, from, to);
|
||||||
CGAL::Plane_3<Kernel_>(
|
std::size_t mutated = 0;
|
||||||
static_cast<NumberEpeck*>(from.get(0))->value(),
|
auto nw = shape_->map(mp, mutated);
|
||||||
static_cast<NumberEpeck*>(from.get(1))->value(),
|
|
||||||
static_cast<NumberEpeck*>(from.get(2))->value(),
|
|
||||||
static_cast<NumberEpeck*>(from.get(3))->value()
|
|
||||||
),
|
|
||||||
CGAL::Plane_3<Kernel_>(
|
|
||||||
static_cast<NumberEpeck*>(to.get(0))->value(),
|
|
||||||
static_cast<NumberEpeck*>(to.get(1))->value(),
|
|
||||||
static_cast<NumberEpeck*>(to.get(2))->value(),
|
|
||||||
static_cast<NumberEpeck*>(to.get(3))->value()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
auto nw = shape_->map(mp);
|
|
||||||
shape_ = std::move(nw);
|
shape_ = std::move(nw);
|
||||||
|
return mutated;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::map(const std::vector<OpaqueCoordinate<4>>& froms, const std::vector<OpaqueCoordinate<4>>& tos) {
|
std::size_t ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::map(const std::vector<OpaqueCoordinate<4>>& froms, const std::vector<OpaqueCoordinate<4>>& tos) {
|
||||||
plane_map<Kernel_> mp;
|
plane_map<Kernel_> mp;
|
||||||
if (froms.size() != tos.size()) {
|
if (froms.size() != tos.size()) {
|
||||||
throw std::runtime_error("Expected equal size");
|
throw std::runtime_error("Expected equal size");
|
||||||
@@ -987,23 +1209,12 @@ void ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::map(const std::vec
|
|||||||
for (; it < froms.end(); ++it, ++jt) {
|
for (; it < froms.end(); ++it, ++jt) {
|
||||||
auto& from = *it;
|
auto& from = *it;
|
||||||
auto& to = *jt;
|
auto& to = *jt;
|
||||||
mp.insert({
|
insert_normalized_plane_map(mp, from, to);
|
||||||
CGAL::Plane_3<Kernel_>(
|
|
||||||
static_cast<NumberEpeck*>(from.get(0))->value(),
|
|
||||||
static_cast<NumberEpeck*>(from.get(1))->value(),
|
|
||||||
static_cast<NumberEpeck*>(from.get(2))->value(),
|
|
||||||
static_cast<NumberEpeck*>(from.get(3))->value()
|
|
||||||
),
|
|
||||||
CGAL::Plane_3<Kernel_>(
|
|
||||||
static_cast<NumberEpeck*>(to.get(0))->value(),
|
|
||||||
static_cast<NumberEpeck*>(to.get(1))->value(),
|
|
||||||
static_cast<NumberEpeck*>(to.get(2))->value(),
|
|
||||||
static_cast<NumberEpeck*>(to.get(3))->value()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
auto nw = shape_->map(mp);
|
std::size_t mutated = 0;
|
||||||
|
auto nw = shape_->map(mp, mutated);
|
||||||
shape_ = std::move(nw);
|
shape_ = std::move(nw);
|
||||||
|
return mutated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1012,4 +1223,4 @@ ConversionResultShape* ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::
|
|||||||
throw std::runtime_error("Not implemented");
|
throw std::runtime_error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
#include <CGAL/Polygon_mesh_processing/compute_normal.h>
|
#include <CGAL/Polygon_mesh_processing/compute_normal.h>
|
||||||
#include <CGAL/Polygon_mesh_processing/self_intersections.h>
|
#include <CGAL/Polygon_mesh_processing/self_intersections.h>
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#ifdef IFOPSH_SIMPLE_KERNEL
|
#ifdef IFOPSH_SIMPLE_KERNEL
|
||||||
|
|
||||||
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
|
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
|
||||||
@@ -179,13 +181,17 @@ namespace ifcopenshell { namespace geometry {
|
|||||||
|
|
||||||
class IFC_GEOMLIBRARY_API CgalShape : public IfcGeom::ConversionResultShape {
|
class IFC_GEOMLIBRARY_API CgalShape : public IfcGeom::ConversionResultShape {
|
||||||
private:
|
private:
|
||||||
|
typedef std::variant<cgal_shape_t, cgal_point_t, cgal_wire_t> cgal_shape_storage_t;
|
||||||
|
|
||||||
bool convex_tag_ = false;
|
bool convex_tag_ = false;
|
||||||
mutable boost::optional<cgal_shape_t> shape_;
|
mutable boost::optional<cgal_shape_storage_t> shape_;
|
||||||
#ifndef IFOPSH_SIMPLE_KERNEL
|
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||||
mutable boost::optional<CGAL::Nef_polyhedron_3<Kernel_>> nef_;
|
mutable boost::optional<CGAL::Nef_polyhedron_3<Kernel_>> nef_;
|
||||||
#endif
|
#endif
|
||||||
public:
|
public:
|
||||||
CgalShape(const cgal_shape_t& shape, bool convex = false, Logger& logger = Logger::Root());
|
CgalShape(const cgal_shape_t& shape, bool convex = false, Logger& logger = Logger::Root());
|
||||||
|
CgalShape(const cgal_point_t& point, bool convex = false);
|
||||||
|
CgalShape(const cgal_wire_t& wire, bool convex = false);
|
||||||
|
|
||||||
#ifndef IFOPSH_SIMPLE_KERNEL
|
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||||
CgalShape(const CGAL::Nef_polyhedron_3<Kernel_>& shape, bool convex = false) {
|
CgalShape(const CGAL::Nef_polyhedron_3<Kernel_>& shape, bool convex = false) {
|
||||||
@@ -206,14 +212,29 @@ namespace ifcopenshell { namespace geometry {
|
|||||||
void to_poly() const {}
|
void to_poly() const {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
operator const cgal_shape_t& () const { to_poly(); return *shape_; }
|
operator const cgal_shape_t& () const { return poly(); }
|
||||||
const cgal_shape_t& poly() const { to_poly(); return *shape_; }
|
const cgal_shape_t& poly() const;
|
||||||
|
bool is_poly() const { return shape_ && std::holds_alternative<cgal_shape_t>(*shape_); }
|
||||||
|
bool is_point() const { return shape_ && std::holds_alternative<cgal_point_t>(*shape_); }
|
||||||
|
bool is_wire() const { return shape_ && std::holds_alternative<cgal_wire_t>(*shape_); }
|
||||||
|
const cgal_point_t& point() const { return std::get<cgal_point_t>(*shape_); }
|
||||||
|
const cgal_wire_t& wire() const { return std::get<cgal_wire_t>(*shape_); }
|
||||||
|
|
||||||
virtual void Triangulate(ifcopenshell::geometry::Settings settings, const ifcopenshell::geometry::taxonomy::matrix4& place, IfcGeom::Representation::Triangulation* t, int item_id, int surface_style_id, Logger& logger = Logger::Root()) const;
|
virtual void Triangulate(ifcopenshell::geometry::Settings settings, const ifcopenshell::geometry::taxonomy::matrix4& place, IfcGeom::Representation::Triangulation* t, int item_id, int surface_style_id, Logger& logger = Logger::Root()) const;
|
||||||
virtual void Serialize(const ifcopenshell::geometry::taxonomy::matrix4& place, std::string&) const;
|
virtual void Serialize(const ifcopenshell::geometry::taxonomy::matrix4& place, std::string&) const;
|
||||||
|
|
||||||
virtual IfcGeom::ConversionResultShape* clone() const {
|
virtual IfcGeom::ConversionResultShape* clone() const {
|
||||||
return new CgalShape(*shape_);
|
if (shape_) {
|
||||||
|
return std::visit([this](const auto& value) -> IfcGeom::ConversionResultShape* {
|
||||||
|
return new CgalShape(value, convex_tag_);
|
||||||
|
}, *shape_);
|
||||||
|
}
|
||||||
|
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||||
|
if (nef_) {
|
||||||
|
return new CgalShape(*nef_, convex_tag_);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool is_manifold() const;
|
virtual bool is_manifold() const;
|
||||||
@@ -255,8 +276,8 @@ namespace ifcopenshell { namespace geometry {
|
|||||||
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
||||||
virtual ConversionResultShape* concat(ConversionResultShape*);
|
virtual ConversionResultShape* concat(ConversionResultShape*);
|
||||||
|
|
||||||
virtual void map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
virtual std::size_t map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
||||||
virtual void map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
virtual std::size_t map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
||||||
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const;
|
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const;
|
||||||
|
|
||||||
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const;
|
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const;
|
||||||
@@ -326,8 +347,8 @@ namespace ifcopenshell { namespace geometry {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
virtual std::size_t map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
||||||
virtual void map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
virtual std::size_t map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
||||||
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const;
|
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const;
|
||||||
|
|
||||||
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const {
|
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
#include <boost/iterator/transform_iterator.hpp>
|
#include <boost/iterator/transform_iterator.hpp>
|
||||||
#include <boost/graph/copy.hpp>
|
#include <boost/graph/copy.hpp>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -116,6 +117,23 @@ template <typename Kernel>
|
|||||||
using plane_map = std::map<typename Kernel::Plane_3, typename Kernel::Plane_3, PlaneLess<Kernel>>;
|
using plane_map = std::map<typename Kernel::Plane_3, typename Kernel::Plane_3, PlaneLess<Kernel>>;
|
||||||
// using plane_map = std::unordered_map<typename Kernel::Plane_3, typename Kernel::Plane_3, PlaneHash<Kernel>>;
|
// using plane_map = std::unordered_map<typename Kernel::Plane_3, typename Kernel::Plane_3, PlaneHash<Kernel>>;
|
||||||
|
|
||||||
|
template <typename Kernel>
|
||||||
|
typename Kernel::Plane_3 normalized_plane_for_map(const typename Kernel::Plane_3& plane) {
|
||||||
|
std::array<typename Kernel::FT, 3> abc{ plane.a(), plane.b(), plane.c() };
|
||||||
|
auto minel = std::min_element(abc.begin(), abc.end());
|
||||||
|
auto maxel = std::max_element(abc.begin(), abc.end());
|
||||||
|
auto maxval = ((-*minel) > *maxel) ? (-*minel) : *maxel;
|
||||||
|
if (maxval == 0) {
|
||||||
|
return plane;
|
||||||
|
}
|
||||||
|
return typename Kernel::Plane_3(
|
||||||
|
plane.a() / maxval,
|
||||||
|
plane.b() / maxval,
|
||||||
|
plane.c() / maxval,
|
||||||
|
plane.d() / maxval
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Lexicographic comparator for CGAL Point_d (operator< is deleted in CGAL 6.x)
|
// Lexicographic comparator for CGAL Point_d (operator< is deleted in CGAL 6.x)
|
||||||
struct Point_d_4d_Less {
|
struct Point_d_4d_Less {
|
||||||
using Point_d = CGAL::Epick_d<CGAL::Dimension_tag<4>>::Point_d;
|
using Point_d = CGAL::Epick_d<CGAL::Dimension_tag<4>>::Point_d;
|
||||||
@@ -264,7 +282,11 @@ class halfspace_tree {
|
|||||||
public:
|
public:
|
||||||
virtual CGAL::Nef_polyhedron_3<Kernel> evaluate() const = 0;
|
virtual CGAL::Nef_polyhedron_3<Kernel> evaluate() const = 0;
|
||||||
virtual void accumulate(std::list<typename Kernel::Plane_3>&) const = 0;
|
virtual void accumulate(std::list<typename Kernel::Plane_3>&) const = 0;
|
||||||
virtual std::unique_ptr<halfspace_tree> map(const plane_map<Kernel>&) const = 0;
|
std::unique_ptr<halfspace_tree> map(const plane_map<Kernel>& m) const {
|
||||||
|
std::size_t ignored = 0;
|
||||||
|
return map(m, ignored);
|
||||||
|
}
|
||||||
|
virtual std::unique_ptr<halfspace_tree> map(const plane_map<Kernel>&, std::size_t& mutated) const = 0;
|
||||||
virtual std::string dump(int level = 0) const = 0;
|
virtual std::string dump(int level = 0) const = 0;
|
||||||
virtual tree_type kind() const = 0;
|
virtual tree_type kind() const = 0;
|
||||||
virtual void merge(CGAL::Nef_polyhedron_3<Kernel>&) const = 0;
|
virtual void merge(CGAL::Nef_polyhedron_3<Kernel>&) const = 0;
|
||||||
@@ -366,10 +388,10 @@ public:
|
|||||||
op->accumulate(points);
|
op->accumulate(points);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
virtual std::unique_ptr<halfspace_tree<Kernel>> map(const plane_map<Kernel>& m) const {
|
virtual std::unique_ptr<halfspace_tree<Kernel>> map(const plane_map<Kernel>& m, std::size_t& mutated) const {
|
||||||
decltype(operands_) mapped;
|
decltype(operands_) mapped;
|
||||||
for (auto& op : operands_) {
|
for (auto& op : operands_) {
|
||||||
mapped.emplace_back(op->map(m));
|
mapped.emplace_back(op->map(m, mutated));
|
||||||
}
|
}
|
||||||
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_nary_branch(operation_, std::move(mapped)));
|
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_nary_branch(operation_, std::move(mapped)));
|
||||||
}
|
}
|
||||||
@@ -485,21 +507,12 @@ public:
|
|||||||
virtual void accumulate(std::list<typename Kernel::Plane_3>& points) const {
|
virtual void accumulate(std::list<typename Kernel::Plane_3>& points) const {
|
||||||
points.push_back(plane_);
|
points.push_back(plane_);
|
||||||
}
|
}
|
||||||
virtual std::unique_ptr<halfspace_tree<Kernel>> map(const plane_map<Kernel>& m) const {
|
virtual std::unique_ptr<halfspace_tree<Kernel>> map(const plane_map<Kernel>& m, std::size_t& mutated) const {
|
||||||
|
CGAL::Plane_3<Kernel> pp = normalized_plane_for_map<Kernel>(plane_);
|
||||||
std::array<typename Kernel::FT, 3> abc{ plane_.a(), plane_.b(), plane_.c() };
|
|
||||||
auto minel = std::min_element(abc.begin(), abc.end());
|
|
||||||
auto maxel = std::max_element(abc.begin(), abc.end());
|
|
||||||
auto maxval = ((-*minel) > *maxel) ? (-*minel) : *maxel;
|
|
||||||
CGAL::Plane_3<Kernel> pp(
|
|
||||||
plane_.a() / maxval,
|
|
||||||
plane_.b() / maxval,
|
|
||||||
plane_.c() / maxval,
|
|
||||||
plane_.d() / maxval
|
|
||||||
);
|
|
||||||
|
|
||||||
auto it = m.find(pp);
|
auto it = m.find(pp);
|
||||||
if (it != m.end()) {
|
if (it != m.end()) {
|
||||||
|
++mutated;
|
||||||
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_plane(it->second));
|
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_plane(it->second));
|
||||||
} else {
|
} else {
|
||||||
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_plane(plane_));
|
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_plane(plane_));
|
||||||
@@ -1358,4 +1371,4 @@ bool write_to_obj(const CGAL::Nef_polyhedron_3<Kernel>& a, std::ostream& ofs, si
|
|||||||
return volume_index == std::numeric_limits<size_t>::max();
|
return volume_index == std::numeric_limits<size_t>::max();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -696,10 +696,10 @@ bool ifcopenshell::geometry::OpenCascadeShape::surface_area_along_direction(doub
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::OpenCascadeShape::map(OpaqueCoordinate<4>&, OpaqueCoordinate<4>&) {
|
std::size_t ifcopenshell::geometry::OpenCascadeShape::map(OpaqueCoordinate<4>&, OpaqueCoordinate<4>&) {
|
||||||
throw std::runtime_error("Not implemented");
|
throw std::runtime_error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ifcopenshell::geometry::OpenCascadeShape::map(const std::vector<OpaqueCoordinate<4>>&, const std::vector<OpaqueCoordinate<4>>&) {
|
std::size_t ifcopenshell::geometry::OpenCascadeShape::map(const std::vector<OpaqueCoordinate<4>>&, const std::vector<OpaqueCoordinate<4>>&) {
|
||||||
throw std::runtime_error("Not implemented");
|
throw std::runtime_error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ namespace ifcopenshell {
|
|||||||
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
||||||
virtual ConversionResultShape* concat(ConversionResultShape*);
|
virtual ConversionResultShape* concat(ConversionResultShape*);
|
||||||
|
|
||||||
virtual void map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
virtual std::size_t map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
||||||
virtual void map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
virtual std::size_t map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
||||||
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const;
|
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const;
|
||||||
|
|
||||||
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const;
|
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geometry::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const;
|
||||||
@@ -113,4 +113,4 @@ namespace ifcopenshell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import ifcopenshell
|
||||||
|
|
||||||
|
def test_skip_over_non_entity_instance():
|
||||||
|
data = """
|
||||||
|
ISO-10303-21;
|
||||||
|
HEADER;
|
||||||
|
FILE_DESCRIPTION((''),'2;1');
|
||||||
|
FILE_NAME('','',(''),(''),'','','');
|
||||||
|
FILE_SCHEMA(('IFC2X3'));
|
||||||
|
ENDSEC;
|
||||||
|
DATA;
|
||||||
|
#1=IFCLENGTHMEASURE(0.1);
|
||||||
|
#5=IFCCARTESIANPOINT((0.,0.));
|
||||||
|
ENDSEC;
|
||||||
|
END-ISO-10303-21;
|
||||||
|
"""
|
||||||
|
f = ifcopenshell.file.from_string(data)
|
||||||
|
print(ifcopenshell.get_log())
|
||||||
|
f.by_id(5)
|
||||||
@@ -725,7 +725,8 @@ std::optional<std::tuple<size_t, const IfcParse::declaration*, IfcEntityInstance
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entity_type->as_entity() == nullptr) {
|
if (entity_type->as_entity() == nullptr) {
|
||||||
logger_.get().Message(Logger::LOG_ERROR, "SYN", 4, "Non entity type " + entity_type->name() + " at offset " + std::to_string(token_stream_[2].startPos));
|
logger_.get().Message(Logger::LOG_ERROR, "SYN", 4, "Non-entity type " + entity_type->name() + " at offset " + std::to_string(token_stream_[2].startPos));
|
||||||
|
current_id = 0;
|
||||||
goto advance;
|
goto advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user