mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 02:23:34 +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,
|
||||
DoorSwingReadonlyDecorator,
|
||||
MEPSegmentExtendPreviewDecorator,
|
||||
MEPSystemPathDecorator,
|
||||
SlabDirectionDecorator,
|
||||
WallAxisDecorator,
|
||||
WallFilletPreviewDecorator,
|
||||
WallSystemPathDecorator,
|
||||
)
|
||||
from bonsai.bim.module.model.wall import WallGizmoPreviewDecorator
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
@@ -513,6 +515,8 @@ def _install_viewport_overlays() -> None:
|
||||
NestDecorator.uninstall()
|
||||
WallAxisDecorator.uninstall()
|
||||
SlabDirectionDecorator.uninstall()
|
||||
MEPSystemPathDecorator.uninstall()
|
||||
WallSystemPathDecorator.uninstall()
|
||||
WallFilletPreviewDecorator.uninstall()
|
||||
BendPreviewDecorator.uninstall()
|
||||
MEPSegmentExtendPreviewDecorator.uninstall()
|
||||
@@ -532,6 +536,9 @@ def _install_viewport_overlays() -> None:
|
||||
WallAxisDecorator.install(bpy.context)
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
if model_props.show_paths:
|
||||
MEPSystemPathDecorator.install(bpy.context)
|
||||
WallSystemPathDecorator.install(bpy.context)
|
||||
if model_props.show_bounding_box:
|
||||
BoundingBoxDecorator.install(bpy.context)
|
||||
# Always-installed: draw() self-polls on Scene.BIMPreviewProperties.
|
||||
|
||||
@@ -378,6 +378,11 @@ def unregister():
|
||||
# half-unloaded module state.
|
||||
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:
|
||||
for tool_data in reversed(tools):
|
||||
bpy.utils.unregister_tool(tool_data.tool)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from math import cos, pi, radians, sin, tan
|
||||
from typing import Any, Literal, NamedTuple
|
||||
|
||||
@@ -43,6 +44,7 @@ from mathutils import Matrix, Quaternion, Vector
|
||||
|
||||
import bonsai.core.geometry
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.decorator_cache import TokenCache
|
||||
from bonsai.bim.module.drawing.gizmos import (
|
||||
ARC_SEGMENTS,
|
||||
DOOR_SWING_ANGLE_MAX,
|
||||
@@ -62,6 +64,41 @@ def highlight_color(color, alpha=0.1):
|
||||
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:
|
||||
installed = None
|
||||
|
||||
@@ -2529,3 +2566,461 @@ def draw_polyline_segments(
|
||||
|
||||
_BBOX_HIGHLIGHT_LINE_WIDTH = 1.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.decorator import (
|
||||
BoundingBoxDecorator,
|
||||
MEPSystemPathDecorator,
|
||||
SlabDirectionDecorator,
|
||||
WallAxisDecorator,
|
||||
WallSystemPathDecorator,
|
||||
)
|
||||
from bonsai.bim.module.model.door import update_door_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()
|
||||
|
||||
|
||||
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:
|
||||
if self.show_bounding_box:
|
||||
BoundingBoxDecorator.install(context)
|
||||
@@ -354,6 +369,19 @@ class BIMModelProperties(PropertyGroup):
|
||||
default=False,
|
||||
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_show_gizmo_object_translate: bpy.props.BoolProperty(name="Previous Gizmo Translate")
|
||||
@@ -401,6 +429,7 @@ class BIMModelProperties(PropertyGroup):
|
||||
offset: float
|
||||
show_wall_axis: bool
|
||||
show_slab_direction: bool
|
||||
show_paths: bool
|
||||
|
||||
prev_transform_orientation_slot_type: str
|
||||
prev_show_gizmo_object_translate: bool
|
||||
|
||||
@@ -162,8 +162,8 @@ class ProjectLibraryData:
|
||||
library_file = IfcStore.library_file
|
||||
if library_file is None or library_file.schema == "IFC2X3":
|
||||
return results
|
||||
project = library_file.by_type("IfcProject")[0]
|
||||
results.append((str(project.id()), f"IfcProject {project.Name or 'Unnamed'}", project.Description or ""))
|
||||
root = tool.Project.get_root_context(library_file)
|
||||
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():
|
||||
results.append((str(library_id), data["Name"] or "Unnamed", data["Description"] or ""))
|
||||
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)}
|
||||
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)
|
||||
tool.Project.load_project_libraries_to_ui(ifc_project, hierarchy)
|
||||
tool.Project.load_project_libraries_to_ui(root_context, hierarchy)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -763,7 +763,10 @@ class EditProjectLibrary(bpy.types.Operator):
|
||||
previous_parent_library = tool.Project.get_parent_library(project_library)
|
||||
new_parent_library = library_file.by_id(int(props.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.
|
||||
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
||||
else: # Previous is IfcProjectLibrary.
|
||||
@@ -804,9 +807,12 @@ class AddProjectLibrary(bpy.types.Operator):
|
||||
props = tool.Project.get_project_props()
|
||||
library_file = IfcStore.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")
|
||||
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.
|
||||
props.selected_project_library = str(project_library.id())
|
||||
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.",
|
||||
)
|
||||
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.is_loading = True
|
||||
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))
|
||||
self.project_library_attributes.clear()
|
||||
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.
|
||||
return
|
||||
|
||||
|
||||
@@ -1925,6 +1925,7 @@ class BIM_PT_decorators_overlay(Panel):
|
||||
aggregate_props = tool.Aggregate.get_aggregate_props()
|
||||
nest_props = tool.Nest.get_nest_props()
|
||||
model_props = tool.Model.get_model_props()
|
||||
system_props = tool.System.get_system_props()
|
||||
display_all = overlay.show_overlays
|
||||
|
||||
col = layout.column()
|
||||
@@ -1942,6 +1943,9 @@ class BIM_PT_decorators_overlay(Panel):
|
||||
row = col.row(align=True)
|
||||
row.prop(model_props, "show_slab_direction", text="Slab Direction")
|
||||
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 = col.row(align=True)
|
||||
row.prop(model_props, "show_cut_decorator", text="Cut Decorator")
|
||||
|
||||
@@ -38,15 +38,16 @@ if TYPE_CHECKING:
|
||||
PlaneTuple = tuple[float, float, float, float]
|
||||
PlaneSet = tuple[PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple, PlaneTuple]
|
||||
|
||||
# Outward margin (world units) so the empty's CUBE display edges sit
|
||||
# safely INSIDE the clip volume. Absolute (not relative-to-scale)
|
||||
# because a relative multiplier balloons with scale and produces a
|
||||
# visibly-wrong gap between the wireframe and the clipped geometry.
|
||||
# Sub-mesh-precision value: visually invisible at any reasonable IFC
|
||||
# scale yet large enough to keep the empty's own wireframe edges off
|
||||
# the clip planes when float-precision accumulation pushes a corner
|
||||
# a fractional epsilon outward.
|
||||
# Outward margins so the empty's CUBE display edges sit safely INSIDE
|
||||
# the clip volume. A fixed absolute margin fails under rotation: the
|
||||
# float error in computing each column's length and in per-vertex dot
|
||||
# products at GPU rasterisation scales with the axis's world
|
||||
# half-extent, so once the box is spawned at any non-trivial scale it
|
||||
# can exceed an absolute floor. The relative term tracks that drift;
|
||||
# the absolute term catches sub-unit boxes where the relative term
|
||||
# shrinks below float precision.
|
||||
_CLIP_EXPAND_ABS = 1e-6
|
||||
_CLIP_EXPAND_REL = 1e-5
|
||||
|
||||
|
||||
class ClipBox:
|
||||
@@ -63,6 +64,11 @@ class ClipBox:
|
||||
|
||||
_owned: set[int] = set()
|
||||
_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
|
||||
_last_seen_ifc_id: int = 0
|
||||
# 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
|
||||
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
|
||||
def compute_planes_from_matrix(cls, matrix: Any) -> PlaneSet:
|
||||
@@ -163,7 +171,7 @@ class ClipBox:
|
||||
transform offset, while ``obj.matrix_world`` stays at the
|
||||
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
|
||||
def apply_clip_planes(cls, planes: PlaneSet) -> None:
|
||||
@@ -183,6 +191,7 @@ class ClipBox:
|
||||
cls._owned.add(key)
|
||||
cls._region_by_key[key] = (area, region)
|
||||
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
|
||||
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."""
|
||||
cls._owned.clear()
|
||||
cls._region_by_key.clear()
|
||||
cls._view_matrix_at_arm.clear()
|
||||
|
||||
PSET_NAME = "BBIM_ClipBoxes"
|
||||
COLLECTION_NAME = "BBIM_ClipBoxes"
|
||||
@@ -533,10 +543,16 @@ class ClipBox:
|
||||
cls._last_seen_ifc_id = ifc_id
|
||||
cls._owned.clear()
|
||||
cls._region_by_key.clear()
|
||||
cls._view_matrix_at_arm.clear()
|
||||
cls._persisted_matrices.clear()
|
||||
cls._last_seen_object_matrices.clear()
|
||||
if ifc_file is not None:
|
||||
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
|
||||
# dragging so the active-index change on adoption can't disrupt
|
||||
# the move.
|
||||
@@ -557,6 +573,12 @@ class ClipBox:
|
||||
# one save on release, not N saves per frame.
|
||||
if ifc_file is not None and prev_matrix is not None:
|
||||
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)
|
||||
|
||||
try:
|
||||
@@ -594,6 +616,17 @@ class ClipBox:
|
||||
return
|
||||
region_3d.clip_planes = cls.compute_planes_from_matrix(matrix)
|
||||
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
|
||||
|
||||
@@ -32,6 +32,7 @@ from typing import (
|
||||
NotRequired,
|
||||
Optional,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
import bpy
|
||||
@@ -376,12 +377,31 @@ class Project(bonsai.core.tool.Project):
|
||||
)
|
||||
|
||||
@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:
|
||||
# IfcProjectLibrary.
|
||||
return nests[0].RelatingObject
|
||||
# IfcProject.
|
||||
return project_library.HasContext[0].RelatingContext
|
||||
if has_context := project_library.HasContext:
|
||||
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
|
||||
def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict:
|
||||
@@ -401,6 +421,8 @@ class Project(bonsai.core.tool.Project):
|
||||
return hierarchy
|
||||
for project_library in ifc_file.by_type("IfcProjectLibrary"):
|
||||
parent_library = cls.get_parent_library(project_library)
|
||||
if parent_library is None:
|
||||
continue
|
||||
hierarchy[parent_library][project_library] = hierarchy[project_library]
|
||||
return hierarchy
|
||||
|
||||
|
||||
@@ -114,6 +114,33 @@ class TestComputePlanes(NewFile):
|
||||
planes = tool.ClipBox.compute_planes(host)
|
||||
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):
|
||||
def test_flips_scene_enabled_flag(self):
|
||||
@@ -690,3 +717,84 @@ class TestCapRebuildDebounce(NewFile):
|
||||
):
|
||||
tool.ClipBox.on_depsgraph_update_caps(bpy.context.scene, None)
|
||||
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(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>
|
||||
|
||||
#if USE_VLD
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "../ifcgeom/ConversionSettings.h"
|
||||
#include "../ifcgeom/taxonomy.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
@@ -293,8 +294,8 @@ namespace IfcGeom {
|
||||
virtual ConversionResultShape* intersect(ConversionResultShape*) = 0;
|
||||
virtual ConversionResultShape* concat(ConversionResultShape*) = 0;
|
||||
|
||||
virtual void 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(OpaqueCoordinate<4>& from, 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 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;
|
||||
|
||||
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>
|
||||
CGAL::Direction_3<Kernel_> newell(Facet& face) {
|
||||
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) {
|
||||
shape_ = shape;
|
||||
convex_tag_ = convex;
|
||||
auto& poly = std::get<cgal_shape_t>(*shape_);
|
||||
|
||||
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();
|
||||
CGAL::Plane_3<Kernel_> plane(CGAL::Point_3<Kernel_>(), V);
|
||||
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;
|
||||
|
||||
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();
|
||||
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) {
|
||||
CGAL::Euler::remove_face(face->halfedge(), *shape_);
|
||||
CGAL::Euler::remove_face(face->halfedge(), poly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shape.size_of_facets() != 1) {
|
||||
// the size_of_facets() == 1 check is for handling the specical case of
|
||||
// storing a single point in a polyhedron as a degenerate triangle
|
||||
//
|
||||
// @todo come up with a proper variant for storing lower dimensional entities
|
||||
|
||||
// @todo we don't have access to settings here so we don't know whether we should triangulate
|
||||
// remove_degenerate_faces() is also called in the triangulate() call below though...
|
||||
// CGAL::Polygon_mesh_processing::triangulate_faces(*shape_);
|
||||
// CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_);
|
||||
ifcopenshell::geometry::CgalShape::CgalShape(const cgal_point_t& point, bool convex) {
|
||||
shape_ = point;
|
||||
convex_tag_ = convex;
|
||||
}
|
||||
|
||||
ifcopenshell::geometry::CgalShape::CgalShape(const cgal_wire_t& wire, bool convex) {
|
||||
shape_ = wire;
|
||||
convex_tag_ = convex;
|
||||
}
|
||||
|
||||
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
|
||||
void ifcopenshell::geometry::CgalShape::to_poly() const {
|
||||
if (!shape_) {
|
||||
shape_.emplace();
|
||||
|
||||
convert_to_polyhedron(*nef_, *shape_);
|
||||
if (shape_->size_of_vertices() > 0) {
|
||||
cgal_shape_t poly;
|
||||
convert_to_polyhedron(*nef_, poly);
|
||||
if (poly.size_of_vertices() > 0) {
|
||||
// @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_);
|
||||
}
|
||||
@@ -174,18 +342,23 @@ void ifcopenshell::geometry::CgalShape::to_poly() const {
|
||||
|
||||
void ifcopenshell::geometry::CgalShape::to_nef() const {
|
||||
if (!nef_) {
|
||||
auto shp = poly();
|
||||
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");
|
||||
}
|
||||
}
|
||||
nef_ = utils::create_nef_polyhedron(*shape_);
|
||||
nef_ = utils::create_nef_polyhedron(shp);
|
||||
}
|
||||
}
|
||||
#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 {
|
||||
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();
|
||||
|
||||
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) {
|
||||
// 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();
|
||||
} 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();
|
||||
@@ -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 {
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -432,12 +613,26 @@ double ifcopenshell::geometry::CgalShape::bounding_box(void *& b) const {
|
||||
b = new CGAL::Bbox_3;
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -448,10 +643,13 @@ void ifcopenshell::geometry::CgalShape::set_box(void * b) {
|
||||
}
|
||||
|
||||
int ifcopenshell::geometry::CgalShape::surface_genus() const {
|
||||
to_poly();
|
||||
auto nv = shape_->size_of_vertices();
|
||||
auto ne = shape_->size_of_halfedges() / 2;
|
||||
auto nf = shape_->size_of_facets();
|
||||
if (is_point() || is_wire()) {
|
||||
return 0;
|
||||
}
|
||||
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 genus = (2 - euler) / 2;
|
||||
@@ -461,14 +659,22 @@ int ifcopenshell::geometry::CgalShape::surface_genus() const {
|
||||
|
||||
bool ifcopenshell::geometry::CgalShape::is_manifold() const {
|
||||
// @todo ?
|
||||
to_poly();
|
||||
return shape_->is_valid();
|
||||
return (is_point() || is_wire()) ? true : poly().is_valid();
|
||||
}
|
||||
|
||||
int ifcopenshell::geometry::CgalShape::num_edges() const
|
||||
{
|
||||
to_poly();
|
||||
return (int) shape_->size_of_halfedges() / 2;
|
||||
if (is_point()) {
|
||||
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
|
||||
@@ -479,7 +685,13 @@ int ifcopenshell::geometry::CgalShape::num_faces() const
|
||||
} else
|
||||
#endif
|
||||
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 {
|
||||
return 0;
|
||||
}
|
||||
@@ -487,46 +699,63 @@ int ifcopenshell::geometry::CgalShape::num_faces() const
|
||||
|
||||
OpaqueNumber* ifcopenshell::geometry::CgalShape::CgalShape::length()
|
||||
{
|
||||
to_poly();
|
||||
Kernel_::FT len = 0;
|
||||
for (auto it = shape_->edges_begin(); it != shape_->edges_end(); ++it) {
|
||||
len += CGAL::approximate_sqrt(CGAL::Segment_3<Kernel_>(
|
||||
it->vertex()->point(),
|
||||
it->next()->vertex()->point()
|
||||
).squared_length());
|
||||
if (is_wire()) {
|
||||
len = wire_length(wire());
|
||||
} else if (!is_point()) {
|
||||
const auto& shp = poly();
|
||||
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);
|
||||
}
|
||||
|
||||
OpaqueNumber* ifcopenshell::geometry::CgalShape::area()
|
||||
{
|
||||
to_poly();
|
||||
auto s = *shape_;
|
||||
if (is_wire()) {
|
||||
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);
|
||||
return new NumberType(CGAL::Polygon_mesh_processing::area(s));
|
||||
}
|
||||
|
||||
OpaqueNumber* ifcopenshell::geometry::CgalShape::volume()
|
||||
{
|
||||
to_poly();
|
||||
auto s = *shape_;
|
||||
if (is_point() || is_wire()) {
|
||||
return new NumberType(Kernel_::FT(0));
|
||||
}
|
||||
auto s = poly();
|
||||
CGAL::Polygon_mesh_processing::triangulate_faces(s);
|
||||
return new NumberType(CGAL::Polygon_mesh_processing::volume(s));
|
||||
}
|
||||
|
||||
OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::position()
|
||||
{
|
||||
to_poly();
|
||||
if (shape_->size_of_facets() == 1) {
|
||||
if (is_point()) {
|
||||
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;
|
||||
// CGAL::Vector_3<Kernel_> p;
|
||||
std::array<Kernel_::FT, 3> p;
|
||||
for (auto it = shape_->points_begin(); it != shape_->points_end(); ++it) {
|
||||
std::array<Kernel_::FT, 3> p{ Kernel_::FT(0), Kernel_::FT(0), Kernel_::FT(0) };
|
||||
for (auto it = shp.points_begin(); it != shp.points_end(); ++it) {
|
||||
for (int i = 0; i < 3; ++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) {
|
||||
p[i] /= N;
|
||||
}
|
||||
@@ -542,19 +771,19 @@ OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::position()
|
||||
|
||||
OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::axis()
|
||||
{
|
||||
to_poly();
|
||||
if (shape_->size_of_facets() == 1) {
|
||||
auto pl = Plane_equation()(*shape_->facets_begin());
|
||||
std::array<typename Kernel_::FT, 3> abc{ pl.a(), pl.b(), pl.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;
|
||||
|
||||
return OpaqueCoordinate<3>(
|
||||
new NumberType(pl.a() / maxval),
|
||||
new NumberType(pl.b() / maxval),
|
||||
new NumberType(pl.c() / maxval)
|
||||
);
|
||||
if (is_wire()) {
|
||||
if (wire().size() == 2) {
|
||||
return opaque_axis(wire()[1] - wire()[0]);
|
||||
}
|
||||
if (wire().size() >= 3) {
|
||||
return opaque_axis(wire_normal(wire()));
|
||||
}
|
||||
throw std::runtime_error("Invalid shape type");
|
||||
}
|
||||
auto shp = poly();
|
||||
if (shp.size_of_facets() == 1) {
|
||||
auto pl = Plane_equation()(*shp.facets_begin());
|
||||
return opaque_axis(cgal_vector_t(pl.a(), pl.b(), pl.c()));
|
||||
} else {
|
||||
throw std::runtime_error("Invalid shape type");
|
||||
}
|
||||
@@ -562,6 +791,14 @@ OpaqueCoordinate<3> ifcopenshell::geometry::CgalShape::axis()
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -612,75 +849,71 @@ ConversionResultShape * ifcopenshell::geometry::CgalShape::box()
|
||||
|
||||
ConversionResultShape* ifcopenshell::geometry::CgalShape::wrap_in_compound()
|
||||
{
|
||||
return new CgalShape(poly(), convex_tag_);
|
||||
return clone();
|
||||
}
|
||||
|
||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::vertices()
|
||||
{
|
||||
// @todo this is ridiculous
|
||||
to_poly();
|
||||
std::vector<ConversionResultShape*> result;
|
||||
for (auto& p : shape_->points()) {
|
||||
std::vector<cgal_point_t> ps = {
|
||||
p, p, p
|
||||
};
|
||||
|
||||
std::vector<std::vector<size_t>> ids(1);
|
||||
ids.front().push_back(0);
|
||||
ids.front().push_back(1);
|
||||
ids.front().push_back(2);
|
||||
|
||||
cgal_shape_t poly;
|
||||
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(ps, ids, poly);
|
||||
|
||||
result.push_back(new CgalShape(poly));
|
||||
if (is_point()) {
|
||||
result.push_back(new CgalShape(point()));
|
||||
return result;
|
||||
}
|
||||
if (is_wire()) {
|
||||
for (const auto& p : wire()) {
|
||||
result.push_back(new CgalShape(p));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
for (const auto& p : poly().points()) {
|
||||
result.push_back(new CgalShape(p));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::edges()
|
||||
{
|
||||
// @todo this is ridiculous
|
||||
to_poly();
|
||||
std::vector<ConversionResultShape*> result;
|
||||
for (auto& ed : shape_->edges()) {
|
||||
std::vector<cgal_point_t> ps = {
|
||||
ed.vertex()->point(),
|
||||
ed.vertex()->point(),
|
||||
ed.next()->vertex()->point()
|
||||
};
|
||||
|
||||
std::vector<std::vector<size_t>> ids(1);
|
||||
ids.front().push_back(0);
|
||||
ids.front().push_back(1);
|
||||
ids.front().push_back(2);
|
||||
|
||||
cgal_shape_t poly;
|
||||
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(ps, ids, poly);
|
||||
|
||||
result.push_back(new CgalShape(poly));
|
||||
if (is_point()) {
|
||||
return result;
|
||||
}
|
||||
if (is_wire()) {
|
||||
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] }));
|
||||
}
|
||||
if (w.size() > 2) {
|
||||
result.push_back(new CgalShape(cgal_wire_t{ w.back(), w.front() }));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
for (auto ed : poly().edges()) {
|
||||
result.push_back(new CgalShape(cgal_wire_t{ ed.vertex()->point(), ed.opposite()->vertex()->point() }));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<ConversionResultShape*> ifcopenshell::geometry::CgalShape::facets()
|
||||
{
|
||||
to_poly();
|
||||
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<std::vector<size_t>> ids(1);
|
||||
|
||||
auto it = face->facet_begin();
|
||||
do {
|
||||
ps.push_back(it->vertex()->point());
|
||||
ids.front().push_back(ids.front().size());
|
||||
} while (++it != face->facet_begin());
|
||||
|
||||
cgal_shape_t poly;
|
||||
CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(ps, ids, poly);
|
||||
|
||||
result.push_back(new CgalShape(poly));
|
||||
result.push_back(new CgalShape(ps));
|
||||
}
|
||||
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
|
||||
{
|
||||
cgal_shape_t s = *this;
|
||||
if (place->is_identity()) {
|
||||
return clone();
|
||||
}
|
||||
|
||||
if (!place->is_identity()) {
|
||||
const auto& m = place->ccomponents();
|
||||
const auto trsf = make_transform(*place);
|
||||
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
|
||||
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);
|
||||
}
|
||||
cgal_shape_t s = poly();
|
||||
for (auto &vertex : s.vertex_handles()) {
|
||||
vertex->point() = vertex->point().transform(trsf);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -957,27 +1190,16 @@ ConversionResultShape* ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::
|
||||
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;
|
||||
mp.insert({
|
||||
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);
|
||||
insert_normalized_plane_map(mp, from, to);
|
||||
std::size_t mutated = 0;
|
||||
auto nw = shape_->map(mp, mutated);
|
||||
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;
|
||||
if (froms.size() != tos.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) {
|
||||
auto& from = *it;
|
||||
auto& to = *jt;
|
||||
mp.insert({
|
||||
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()
|
||||
)
|
||||
});
|
||||
insert_normalized_plane_map(mp, from, to);
|
||||
}
|
||||
auto nw = shape_->map(mp);
|
||||
std::size_t mutated = 0;
|
||||
auto nw = shape_->map(mp, mutated);
|
||||
shape_ = std::move(nw);
|
||||
return mutated;
|
||||
}
|
||||
|
||||
|
||||
@@ -1012,4 +1223,4 @@ ConversionResultShape* ifcopenshell::geometry::CgalShapeHalfSpaceDecomposition::
|
||||
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/self_intersections.h>
|
||||
|
||||
#include <variant>
|
||||
|
||||
#ifdef IFOPSH_SIMPLE_KERNEL
|
||||
|
||||
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
|
||||
@@ -179,13 +181,17 @@ namespace ifcopenshell { namespace geometry {
|
||||
|
||||
class IFC_GEOMLIBRARY_API CgalShape : public IfcGeom::ConversionResultShape {
|
||||
private:
|
||||
typedef std::variant<cgal_shape_t, cgal_point_t, cgal_wire_t> cgal_shape_storage_t;
|
||||
|
||||
bool convex_tag_ = false;
|
||||
mutable boost::optional<cgal_shape_t> shape_;
|
||||
mutable boost::optional<cgal_shape_storage_t> shape_;
|
||||
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||
mutable boost::optional<CGAL::Nef_polyhedron_3<Kernel_>> nef_;
|
||||
#endif
|
||||
public:
|
||||
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
|
||||
CgalShape(const CGAL::Nef_polyhedron_3<Kernel_>& shape, bool convex = false) {
|
||||
@@ -206,14 +212,29 @@ namespace ifcopenshell { namespace geometry {
|
||||
void to_poly() const {}
|
||||
#endif
|
||||
|
||||
operator const cgal_shape_t& () const { to_poly(); return *shape_; }
|
||||
const cgal_shape_t& poly() const { to_poly(); return *shape_; }
|
||||
operator const cgal_shape_t& () const { return poly(); }
|
||||
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 Serialize(const ifcopenshell::geometry::taxonomy::matrix4& place, std::string&) 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;
|
||||
@@ -255,8 +276,8 @@ namespace ifcopenshell { namespace geometry {
|
||||
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
||||
virtual ConversionResultShape* concat(ConversionResultShape*);
|
||||
|
||||
virtual void 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(OpaqueCoordinate<4>& from, 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 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;
|
||||
}
|
||||
|
||||
virtual void 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(OpaqueCoordinate<4>& from, 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 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/graph/copy.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <list>
|
||||
#include <queue>
|
||||
#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::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)
|
||||
struct Point_d_4d_Less {
|
||||
using Point_d = CGAL::Epick_d<CGAL::Dimension_tag<4>>::Point_d;
|
||||
@@ -264,7 +282,11 @@ class halfspace_tree {
|
||||
public:
|
||||
virtual CGAL::Nef_polyhedron_3<Kernel> evaluate() 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 tree_type kind() const = 0;
|
||||
virtual void merge(CGAL::Nef_polyhedron_3<Kernel>&) const = 0;
|
||||
@@ -366,10 +388,10 @@ public:
|
||||
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;
|
||||
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)));
|
||||
}
|
||||
@@ -485,21 +507,12 @@ public:
|
||||
virtual void accumulate(std::list<typename Kernel::Plane_3>& points) const {
|
||||
points.push_back(plane_);
|
||||
}
|
||||
virtual std::unique_ptr<halfspace_tree<Kernel>> map(const plane_map<Kernel>& m) const {
|
||||
|
||||
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
|
||||
);
|
||||
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_);
|
||||
|
||||
auto it = m.find(pp);
|
||||
if (it != m.end()) {
|
||||
++mutated;
|
||||
return std::unique_ptr<halfspace_tree<Kernel>>(new halfspace_tree_plane(it->second));
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -696,10 +696,10 @@ bool ifcopenshell::geometry::OpenCascadeShape::surface_area_along_direction(doub
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@ namespace ifcopenshell {
|
||||
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
||||
virtual ConversionResultShape* concat(ConversionResultShape*);
|
||||
|
||||
virtual void 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(OpaqueCoordinate<4>& from, 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 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user