Compare commits

..

39 Commits

Author SHA1 Message Date
CyrilWaechter 0e084f2830 Port space/boundary code to v0.9.0 API changes
v0.9.0 changed two APIs used by the space generation feature:

1. geom.tree.select_ray no longer accepts length as a keyword argument;
   pass it positionally.

2. ifcopenshell.file no longer exposes wrapped_data; serialize with
   file.to_string() directly.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter dd5bd58916 Add footprint corner sampling for roof detection
get_vertical_bounding_planes now also casts rays from the footprint
polygon's corner vertices, offset slightly inward toward the centroid.
This catches bounding elements (e.g. sloped roofs) that only cover a
corner of the space.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter c3abe0b3c7 Fix space regeneration determinism and caching bugs
Fix three issues in generate_space:

1. Z location drift: z was derived from the Blender bounding box, which
   changes after every regeneration. Use active_obj.location.z instead.

2. Cache invalidation for moved roofs/slabs: commit placements for
   HEIGHT_DETECTION_CLASSES in addition to BOUNDING_CLASSES so the
   geometry cache reflects recent moves.

3. Non-deterministic regeneration: the old Body representation was still
   present in the IFC file when get_space_volume_strategy built the
   geometry tree, so ray hits from get_vertical_bounding_planes hit the
   space's own body. Since each regeneration produced a different Body
   (BooleanClippingResult/FacetedBrep), the strategy alternated between
   EXTRUDE_CLIP and BREP. Remove all Body representations before
   strategy detection so the tree only contains bounding elements.

Also clean up stale IfcRelSpaceBoundary relationships before each
regeneration to prevent old boundary references from contaminating
subsequent runs. Remove ALL existing Body representations (not just
the first one found) to prevent duplicate half-space clipping chains.

Add regression tests including a 5-iteration stability check.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter 7017d5400d Fix rotated space placement localization during regeneration
set_space_representation_from_polygon was localising the footprint and
clipping planes by subtracting only the object origin. For spaces with a
rotated ObjectPlacement (e.g. Space 5710 in the test IFC) the footprint
was not rotated into the space's local coordinate system, so the
regenerated mesh was rotated by the placement angle and appeared at the
wrong world location.

Now the polygon and planes are transformed with the full inverse of the
object's placement matrix, and the plane normals are also rotated. The
local mesh is therefore aligned with the object's local axes and appears
in the correct world position when the placement is applied.

Added regression tests for Space 5710 (rotated placement) and Space 2363
(identity placement) using the real HouseWithGarage_AC22_IFC2X3.ifc
fixture.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter fb3cd09d6d Fix space generation location and IFC2X3 B-rep
Two fixes for space generation:

1. IFC2X3 schema: build_brep_space now falls back to plain
   IfcRelSpaceBoundary because IfcRelSpaceBoundary1stLevel does not exist in
   IFC2X3.

2. Geometry location: set_space_representation_from_polygon now aligns the
   IFC ObjectPlacement with the Blender object, converts base_z/planes and
   the footprint polygon to the object's local coordinate system before
   building, and fixes the base_z unit scale. The centred-cube regeneration
   test was updated to check world bounds because the mesh is now placed
   relative to the object placement.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter 332435416a Fix clipped space top reaching sloped planes
The extrusion height was capped at the top/bottom plane anchor z (the mean
of the ray-cast hits, near the footprint centre), so a sloped ceiling's
high side stopped short of the plane (e.g. 5.5 m instead of 6.88 m for the
shed roof test). Extend the extrusion to the plane's z at every footprint
vertex before clipping, falling back to the base z as before. Also correct
the mirrored profile-to-world mapping comment in the shed roof test helper
(the ridge is at world y=-5, not y=+5).

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter f0c6de4bdf Add regression tests for sloped slab, curved wall
Cover a curved vertical wall (EXTRUDE_CLIP strategy) in test_space.py and a
sloped slab (clipped extrusion) in test_spatial.py.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter 665c5fa77e Pass bounding walls into space representation
Thread the footprint-query bounding walls and container into the
set_space_representation_from_polygon dispatcher and add an end-to-end
test for a space under a shed roof with a sloped underside.

Generated with the assistance of an AI coding tool.
2026-08-18 00:11:00 +02:00
CyrilWaechter de13379162 Wire space volume strategy detection
Dispatch on EXTRUDE_CLIP vs B-rep when building space volumes, and
fix fixture placement and visibility bugs in the spatial tests.

Generated with the assistance of an AI coding tool.
2026-08-18 00:09:56 +02:00
CyrilWaechter 3b16356181 Add B-rep fallback space builder
Build a faceted B-rep space from auto-generated boundary faces when
sloped or curved bounding elements make a clipped extrusion
unsuitable.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 44860cd615 Match sloped faces in boundary generation
Allow sloped roof/slab/wall faces to bound space faces when the
strict anti-parallel rule leaves a face uncovered, using a
footprint-scaled distance tolerance. Existing matching behaviour
is preserved (fallback-only).

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 5651cd6494 Add clipped extrusion space builder
Build IfcExtrudedAreaSolid clipped by top/bottom half-space planes.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 29b9d8807e Add space volume strategy detection
Detect whether a space can be built as a clipped extrusion or needs
a B-rep fallback, based on wall face orientation and top/bottom
bounding planes.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 37f557b4b5 Align vertical bounding plane strategy contract with caller
get_vertical_bounding_planes always returns EXTRUDE_CLIP; the
EXTRUDE_CLIP-vs-BREP decision belongs to the calling layer.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 881fb10fe6 Add vertical bounding plane detection for space generation
Implement get_vertical_bounding_planes using ray-casting from the RL
cut elevation with nearest-hit and coplanar grouping.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 1949adda44 Expand space regeneration design spec with prior art
Add prior-art references, known limitations, and non-goals
identified during self-review.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 87193ac323 Add space regeneration sloped-roof design spec
Design for extending generate_space with a hybrid parametric
extrusion + clipping / B-rep fallback strategy supporting sloped
roofs, sloped walls, sloped slabs, and curved walls.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter c34a6ddac6 Bonsai: add regression test for closed IfcPolyline loop conversion
Verifies that convert_curve_to_mesh produces the closing edge instead of overwriting the last segment, matching the fix from PR #8043.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
Petru Conduraru 92cc601a85 Bonsai: close IfcPolyline loops by appending the closing edge (#8043)
convert_curve_to_mesh built the edge chain of a polyline with extend, then for a
closed polyline overwrote the last edge with the closing edge instead of
appending it. That discarded the final real segment, so every closed IfcPolyline
loop came back one edge short and open. On the edit mode round trip the inner
void loop of an IfcArbitraryProfileDefWithVoids was then lost or misclassified,
and the profile was rewritten without its void, collapsing the extrusion to a
bounding box.

Append the closing edge instead, matching the IfcIndexedPolyCurve branch. Live
tested: the Tab round trip now keeps both loops closed and re-exports the
IfcArbitraryProfileDefWithVoids with its inner void intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 00:06:27 +02:00
CyrilWaechter adf01be1d0 ifcopenshell.util.boundary: make face matrix robust against collinear first vertices
_face_matrix_from_verts used only the first 3 vertices and sb.np_normal, which divides by zero when they are collinear. Triangulated meshes from generated spaces often start with collinear boundary vertices, producing NaN matrices and a shapely LinearRing error. Walk the polygon to find a non-degenerate normal and edge.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter c8f8196b09 Bonsai: commit moved bounding objects before IFC-based space generation
The auto-generate-ifc-based-space-boundaries path builds a geometry cache from the IFC file. If a user (or a BDD helper) only moves the Blender object matrix, the cache still sees the old IFC placement and the space footprint is open. Commit any moved visible bounding objects and clear the cache before generating the space.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter cb22dd7a43 Use space's own container for regeneration instead of requiring default
When regenerating an existing IfcSpace, the default container is no longer required. Instead, the space's container is found via get_parent(element), which walks the full spatial hierarchy (aggregation, containment, nesting). For new space creation, the default container is still required.

Add optional container parameter to get_space_polygon_from_context_visible_objects so regeneration can pass the resolved container directly.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 243f13de09 Optimize coplanar face reconstruction and add tests
Vectorize the coplanarity prefilter in _union_coplanar_face_polygon and
compute per-triangle normals once instead of per space face. Add
regression tests for the SmallHouse and Triangle boundary test models.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 0d5c9a0ce2 Keep shaft holes in generated space boundaries
dissolve_faces with merge_coplanar drops interior rings, so the shaft
opening in a ceiling was lost and replaced by spurious wall-cap
boundaries. Reconstruct the space face from its raw coplanar triangles,
preserve interior rings in the assigned boundary, absorb redundant
candidates by plane offset, and raise the full-face tolerance so walls
offset by their half thickness get a single boundary.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter ea6f03409f Fix space boundary generation regressions
When several elements match the same space face, offset matches that only
duplicate coplanar coverage are now skipped, and a single bounding element
within a small plane offset gets the full space face instead of a clipped
polygon. Existing boundaries are removed before regeneration so stale 2nd
level boundaries are not left behind, and the Bonsai operator delegates
element filtering to auto_generate_boundaries.

Regenerates SmallHouse boundaries to match the reference output and keeps
the ExternalEarth opening unioning intact.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 21b4cd2403 Deduplicate opening boundaries in auto_generate_boundaries
When a building element has multiple ngons matching the same space face,
_process_openings was called multiple times for the same opening/filling,
producing duplicate boundaries (e.g. two boundaries for the same door).

Fix: pass a set of processed filling IDs to _process_openings and skip
already-processed openings.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter a543022bba Fix dissolve_faces polygon reconstruction with merge_coplanar
When merge_coplanar merges two sub-faces that share an edge from the
original BRep (e.g. two rectangles forming an L-shape cap), that shared
edge remained in boundary_edges via original_edges filtering, causing
the edge_adjacency walk to produce wrong polygons.

Fix: after coplanar merging, use edge frequency (edges used by exactly
1 triangle = boundary) instead of original_edges filtering, which
correctly identifies only outer boundary edges.

Also add safety checks: edge_adjacency emptiness guard, infinite loop
protection, and minimum polygon length check.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 6f9d5c4005 Fix axis/ref_direction swap in connection geometry
The a2p placement matrix stores col[0]=X (edge direction) and
col[2]=Z (face normal), but assign_connection_geometry expects
axis=Z (normal) and ref_direction=X (edge).

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter a7b6c66f77 Extract boundary generation to ifcopenshell.util.boundary
Move Blender-independent boundary generation algorithm from Bonsai
(GPL) to ifcopenshell.util.boundary (LGPL):

- ifcopenshell.util.shape.dissolve_faces: reconstruct polygonal faces
  from triangulated mesh using original edges from get_edges() + Union-Find
- ifcopenshell.util.boundary.auto_generate_boundaries: full boundary
  generation algorithm using IFC geometry (numpy, shapely) without
  Blender — replaces bmesh, matrix_world, tool.Cad.is_x, mathutils with
  numpy equivalents
- Uses existing ifcopenshell.api.boundary.assign_connection_geometry
  for connection geometry creation
- Uses existing ifcopenshell.util.placement.a2p + np_normal for face
  matrix construction
- BOUNDARY_ELEMENT_CLASSES expanded to include IfcColumn and
  IfcCurtainWall

Bonsai's boundary/operator.py auto_generate_boundaries is now a thin
adapter handling Blender-specific preprocessing (flushing moved
objects, building iterator + tree) then delegating to the util module.

Added 12 tests: 3 for dissolve_faces, 3 for auto_generate_boundaries.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 6dc671f24d Fix shapely topology crash in boundary generation
Add buffer(0) validation for space_face_polygon and face_polygon
before intersection, following the same pattern as tool/cad.py.

Wrap the intersection in try/except for shapely.errors.GEOSException
to catch remaining topology errors. On exception, set
bonsai.last_error (so the 'Copy Error Message To Clipboard' button
appears in the UI), report an ERROR to the operator, and continue
processing other face pairs instead of crashing.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 9b28444255 Fix space regen Z placement for centered representations
get_x_y_z_h_mat_from_obj computes z from bound_box[0], which differs from obj.location.z when the representation is centered at origin (e.g., a PolygonalFaceSet unit cube). The previous fix (24f7629f) removed translate_obj_to_z_location to prevent doubling Z for extrusion representations, but that broke centered ones where z != location.z.

Replace the removed relative translate with an absolute active_obj.location.z = z. This is a no-op for extrusion representations (z == location.z) and corrects the position for centered ones.

Add test_regenerate_space_from_centered_cube_representation to cover the regeneration path with a centered mesh representation.

Generated with the assistance of an AI coding tool.
2026-08-18 00:06:27 +02:00
CyrilWaechter 7c04a0d533 Fix pyright possibly-missing-submodule in covering test
Add explicit import bonsai.core.tool to satisfy pyright's type checker,
which requires submodules to be explicitly imported rather than relying
on transitive imports from import bonsai.
2026-08-17 23:03:42 +02:00
CyrilWaechter 02126a8d82 Fix ruff import-ordering in covering and spatial tests
ruff check flagged unsorted imports in test/core/test_covering.py
and test/tool/test_spatial.py. Fix by reorganising import blocks.
2026-08-17 23:03:42 +02:00
CyrilWaechter c9fcabef65 Rework TestGenerateSpace to use IFC representations instead of Blender cubes
All 8 space-generation tests now create IFC walls/slabs with real
solid-block representations using IfcExtrudedAreaSolid, instead of
relying on the old Blender-mesh bisection path (broken since 79ee88da5
switched to IFC-geometry-only for boundary detection).

- _BlockHelper provides create_wall (10x10xheight block) and create_slab
  (12x12x1.0 block) helpers using standard IfcOpenShell API calls.
- The wall block bisects to a 10x10 polygon at the cutting plane
  (matching the old cube-behaviour), and auto-height detects wall_top_z.
- Pre-existing height assertions (z=10) now pass correctly because
  auto-height = wall_top_z - base_z = 10 - 0 = 10 (the old values were
  wrong for the Blender path where h defaulted to 3).
- test_regenerate_after_wall_height_change modifies the IFC extrusion
  depth directly and bumps the geom cache token via
  _bump_geom_cache_token() instead of relying on Blender depsgraph.
- No Blender cubes are created except when absolutely needed for
  selection/active-object flow (regeneration, apply-height).
- Added ifcopenshell.util.representation to imports.
- Import _bump_geom_cache_token from bonsai.tool.spatial.

Generated with the assistance of an AI coding tool.
2026-08-17 23:03:42 +02:00
CyrilWaechter 99c89c3f44 Add covering core tests verifying tuple-unpack fix
Covers all three covering operators with success and error-path tests
using the Prophecy mocking framework. The key assertion verifies that
get_space_polygon_from_context_visible_objects' return value is unpacked
so the polygon (not the tuple of polygon+bounding_elements) reaches
set_covering_representation_from_polygon.

Shapely geometry objects are not JSON-serialisable (Prophecy call
serialisation), so we use the plain integer 42 as a stand-in for the
polygon value.

Generated with the assistance of an AI coding tool.
2026-08-17 23:03:42 +02:00
CyrilWaechter b78396051d Fix covering operators to unpack tuple return from get_space_polygon_from_context_visible_objects
Three covering core functions (add_instance_flooring_covering_from_cursor,
add_instance_ceiling_covering_from_cursor, regen_selected_covering_object)
used the old single-value assignment from
get_space_polygon_from_context_visible_objects, which now returns a
(polygon, bounding_walls) tuple. The isinstance(str) guard never fired,
causing the tuple to flow into set_covering_representation_from_polygon
and raise a shapely error.

Fix by unpacking space_polygon, _ at all three call sites.

Generated with the assistance of an AI coding tool.
2026-08-17 23:03:42 +02:00
CyrilWaechter 6715e684a8 Extract space generation algorithms to ifcopenshell.util
Move Blender-independent space generation algorithms from Bonsai
(GPL) to ifcopenshell.util (LGPL):

- ifcopenshell.util.shape.bisect_mesh_plane_vf: vectorized numpy
  triangle/plane intersection for mesh bisection
- ifcopenshell.util.element.iter_top_connections: walker for
  IfcRelConnectsElements(TOP) relationships
- ifcopenshell.util.space: new module with get_boundary_lines,
  get_space_polygon, get_auto_space_height and height detection
  helpers — all operating on IFC geometry without Blender

Bonsai's tool/spatial.py now delegates to these utilities via
thin wrappers, keeping only Blender-specific concerns (cache
management with depsgraph invalidation, UI property reads).

tool/wall.py iter_wall_slab_connections delegates to
ifcopenshell.util.element.iter_top_connections.

Added 22 tests: 6 for bisect_mesh_plane_vf, 10 for space
generation algorithms, 4 for iter_top_connections, 2 Bonsai
integration tests for cache behavior.

Generated with the assistance of an AI coding tool.
2026-08-17 23:03:42 +02:00
CyrilWaechter 1695571256 Add auto-detect space height from elements above
Space height is now auto-detected using IFC geometry directly
(ifcopenshell.geom.create_shape + get_shape_bottom/top_elevation)
instead of Blender object bounding boxes. This fixes height detection
when the slab above is not loaded in Blender.

Detection priority:
1. IfcRelConnectsElements(TOP) connections on bounding walls
2. IfcSlab / IfcRoof elements above with XY overlap to space polygon
3. Minimum wall top Z of bounding walls
4. Fallback to space_height property (default 3m)

Added space_height and force_space_height properties to
BIMSpatialDecompositionProperties. The height field is synced to
the active space's height via active_object_callback (msgbus), not
in draw().

Added ApplySpaceHeightToSelection operator to modify
IfcExtrudedAreaSolid.Depth in place without regenerating footprint.

bounding_walls changed from list[tuple[element, obj]] to
list[entity_instance] since Blender objects are no longer needed.

Generated with the assistance of an AI coding tool.
2026-08-17 23:03:41 +02:00
CyrilWaechter 4f0e572e0f Add copy attribute to selection for boundaries
Add a paste button to IfcRelSpaceBoundary specific attributes
(RelatingSpace, RelatedBuildingElement, ParentBoundary,
CorrespondingBoundary, PhysicalOrVirtualBoundary,
InternalOrExternalBoundary) reusing the existing
copy_attribute_to_selection core function.

The core function value type hint is broadened from Union[str, None]
to Any since boundary relation attributes pass IFC entity instances.

Generated with the assistance of an AI coding tool.
2026-08-17 23:03:41 +02:00
222 changed files with 49064 additions and 45787 deletions
-377
View File
@@ -1,377 +0,0 @@
# /// script
# dependencies = [
# "pytest",
# ]
# ///
"""Check (and by default fix) whitespace issues in tracked source files:
- stray CR, e.g. 'hello\\rworld' -> 'helloworld'
- line ending mismatch, e.g. 'hello\\r\\n' -> 'hello\\n' (or vice versa)
- missing newline at end of file
- extra newline(s) at end of file
- trailing whitespace at end of line
"""
import argparse
import io
import os
import re
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
from typing import BinaryIO, Literal, cast
import pytest
class C:
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RESET = "\033[0m"
CR = b"\r"
CRLF = b"\r\n"
LF = b"\n"
LineSeparator = Literal[b"\r\n", b"\n"]
SYSTEM_LINE_SEPARATOR = cast(LineSeparator, os.linesep.encode())
class Checker:
def __init__(self, newline: LineSeparator = SYSTEM_LINE_SEPARATOR) -> None:
self.newline = newline
self.issues = 0
def report(self, label: str, issue: str) -> None:
self.issues += 1
print(f"{label}: {C.RED}{issue}{C.RESET}")
def check_stray_cr(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_stray_cr(f, str(filepath), check)
def _check_stray_cr(self, f: BinaryIO, label: str, check: bool) -> None:
# a CR is "stray" if it isn't immediately followed by a LF, i.e. not part of a CRLF pair
# CRLF/CR mismatch will be reported separately.
stray_cr = re.compile(rb"\r(?!\n)")
content = f.read()
matches = list(stray_cr.finditer(content))
if not matches:
return
line_numbers = dict.fromkeys(content.count(b"\n", 0, m.start()) + 1 for m in matches)
for line_number in line_numbers:
self.report(f"{label}:{line_number}", "stray carriage return")
if check:
return
f.seek(0)
f.write(stray_cr.sub(b"", content))
f.truncate()
def check_line_endings_mismatch(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_line_endings_mismatch(f, str(filepath), check)
def _check_line_endings_mismatch(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
def get_line_ending(line: bytes) -> LineSeparator | None:
if line.endswith(CRLF):
return CRLF
if line.endswith(LF):
return LF
# last line with no trailing newline at all; check_eof_newline handles that
return None
changed = False
fixed_lines = []
for line_number, line in enumerate(f, start=1):
found = get_line_ending(line)
if found in (NEWLINE, None):
fixed_lines.append(line)
continue
self.report(f"{label}:{line_number}", f"line ending mismatch (expected {NEWLINE!r}, found {found!r})")
changed = True
content = line[: -len(found)]
fixed_lines.append(content + NEWLINE)
if changed and not check:
f.seek(0)
f.write(b"".join(fixed_lines))
f.truncate()
def check_eof_newline(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_eof_newline(f, str(filepath), check)
def _check_eof_newline(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
NEWLINE_SIZE = len(NEWLINE)
size = f.seek(0, os.SEEK_END)
if size == 0:
return
trailing_newlines = 0
while True:
pos = f.seek((-trailing_newlines - 1) * NEWLINE_SIZE, os.SEEK_END)
if f.read(NEWLINE_SIZE) != NEWLINE:
break
trailing_newlines += 1
if pos == 0:
break
if trailing_newlines == 0:
self.report(label, "missing newline at end of file")
if check:
return
f.seek(0, os.SEEK_END)
f.write(NEWLINE)
elif trailing_newlines > 1:
self.report(label, f"{trailing_newlines} trailing newlines at end of file")
if check:
return
f.truncate(size - (trailing_newlines - 1) * NEWLINE_SIZE)
def check_trailing_whitespaces(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_trailing_whitespaces(f, str(filepath), check)
def _check_trailing_whitespaces(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
NEWLINE_SIZE = len(NEWLINE)
changed = False
fixed_lines = []
for line_number, line in enumerate(f, start=1):
has_newline = line.endswith(NEWLINE)
content = line[:-NEWLINE_SIZE] if has_newline else line
stripped = content.rstrip()
if stripped != content:
self.report(f"{label}:{line_number}", "trailing whitespace")
changed = True
fixed_lines.append(stripped + (NEWLINE if has_newline else b""))
if changed and not check:
f.seek(0)
f.write(b"".join(fixed_lines))
f.truncate()
CheckMethod = Callable[[Checker, BinaryIO, str, bool], None]
class TestChecker:
def _assert_check(
self,
method: CheckMethod,
content: bytes,
expected_issues: int,
fixed: bytes,
check: bool,
line_ending: LineSeparator,
*,
transform: bool = True,
) -> None:
checker = Checker(line_ending)
if line_ending == CRLF and transform:
content = content.replace(LF, CRLF)
fixed = fixed.replace(LF, CRLF)
buffer = io.BytesIO(content)
method(checker, buffer, "test", check)
assert buffer.getvalue() == (content if check else fixed)
assert checker.issues == expected_issues
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
(b"line1\r\nline2\n", 0, b"line1\r\nline2\n"),
# ERR
(b"hello\rworld\n", 1, b"helloworld\n"),
(b"a\rb\rc\n", 1, b"abc\n"),
(b"hello\r", 1, b"hello"),
),
)
@pytest.mark.parametrize("check", [False, True])
def test_check_stray_cr(self, content: bytes, expected_issues: int, fixed: bytes, check: bool) -> None:
# Don't parametrize by line endings, since in this case it doesn't matter.
self._assert_check(Checker._check_stray_cr, content, expected_issues, fixed, check, LF)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed", "line_ending"),
(
# OK
(b"", 0, b"", LF),
(b"hello\n", 0, b"hello\n", LF),
(b"hello\r\n", 0, b"hello\r\n", CRLF),
# ERR
(b"hello\r\n", 1, b"hello\n", LF),
(b"a\nb\r\nc\n", 1, b"a\nb\nc\n", LF),
(b"a\r\nb\r\n", 2, b"a\nb\n", LF),
(b"hello\n", 1, b"hello\r\n", CRLF),
(b"a\r\nb\nc\r\n", 1, b"a\r\nb\r\nc\r\n", CRLF),
),
)
@pytest.mark.parametrize("check", [False, True])
def test_check_line_endings_mismatch(
self, content: bytes, expected_issues: int, fixed: bytes, line_ending: LineSeparator, check: bool
) -> None:
self._assert_check(
Checker._check_line_endings_mismatch, content, expected_issues, fixed, check, line_ending, transform=False
)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
# ERR
(b"hello", 1, b"hello\n"),
(b"hello\n\n\n", 1, b"hello\n"),
(b"\n\n\n", 1, b"\n"),
),
)
@pytest.mark.parametrize("check", [False, True])
@pytest.mark.parametrize("line_ending", [LF, CRLF])
def test_check_eof_newline(
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
) -> None:
self._assert_check(Checker._check_eof_newline, content, expected_issues, fixed, check, line_ending)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
(b"hello", 0, b"hello"),
# ERR
(b" ", 1, b""),
(b"hello ", 1, b"hello"),
),
)
@pytest.mark.parametrize("check", [False, True])
@pytest.mark.parametrize("line_ending", [LF, CRLF])
def test_check_trailing_whitespaces(
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
) -> None:
self._assert_check(Checker._check_trailing_whitespaces, content, expected_issues, fixed, check, line_ending)
@staticmethod
def run_tests(extra_args: list[str] | None = None) -> None:
pytest.main([__file__, *(extra_args or [])])
def existing_path(value: str) -> Path:
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(f"path not found: {value}")
return path
# Python files are covered by `black`.
PATTERNS = (
"*.cpp",
"*.h",
"*.i",
)
REPO_ROOT = Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
# Generated files; formatted by the express codegen, not by this script.
IGNORED_DIRS = (REPO_ROOT / "src/ifcparse/schemas",)
def get_tracked_files(root: Path | None = None) -> list[Path]:
output = subprocess.check_output(
["git", "ls-files", "--others", "--cached", "--exclude-standard", *PATTERNS],
cwd=root,
text=True,
)
base = root if root is not None else Path()
filepaths = []
for line in output.splitlines():
filepath = base / line
if not any(filepath.resolve().is_relative_to(d) for d in IGNORED_DIRS):
filepaths.append(filepath)
return filepaths
def main() -> int:
# anything after "--" is forwarded to pytest, e.g. `--test -- --capture=no`
argv = sys.argv[1:]
if "--" in argv:
split = argv.index("--")
argv, extra_args = argv[:split], argv[split + 1 :]
else:
extra_args = []
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=__doc__,
)
parser.add_argument("paths", type=existing_path, nargs="*", help="files or directories to check")
parser.add_argument(
"--check",
action="store_true",
help="only check for whitespace issues without applying fixes",
)
parser.add_argument(
"--test",
action="store_true",
help="run self-tests",
)
parser.add_argument(
"--verbose",
action="store_true",
help="print each checked path",
)
args = parser.parse_args(argv)
if args.test:
TestChecker.run_tests(extra_args)
return 0
if args.paths:
filepaths: list[Path] = []
for path in args.paths:
filepaths.extend(get_tracked_files(path) if path.is_dir() else [path])
else:
filepaths = get_tracked_files()
# dict.fromkeys() dedupes while preserving order, unlike set().
filepaths = list(dict.fromkeys(filepaths))
checker = Checker()
for filepath in filepaths:
if args.verbose:
print(f"checking {filepath}")
checker.check_stray_cr(filepath, args.check)
checker.check_line_endings_mismatch(filepath, args.check)
checker.check_eof_newline(filepath, args.check)
checker.check_trailing_whitespaces(filepath, args.check)
print(f"{len(filepaths)} file(s) checked.")
if not checker.issues:
color = C.GREEN
elif args.check:
color = C.RED
else:
color = C.YELLOW
outcome = "found" if args.check else "found and fixed"
print(f"{color}{checker.issues} issue(s) {outcome}.{C.RESET}")
return 1 if args.check and checker.issues else 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -64,7 +64,7 @@ jobs:
max-size: 5000MB
- name: Set up Python for connector build
uses: actions/setup-python@v7
uses: actions/setup-python@v6
with:
python-version: '3.12'
+3
View File
@@ -17,3 +17,6 @@
[submodule "src/svgfill/3rdparty/svgpp"]
path = src/svgfill/3rdparty/svgpp
url = https://github.com/svgpp/svgpp
[submodule "src/ifcopenshell-python/test/IfcRelSpaceBoundary_TestFiles"]
path = src/ifcopenshell-python/test/IfcRelSpaceBoundary_TestFiles
url = https://github.com/CyrilWaechter/IfcRelSpaceBoundary_TestFiles
@@ -0,0 +1,168 @@
# Design Spec: Space Regeneration with Sloped Roofs, Walls, and Slabs
## Goal
Extend `bonsai.core.spatial.generate_space` so it produces correct `IfcSpace`
geometry for non-rectilinear envelopes:
- sloped roofs,
- sloped slabs,
- sloped walls,
- curved walls.
The existing footprint-based `IfcExtrudedAreaSolid` path is preserved for
ordinary vertical extrusions. A new hybrid path keeps the representation
parametric when possible and falls back to an `IfcFacetedBrep` only when the
boundary cannot be expressed as a clipped extrusion.
## Architecture
```
┌─────────────────────────────────────────┐
│ Existing footprint generation │
│ (get_space_polygon_from_*_objects) │
└──────────────┬────────────────────────────┘
v
┌─────────────────────────────────────────┐
│ Detect extrudability and bounding planes │
│ (pure-Python util, Blender-independent) │
└──────────────┬────────────────────────────┘
v
┌──────┴──────┐
v v
┌───────────────────┐ ┌───────────────────┐
│ Extrusion + clips │ │ B-rep fallback │
│ IfcExtrudedAreaSolid│ │ IfcFacetedBrep │
│ + IfcBooleanClippingResult│ │ (or IfcPolygonalFaceSet) │
└───────────────────┘ └───────────────────┘
```
## Prior art
- **CBIP** (Lilis et al.): constructive solid geometry approach that builds
space volumes as half-space intersections of bounding planes — the basis for
the parametric clipping path.
- **Fichter et al. 2021**: ray-tracing method for automatic boundary
generation; motivates the use of `geom.tree.select_ray` for top/bottom plane
detection.
- **Lilis et al. 2021**: semi-automatic boundary recognition; informs the
fallback to existing `boundary.auto_generate_boundaries` machinery.
- **Ying & Lee 2019**: faceting of curved walls; motivates the B-rep fallback
for curved-in-plan walls that cannot be represented as vertical extruded
profiles.
## Detection criteria
Use the parametric `IfcExtrudedAreaSolid` + `IfcBooleanClippingResult` path
when **all** are true:
1. Side walls are vertical extrusions (face normal is horizontal).
Curved-in-plan walls are allowed; their footprint is polygonized or
reconstructed as a curved profile.
2. The roof/top boundary is piecewise-planar.
3. The bottom slab/floor boundary is piecewise-planar.
4. The footprint is a single closed outer region, possibly with inner closed
regions for holes.
5. The resulting half-space intersection is non-empty and produces a single
solid.
Otherwise use the B-rep fallback.
## Parametric extrusion + clipping algorithm
1. **Build the profile**
- Outer ring from the footprint polygon → `IfcArbitraryClosedProfileDef`.
- Inner rings (holes, e.g., around columns) →
`IfcArbitraryProfileDefWithVoids`.
2. **Extrude**
- Create `IfcExtrudedAreaSolid` along local +Z, with a height large enough
to cover all bounding planes.
3. **Find top planes**
- Cast vertical rays upward from the footprint centroid and sample points
using `ifcopenshell.geom.tree.select_ray`.
- Check each hit face for planarity with
`ifcopenshell.util.shape.dissolve_faces(..., merge_coplanar=True)`.
- Group coplanar hits into distinct planes.
4. **Find bottom planes**
- Same as top, but downward.
5. **Clip**
- For each top plane: create `IfcHalfSpaceSolid` with normal pointing
upward (removed side), apply via `ifcopenshell.api.geometry.clip_solid`.
- For each bottom plane: create `IfcHalfSpaceSolid` with normal pointing
downward, apply via `clip_solid`.
6. **Output**
- `IfcExtrudedAreaSolid` wrapped in a chain of `IfcBooleanClippingResult`.
## B-rep fallback algorithm
For non-extrudable cases (sloped walls, curved roofs, etc.):
1. **Seed space**
- Create a temporary rough mesh (e.g., extruded footprint bounding box) as
a placeholder.
2. **Extract boundary faces**
- Run `ifcopenshell.util.boundary.auto_generate_boundaries` against the
seed to identify the faces of bounding elements that touch the space.
- Convert each boundary polygon from face-local back to 3D world
coordinates.
3. **Build closed shell**
- Collect the 3D boundary faces.
- Add narrow gap-closing faces if `auto_generate_boundaries` leaves
unmatched edges.
- Triangulate and produce `IfcClosedShell``IfcFacetedBrep` (or
`IfcPolygonalFaceSet` for IFC4+).
4. **Clean up**
- Assign the B-rep to the `IfcSpace` and remove the temporary seed
geometry.
## Files to touch
- `src/ifcopenshell-python/ifcopenshell/util/space.py`
- New: `detect_space_volume_strategy`
- New: `build_extruded_clipped_space`
- New: `build_brep_space`
- New helpers for ray-cast plane detection and face planarity checks.
- `src/bonsai/bonsai/tool/spatial.py`
- Extend `set_space_representation_from_polygon` to dispatch to the new
strategy.
- Extend footprint/profile creation to support inner rings for holes.
- `src/bonsai/bonsai/core/spatial.py`
- `generate_space` calls the dispatcher.
## Testing
- Add unit tests in `src/ifcopenshell-python/test/util/test_space.py` for pure
geometry helpers:
- simple shed roof,
- gable roof,
- sloped slab,
- L-shaped footprint with sloped roof,
- curved wall.
- Add Bonsai tests in `src/bonsai/test/tool/test_spatial.py` for end-to-end
`generate_space` with non-rectilinear geometry.
## Error handling
- If detection fails or half-space clipping produces an invalid result, fall
back to the B-rep path.
- If the B-rep path also fails, return an error string and leave the existing
space representation unchanged.
## Known limitations and non-goals
- **Curved (single/double-curvature) roofs and domes** are handled only via the
B-rep fallback; they are not expressible as `IfcExtrudedAreaSolid` +
`IfcBooleanClippingResult` in this design.
- The B-rep fallback produces **non-parametric** geometry: the resulting
`IfcFacetedBrep`/`IfcPolygonalFaceSet` cannot be re-edited parametrically by
the user afterwards. This is an accepted trade-off; the parametric path is
preferred whenever detection succeeds.
- The B-rep fallback depends on `boundary.auto_generate_boundaries`, so it
inherits its assumptions: bounding elements must be related to the space and
the seed volume must intersect them. Gap-closing faces may produce
non-manifold output for degenerate envelopes; we accept this for
non-extrudable edge cases.
- The parametric path requires a single closed outer footprint with optional
inner holes. Multi-region disconnected footprints are not supported and fall
back to B-rep.
-1
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
"""Intended to be run after nix/build-all.py has finished the wasm build."""
import shutil
import subprocess
from pathlib import Path
+5 -13
View File
@@ -6,6 +6,11 @@ version = "0.0.0"
[tool.black]
line-length = 120
include = '''
src/.*.pyi?$
|nix/.*.pyi?$
|pyodide/.*.pyi?$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/rules/*
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
@@ -14,15 +19,6 @@ extend-exclude = '''
|src/ifc2ca/templates/*
|src/svgfill
|src/exterior-shell-extractor
|choco/bonsai/tools/enable_blenderbim_addon.py
|choco/bonsai/tools/disable_blenderbim_addon.py
|docs/conf.py
|docs/generate_docs.py
|aws/lambda/example_handler/__init__.py
|conda/update_version_init.py
|test/bpy.py
|test/tests.py
|test/run.py
'''
[tool.pyright]
@@ -116,9 +112,7 @@ invalid-assignment = "ignore"
invalid-parameter-default = "ignore"
missing-override-decorator = "ignore"
invalid-yield = "ignore"
unsound-yield = "ignore"
invalid-return-type = "ignore"
unsound-return-statement = "ignore"
non-callable-init-subclass = "ignore"
not-iterable = "ignore"
possibly-missing-attribute = "ignore"
@@ -187,8 +181,6 @@ dev-setup.help = "Install repo packages in editable mode"
ruff = "ruff check"
check-whitespace = "uv run .github/scripts/check-whitespace.py"
black = "black ."
ty.sequence = ["ty-bonsai", "ty-ios"]
+1 -1
View File
@@ -1,5 +1,5 @@
black==26.3.1
ruff==0.16.0
poethepoet
ty==0.0.72
ty==0.0.63
gersemi==0.28.0
+19
View File
@@ -59,6 +59,7 @@ from bonsai.bim.module.model.decorator import (
)
from bonsai.bim.module.model.wall import WallGizmoPreviewDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from bonsai.tool.spatial import install_geom_cache_handlers, uninstall_geom_cache_handlers
cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object()
@@ -121,9 +122,25 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
def active_object_callback():
refresh_ui_data()
update_bim_tool_props()
update_spatial_tool_props()
tool.Geometry.sync_item_positions()
def update_spatial_tool_props():
"""Sync ``BIMSpatialDecompositionProperties.space_height`` with the
active object's height when it is an ``IfcSpace``, otherwise reset to
the 3m default. Called from the msgbus active-object callback so Scene
property writes happen outside ``draw()``."""
obj = tool.Blender.get_active_object()
props = tool.Spatial.get_spatial_props()
if obj:
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcSpace"):
props.space_height = obj.dimensions.z
return
props.space_height = 3
def update_bim_tool_props():
"""Selection-driven BIM Tool sync: re-target user-intent enums
(ifc_class, relating_type_id) AND refresh header values
@@ -528,6 +545,7 @@ def _install_viewport_overlays() -> None:
ArrayPreviewDecorator.uninstall()
ArraySelectionHighlightDecorator.uninstall()
uninstall_decorator_cache_handlers()
uninstall_geom_cache_handlers()
try:
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
@@ -570,6 +588,7 @@ def _install_viewport_overlays() -> None:
ArrayPreviewDecorator.install(bpy.context)
finally:
install_decorator_cache_handlers()
install_geom_cache_handlers()
@persistent
@@ -23,6 +23,7 @@ from . import operator, prop, ui
classes = (
operator.AddBoundary,
operator.ColourByRelatedBuildingElement,
operator.CopyBoundaryAttributeToSelection,
operator.DecorateBoundaries,
operator.DisableEditingBoundary,
operator.DisableEditingBoundaryGeometry,
+51 -195
View File
@@ -18,7 +18,7 @@
import logging
import multiprocessing
from math import acos, degrees, inf, pi, radians
from math import inf, pi
from typing import Optional, Union
import bmesh
@@ -28,6 +28,7 @@ import ifcopenshell.api.boundary
import ifcopenshell.api.root
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.boundary
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.shape
@@ -39,6 +40,7 @@ from ifcopenshell.util.shape_builder import ShapeBuilder
from mathutils import Matrix, Vector
import bonsai.bim.import_ifc as import_ifc
import bonsai.core.attribute as core
import bonsai.core.geometry
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
@@ -422,6 +424,32 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class CopyBoundaryAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_boundary_attribute_to_selection"
bl_label = "Copy Boundary Attribute To Selection"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def _execute(self, context):
obj = tool.Blender.get_active_object()
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
if self.name in EDITABLE_ATTRIBUTES:
blender_prop = EDITABLE_ATTRIBUTES[self.name]
blender_obj = getattr(bprops, blender_prop, None)
value = tool.Ifc.get_entity(blender_obj) if blender_obj else None
elif self.name == "PhysicalOrVirtualBoundary":
value = bprops.physical_or_virtual
elif self.name == "InternalOrExternalBoundary":
value = bprops.internal_or_external
else:
return
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.")
class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_boundary_geometry"
bl_label = "Update Boundary Geometry"
@@ -668,36 +696,30 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
def auto_generate_boundaries(
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
) -> Union[str, list[ifcopenshell.entity_instance]]:
"""
:return: list of created boundaries or a string with error description.
"""Generate boundaries by delegating to ifcopenshell.util.boundary.
This method handles Blender-specific preprocessing (flushing moved
objects, building the geometry cache + spatial tree) then delegates
the algorithm to the Blender-independent util module.
"""
ifc_file = tool.Ifc.get()
props = tool.Model.get_model_props()
boundaries: list[ifcopenshell.entity_instance] = []
assert isinstance(space_obj.data, bpy.types.Mesh)
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = list(
tool.Ifc.get().by_type("IfcWall")
+ tool.Ifc.get().by_type("IfcSlab")
+ tool.Ifc.get().by_type("IfcVirtualElement")
)
building_elements = []
for ifc_class in ifcopenshell.util.boundary.BOUNDARY_ELEMENT_CLASSES:
building_elements.extend(ifc_file.by_type(ifc_class))
# Flush moved objects to IFC
for building_element in building_elements:
if obj := tool.Ifc.get_object(building_element):
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(space_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=space_obj)
# Don't generate boundaries of building elements that we've already got bounaries for.
for boundary in space.BoundedBy:
if boundary.RelatedBuildingElement in building_elements:
building_elements.remove(boundary.RelatedBuildingElement)
# Create tree of gross shapes of all potential related building elements
# Build shapes dict with iterator (parallel, includes space + building elements)
include = building_elements + [space]
tree = ifcopenshell.geom.tree()
shapes = {}
@@ -712,189 +734,23 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
shapes[shape.id] = {
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
"edges": ifcopenshell.util.shape.get_edges(shape.geometry),
"matrix": ifcopenshell.util.shape.get_shape_matrix(shape),
}
if not iterator.next():
break
# Spatially query all potential boundary elements via a 100mm extension of the space
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
# Pass all building element shapes to the auto-generation function.
# The function performs its own spatial filtering (coplanarity + overlap),
# so tree-adjacency filtering is not needed here.
filtered_shapes = {space.id(): shapes[space.id()]}
for element in building_elements:
if element.id() in shapes:
filtered_shapes[element.id()] = shapes[element.id()]
if not building_elements:
return "No building elements found to create boundaries."
# Create a dissolved bmesh for the space
space_bm = bmesh.new()
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
# Create dissolved bmeshes for all boundary elements
building_element_bms = {}
for building_element in building_elements:
bm = bmesh.new()
shape = shapes[building_element.id()]
for vert in shape["verts"]:
bm.verts.new(Vector(vert))
bm.verts.ensure_lookup_table()
for face in shape["faces"]:
bm.faces.new([bm.verts[i] for i in face])
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
bm.normal_update() # Needed so that dissolve_limit will work.
bmesh.ops.dissolve_limit(bm, angle_limit=radians(1), verts=bm.verts[:], edges=bm.edges[:])
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
building_element_bms[building_element.id()] = bm
# Compare space faces and building element faces to see if they relate to one another
for space_face in space_bm.faces:
space_face_normal = space_obj.matrix_world.to_3x3() @ space_face.normal
space_face_vert = space_obj.matrix_world @ space_face.verts[0].co
for building_element in building_elements:
for face in building_element_bms[building_element.id()].faces:
building_obj = tool.Ifc.get_object(building_element)
face_normal = building_obj.matrix_world.to_3x3() @ face.normal
angle = degrees(acos(max(min(space_face_normal.dot(face_normal), 1), -1)))
if tool.Cad.is_x(angle, 180, tolerance=2):
pass # Faces need to be parallel and have opposite normals to be related.
elif building_element.is_a("IfcVirtualElement") and tool.Cad.is_x(angle, 0, tolerance=2):
pass # Virtual elements only need to be parallel to be related, since they are planes.
else:
continue
# Both faces should be close to one another. Say within 50mm.
space_vert = building_obj.matrix_world.inverted() @ space_face_vert
dist = mathutils.geometry.distance_point_to_plane(space_vert, face.verts[0].co, face.normal)
if abs(dist) > 0.05:
continue
# Project the building element face onto the space face
space_face_verts = [v.co.copy() for v in space_face.verts]
space_face_matrix = self.get_face_matrix(*[v.copy() for v in space_face_verts[0:3]])
space_face_matrix_i = space_face_matrix.inverted()
space_face_polygon = shapely.Polygon(
[tuple((space_face_matrix_i @ v).xy) for v in space_face_verts]
)
space_matrix_world_i = space_obj.matrix_world.inverted()
face_verts = [space_matrix_world_i @ building_obj.matrix_world @ v.co.copy() for v in face.verts]
face_polygon = shapely.Polygon([tuple((space_face_matrix_i @ v).xy) for v in face_verts])
gross_boundary_polygon = space_face_polygon.intersection(face_polygon)
if type(gross_boundary_polygon) == shapely.GeometryCollection:
for geom in gross_boundary_polygon.geoms:
if type(geom) == shapely.Polygon:
gross_boundary_polygon = geom
break
if (
not (isinstance(gross_boundary_polygon, shapely.Polygon) and gross_boundary_polygon.is_valid)
or gross_boundary_polygon.is_empty
):
continue
# The gross boundary polygon may not be a true gross boundary since it
# may have openings already removed, such as in IFC4 Reference View. So
# we cheat by using the exterior boundary to mean "gross".
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
parent_boundary.InternalOrExternalBoundary = "NOTDEFINED"
if building_element.is_a("IfcWall"):
is_external = ifcopenshell.util.element.get_pset(
building_element, "Pset_WallCommon", "IsExternal"
)
if is_external is True:
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
elif is_external is False:
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
elif building_element.is_a("IfcSlab"):
predefined_type = ifcopenshell.util.element.get_predefined_type(building_element)
if predefined_type == "BASESLAB":
parent_boundary.InternalOrExternalBoundary = "EXTERNAL_EARTH"
else:
is_external = ifcopenshell.util.element.get_pset(
building_element, "Pset_SlabCommon", "IsExternal"
)
if is_external is True:
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
elif is_external is False:
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
parent_boundary.RelatingSpace = space
parent_boundary.RelatedBuildingElement = building_element
parent_boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
exterior_boundary_polygon, space_face_matrix
)
self.set_boundary_name(parent_boundary)
boundaries.append(parent_boundary)
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
filling = opening.HasFillings[0].RelatedBuildingElement if opening.HasFillings else None
# Create shape of opening as a dissolved BMesh
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, opening)
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
opening_bm = bmesh.new()
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
opening_bm.verts.new(Vector(vert))
opening_bm.verts.ensure_lookup_table()
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
for face in faces:
opening_bm.faces.new([opening_bm.verts[i] for i in face])
opening_bm.verts.ensure_lookup_table()
opening_bm.faces.ensure_lookup_table()
opening_bm.normal_update() # Needed so that dissolve_limit will work.
bmesh.ops.dissolve_limit(
opening_bm, angle_limit=radians(1), verts=opening_bm.verts[:], edges=opening_bm.edges[:]
)
opening_bm.verts.ensure_lookup_table()
opening_bm.faces.ensure_lookup_table()
# Get relevant faces of BMesh that can turn into boundaries
opening_polygons = []
for opening_face in opening_bm.faces:
opening_face_normal = mat.to_3x3() @ opening_face.normal
angle = degrees(acos(max(min(opening_face_normal.dot(face_normal), 1), -1)))
if not tool.Cad.is_x(angle, 180, tolerance=2):
continue # Any non-parallel faces are not relevant
opening_face_verts = [space_matrix_world_i @ mat @ v.co.copy() for v in opening_face.verts]
polygon = shapely.Polygon([tuple((space_face_matrix_i @ v).xy) for v in opening_face_verts])
opening_polygons.append(polygon)
# Merge them all into a single opening polygon for our boundary
opening_polygon = shapely.ops.unary_union(opening_polygons)
# Only openings that are projected onto our exterior boundary are relevant.
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
opening_polygon, space_face_matrix
)
if filling:
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
else:
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
boundary.InternalOrExternalBoundary = parent_boundary.InternalOrExternalBoundary
if boundary.is_a() != "IfcRelSpaceBoundary":
boundary.ParentBoundary = parent_boundary
self.set_boundary_name(boundary)
boundaries.append(boundary)
return boundaries
return ifcopenshell.util.boundary.auto_generate_boundaries(
ifc_file, space, filtered_shapes, props.boundary_class
)
def create_element_boundary(
self,
+8 -2
View File
@@ -77,10 +77,14 @@ class BIM_PT_Boundary(Panel):
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
row = self.layout.row()
row = self.layout.row(align=True)
row.prop(self.bprops, "physical_or_virtual")
row = self.layout.row()
op = row.operator("bim.copy_boundary_attribute_to_selection", text="", icon="COPYDOWN")
op.name = "PhysicalOrVirtualBoundary"
row = self.layout.row(align=True)
row.prop(self.bprops, "internal_or_external")
op = row.operator("bim.copy_boundary_attribute_to_selection", text="", icon="COPYDOWN")
op.name = "InternalOrExternalBoundary"
else:
row = self.layout.row()
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
@@ -125,6 +129,8 @@ class BIM_PT_Boundary(Panel):
if hasattr(boundary, ifc_attribute):
row = self.layout.row(align=True)
row.prop(self.bprops, blender_property)
op = row.operator("bim.copy_boundary_attribute_to_selection", text="", icon="COPYDOWN")
op.name = ifc_attribute
class BIM_PT_SpaceBoundaries(Panel):
@@ -178,6 +178,7 @@ classes = (
covering.RegenSelectedCoveringObject,
space.ToggleSpaceVisibility,
space.ToggleHideSpaces,
space.ApplySpaceHeightToSelection,
mep.FitFlowSegments,
mep.RegenerateDistributionElement,
prop.SnapMousePoint,
@@ -18,7 +18,9 @@
import bpy
import ifcopenshell.util.unit
import bonsai.core.geometry as core_geometry
import bonsai.core.spatial as core
import bonsai.tool as tool
@@ -115,3 +117,47 @@ class ToggleHideSpaces(bpy.types.Operator):
def execute(self, context):
core.toggle_hide_spaces(tool.Ifc, tool.Spatial)
return {"FINISHED"}
class ApplySpaceHeightToSelection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.apply_space_height_to_selection"
bl_label = "Apply Space Height To Selection"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Apply the space height value to all selected spaces without regenerating their footprint"
@classmethod
def poll(cls, context):
selected_spaces = [
obj
for obj in context.selected_objects
if (element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace")
]
if not selected_spaces:
cls.poll_message_set("No spaces selected.")
return False
return True
def _execute(self, context):
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
depth_ifc = tool.Spatial.get_spatial_props().space_height / si_conversion
total = 0
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcSpace"):
continue
body = tool.Geometry.get_body_representation(element)
if not body:
continue
extrusion = tool.Model.get_extrusion(body)
if not extrusion:
continue
extrusion.Depth = depth_ifc
core_geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
)
total += 1
self.report({"INFO"}, f"Height applied to {total} spaces.")
@@ -24,6 +24,7 @@ from bpy.props import (
BoolProperty,
CollectionProperty,
EnumProperty,
FloatProperty,
IntProperty,
PointerProperty,
StringProperty,
@@ -277,6 +278,17 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
should_include_children: BoolProperty(
name="Should Include Children", default=True, update=update_should_include_children
)
space_height: FloatProperty(
name="Space Height",
default=3,
subtype="DISTANCE",
description="Space height in meters. Auto-detected on generation unless forced. Used as fallback.",
)
force_space_height: BoolProperty(
name="Force Height",
default=False,
description="If enabled, uses the height value directly and skips auto-detection",
)
if TYPE_CHECKING:
is_locked: bool
@@ -294,6 +306,8 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
subelement_class: str
default_container: int
should_include_children: bool
space_height: float
force_space_height: bool
@property
def active_container(self) -> Union[BIMContainer, None]:
@@ -83,9 +83,14 @@ class SpatialToolUI:
@classmethod
def draw_default_interface(cls, context):
spatial_props = tool.Spatial.get_spatial_props()
row = cls.layout.row(align=True)
row.prop(data=cls.model_props, property="rl3", text="RL")
row = cls.layout.row(align=True)
row.prop(data=spatial_props, property="space_height", text="Height")
row.prop(data=spatial_props, property="force_space_height", text="", icon="PINNED")
row.operator("bim.apply_space_height_to_selection", text="", icon="COPYDOWN")
row = cls.layout.row(align=True)
op_name = lambda op: op.get_rna_type().name
if AuthoringData.data["active_class"] == "IfcWall" and context.selected_objects:
add_layout_hotkey(
+2 -2
View File
@@ -18,7 +18,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
@@ -31,7 +31,7 @@ def copy_attribute_to_selection(
root: type[tool.Root],
spatial: type[tool.Spatial],
name: str,
value: Union[str, None],
value: Any,
) -> int:
total_changed = 0
has_edited_spatial_name = False
+3 -3
View File
@@ -46,7 +46,7 @@ def add_instance_flooring_covering_from_cursor(
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
space_polygon, _ = spatial.get_space_polygon_from_context_visible_objects(x, y)
if isinstance(space_polygon, str):
return
@@ -81,7 +81,7 @@ def add_instance_ceiling_covering_from_cursor(
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
ceiling_height = covering.get_z_from_ceiling_height()
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
space_polygon, _ = spatial.get_space_polygon_from_context_visible_objects(x, y)
if isinstance(space_polygon, str):
return
@@ -106,7 +106,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
else:
assert False, "Object has to be active and selected."
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
space_polygon, _ = spatial.get_space_polygon_from_context_visible_objects(x, y)
if isinstance(space_polygon, str):
return
+46 -7
View File
@@ -20,9 +20,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
import ifcopenshell
if TYPE_CHECKING:
import bpy
import ifcopenshell
import bonsai.tool as tool
@@ -186,9 +187,6 @@ def generate_space(
"""
:return: None if successful, error message string if not.
"""
if not root.get_default_container():
raise SpaceGenerationError("Please set a default container to create the space in.")
active_obj = spatial.get_active_obj()
selected_objects = spatial.get_selected_objects()
element = None
@@ -206,7 +204,15 @@ def generate_space(
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
if element and element.is_a("IfcSpace"):
z = active_obj.location.z
container = ifcopenshell.util.element.get_parent(element) or root.get_default_container()
else:
container = root.get_default_container()
if not container:
raise SpaceGenerationError("Please set a default container to create the space in.")
space_polygon, bounding_walls = spatial.get_space_polygon_from_context_visible_objects(x, y, container=container)
if isinstance(space_polygon, str):
if space_polygon == "NO POLYGONS FOUND":
@@ -220,8 +226,25 @@ def generate_space(
else:
assert space_polygon
props = spatial.get_spatial_props()
if props.force_space_height:
h = props.space_height
else:
auto_h = spatial.get_auto_space_height(space_polygon, z, bounding_walls)
if auto_h is not None and auto_h > 0:
h = auto_h
if element and element.is_a("IfcSpace"):
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
assert active_obj
spatial.set_space_representation_from_polygon(
active_obj,
element,
space_polygon,
h,
polygon_is_si=True,
bounding_walls=bounding_walls,
container=container,
)
else:
if relating_type:
name = model.generate_occurrence_name(relating_type, "IfcSpace")
@@ -234,7 +257,9 @@ def generate_space(
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True)
spatial.set_space_representation_from_polygon(
obj, element, space_polygon, h, polygon_is_si=True, bounding_walls=bounding_walls, container=container
)
if relating_type:
spatial.assign_relating_type_to_element(ifc, type, element, relating_type)
@@ -248,11 +273,25 @@ def generate_spaces_from_walls(
z = spatial.get_active_obj_z()
h = spatial.get_active_obj_height()
bounding_walls = [
element
for obj in spatial.get_selected_objects()
if (element := ifc.get_entity(obj)) and element.is_a("IfcWall")
]
union = spatial.get_union_shape_from_selected_objects()
props = spatial.get_spatial_props()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
if props.force_space_height:
h = props.space_height
else:
auto_h = spatial.get_auto_space_height(poly, z, bounding_walls)
if auto_h is not None and auto_h > 0:
h = auto_h
name = "Space" + str(i)
obj = spatial.create_object(name)
+1 -1
View File
@@ -618,7 +618,7 @@ class Model(bonsai.core.tool.Model):
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)])
if is_closed:
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
cls.edges.append((len(cls.vertices) - 1, offset)) # Close the loop
elif curve.is_a("IfcCompositeCurve"):
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
+368 -25
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
import json
import multiprocessing
from collections import defaultdict
from collections.abc import Generator, Iterable
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
@@ -34,11 +35,14 @@ import ifcopenshell.util.classification
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import ifcopenshell.util.shape_builder
import ifcopenshell.util.space
import ifcopenshell.util.type
import ifcopenshell.util.unit
import numpy as np
import shapely
import shapely.affinity
import shapely.ops
from mathutils import Matrix, Vector
from natsort import natsorted
@@ -58,8 +62,52 @@ if TYPE_CHECKING:
BIMSpatialDecompositionProperties,
)
_GEOM_CACHE_TOKEN = 0
@bpy.app.handlers.persistent
def _bump_geom_cache_token(*args) -> None:
global _GEOM_CACHE_TOKEN
if len(args) >= 2:
depsgraph = args[1]
if depsgraph is not None and hasattr(depsgraph, "updates"):
if not any(
(getattr(u, "is_updated_geometry", False) or getattr(u, "is_updated_transform", False))
and hasattr(u, "id")
and isinstance(u.id, bpy.types.Object)
for u in depsgraph.updates
):
return
_GEOM_CACHE_TOKEN += 1
def install_geom_cache_handlers() -> None:
for hook in (
bpy.app.handlers.depsgraph_update_post,
bpy.app.handlers.undo_post,
bpy.app.handlers.redo_post,
bpy.app.handlers.load_post,
):
if _bump_geom_cache_token not in hook:
hook.append(_bump_geom_cache_token)
def uninstall_geom_cache_handlers() -> None:
for hook in (
bpy.app.handlers.depsgraph_update_post,
bpy.app.handlers.undo_post,
bpy.app.handlers.redo_post,
bpy.app.handlers.load_post,
):
try:
hook.remove(_bump_geom_cache_token)
except ValueError:
pass
class Spatial(bonsai.core.tool.Spatial):
_geom_cache: dict = {}
@classmethod
def get_spatial_props(cls) -> BIMSpatialDecompositionProperties:
return bpy.context.scene.BIMSpatialDecompositionProperties
@@ -755,29 +803,233 @@ class Spatial(bonsai.core.tool.Spatial):
# HERE STARTS SPATIAL TOOL
@classmethod
def get_or_build_geom_cache(cls) -> dict:
"""Build or return a cached dict of IFC element shapes for space generation.
The cache is keyed on ``_GEOM_CACHE_TOKEN`` which is bumped by a
``depsgraph_update_post`` handler when any Object geometry or transform
changes, and on undo/redo/load. This means the cache survives space
generations (which don't change Object geometry) but is correctly
invalidated when a user moves or edits a wall, slab, etc.
:return: ``{"shapes": {id: {"verts": ndarray, "faces": ndarray, "bottom_z": float, "top_z": float}}, "token": int}``
"""
global _GEOM_CACHE_TOKEN
cached = cls._geom_cache.get("current")
if cached and cached["token"] == _GEOM_CACHE_TOKEN:
return cached
ifc_file = tool.Ifc.get()
include = []
for ifc_class in ifcopenshell.util.space.BOUNDING_CLASSES + ifcopenshell.util.space.HEIGHT_DETECTION_CLASSES:
include.extend(ifc_file.by_type(ifc_class))
settings = ifcopenshell.geom.settings()
settings.set("disable-opening-subtractions", True)
settings.set("use-world-coords", True)
shapes = {}
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
shape = iterator.get()
verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry)
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
zs = verts[:, 2]
shapes[shape.id] = {
"verts": verts,
"faces": faces,
"bottom_z": float(zs.min()),
"top_z": float(zs.max()),
}
if not iterator.next():
break
cache = {"shapes": shapes, "token": _GEOM_CACHE_TOKEN}
cls._geom_cache["current"] = cache
return cache
@classmethod
def is_bounding_class(cls, visible_element: ifcopenshell.entity_instance) -> bool:
for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]:
for ifc_class in ifcopenshell.util.space.BOUNDING_CLASSES:
if visible_element.is_a(ifc_class):
return True
return False
@classmethod
def get_boundary_lines_from_ifc_elements(
cls,
cut_z: float,
) -> tuple[list[shapely.LineString], list[ifcopenshell.entity_instance]]:
"""Generate boundary lines by bisecting IFC element geometry with a horizontal plane.
Uses the class-level geometry cache (parallel iterator) instead of
iterating Blender visible objects. Works without any Blender objects
being loaded.
:param cut_z: Z elevation of the cutting plane in world coordinates.
:return: (boundary_lines, bounding_elements)
"""
cache = cls.get_or_build_geom_cache()
return ifcopenshell.util.space.get_boundary_lines(tool.Ifc.get(), cache["shapes"], cut_z)
@classmethod
def get_space_polygon_from_context_visible_objects(
cls, x: float, y: float
) -> Union[shapely.Polygon, Literal["NO POLYGONS FOUND", "NO POLYGON FOR POINT"]]:
boundary_lines = cls.get_boundary_lines_from_context_visible_objects()
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
if not closed_polygons:
return "NO POLYGONS FOUND"
space_polygon = None
for polygon in closed_polygons.geoms:
if shapely.contains_xy(polygon, x, y):
space_polygon = shapely.force_3d(polygon)
if space_polygon is None:
return "NO POLYGON FOR POINT"
return space_polygon
cls, x: float, y: float, container: Optional[ifcopenshell.entity_instance] = None
) -> tuple[
Union[shapely.Polygon, Literal["NO POLYGONS FOUND", "NO POLYGON FOR POINT"]],
list[ifcopenshell.entity_instance],
]:
props = tool.Model.get_model_props()
calculation_rl = props.rl3
if container is None:
container = tool.Root.get_default_container()
container_obj = tool.Ifc.get_object(container)
cut_z = container_obj.matrix_world.translation.z + calculation_rl
# Commit any moved visible bounding objects before reading IFC geometry,
# so the IFC-based cache uses the current Blender positions.
# Walls/roofs/slabs that affect the space footprint or height must be
# committed before the cache is rebuilt; otherwise the IFC geometry read by
# the iterator will be stale and a moved roof/slab will not be picked up.
affected_classes = ifcopenshell.util.space.BOUNDING_CLASSES + ifcopenshell.util.space.HEIGHT_DETECTION_CLASSES
for obj in bpy.context.visible_objects:
element = tool.Ifc.get_entity(obj)
if element is None or not any(element.is_a(c) for c in affected_classes):
continue
tool.Geometry.commit_placement_if_moved(obj)
cls._geom_cache.clear()
boundary_lines, bounding_elements = cls.get_boundary_lines_from_ifc_elements(cut_z)
polygon, _ = ifcopenshell.util.space.get_space_polygon(boundary_lines, x, y)
if isinstance(polygon, str):
return polygon, []
return polygon, bounding_elements
@classmethod
def get_auto_space_height(
cls,
space_polygon: shapely.Polygon,
base_z: float,
bounding_walls: list[ifcopenshell.entity_instance],
) -> Optional[float]:
"""Auto-detect space height from elements above using IFC geometry.
Delegates to :func:`ifcopenshell.util.space.get_auto_space_height`.
:param space_polygon: The space footprint polygon in world XY.
:param base_z: The space's base Z in world coordinates.
:param bounding_walls: List of IFC wall elements bounding the space.
:return: Detected height in SI (meters), or None if nothing found.
"""
cache = cls.get_or_build_geom_cache()
return ifcopenshell.util.space.get_auto_space_height(
tool.Ifc.get(), cache["shapes"], space_polygon, base_z, bounding_walls
)
@classmethod
def get_space_volume_strategy(
cls,
space_polygon: shapely.Polygon,
base_z: float,
bounding_walls: list[ifcopenshell.entity_instance],
container: Optional[ifcopenshell.entity_instance] = None,
) -> tuple[str, Optional[list], Optional[list]]:
"""Decide how to build the space volume (clipped extrusion or B-rep).
Rays are cast from the RL cut elevation (``container_z + props.rl3``), the
same level at which the space footprint polygon was found.
"""
ifc_file = tool.Ifc.get()
cache = cls.get_or_build_geom_cache()
start_z = None
if container is None:
container = tool.Root.get_default_container()
if container is not None:
container_obj = tool.Ifc.get_object(container)
props = tool.Model.get_model_props()
start_z = container_obj.matrix_world.translation.z + props.rl3
tree = ifcopenshell.geom.tree(ifc_file)
settings = ifcopenshell.geom.settings()
settings.set("disable-opening-subtractions", True)
settings.set("use-world-coords", True)
tree.add_file(ifc_file, settings)
return ifcopenshell.util.space.detect_space_volume_strategy(
ifc_file, cache["shapes"], tree, space_polygon, base_z, bounding_walls, start_z=start_z
)
@classmethod
def _get_or_create_body_context(cls, ifc_file: ifcopenshell.file) -> ifcopenshell.entity_instance:
"""Return the Model/Body/MODEL_VIEW context, creating one if absent."""
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
if context is not None:
return context
# Some subcontexts may not expose the inherited ContextType value, so also
# search by ContextIdentifier/TargetView directly.
for ctx in ifc_file.by_type("IfcGeometricRepresentationSubContext"):
if ctx.ContextIdentifier == "Body" and getattr(ctx, "TargetView", None) == "MODEL_VIEW":
return ctx
# Create a minimal context if none exists.
model_context = ifcopenshell.util.representation.get_context(ifc_file, "Model")
if model_context is None:
model_context = ifc_file.createIfcGeometricRepresentationContext(
ContextType="Model",
CoordinateSpaceDimension=3,
Precision=1e-5,
WorldCoordinateSystem=ifc_file.createIfcAxis2Placement3D(
ifc_file.createIfcCartesianPoint([0.0, 0.0, 0.0])
),
TrueNorth=ifc_file.createIfcDirection([0.0, 1.0, 0.0]),
)
return ifc_file.createIfcGeometricRepresentationSubContext(
ParentContext=model_context,
ContextIdentifier="Body",
TargetView="MODEL_VIEW",
ContextType="Model",
)
@classmethod
def _remove_existing_body_representations(
cls, element: ifcopenshell.entity_instance
) -> Optional[ifcopenshell.entity_instance]:
"""Remove every existing Body representation from an element.
Returns the context of the first removed representation, or None.
"""
ifc_file = tool.Ifc.get()
if element.Representation is None:
return None
body_reps = [r for r in element.Representation.Representations if r.RepresentationIdentifier == "Body"]
context = None
for rep in body_reps:
context = rep.ContextOfItems
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=rep)
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=rep)
return context
@classmethod
def set_brep_representation_from_mesh(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
item: ifcopenshell.entity_instance,
) -> None:
"""Assign a representation item (clipped solid or B-rep) to the element."""
ifc_file = tool.Ifc.get()
context = cls._remove_existing_body_representations(element)
if context is None:
context = cls._get_or_create_body_context(ifc_file)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
new_body = builder.get_representation(context, item)
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_body,
)
@classmethod
def debug_shape(cls, foo: shapely.Polygon) -> None:
@@ -810,7 +1062,9 @@ class Spatial(bonsai.core.tool.Spatial):
bpy.context.view_layer.update()
@classmethod
def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]:
def get_boundary_lines_from_context_visible_objects(
cls,
) -> tuple[list[shapely.LineString], list[ifcopenshell.entity_instance]]:
props = tool.Model.get_model_props()
calculation_rl = props.rl3
container = tool.Root.get_default_container()
@@ -818,6 +1072,7 @@ class Spatial(bonsai.core.tool.Spatial):
cut_point = container_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl))
cut_normal = Vector((0, 0, 1))
boundary_lines = []
bounding_elements = []
for obj in bpy.context.visible_objects:
visible_element = tool.Ifc.get_entity(obj)
@@ -831,6 +1086,7 @@ class Spatial(bonsai.core.tool.Spatial):
):
continue
bounding_elements.append(visible_element)
old_mesh = obj.data
assert isinstance(old_mesh, bpy.types.Mesh)
if visible_element.HasOpenings:
@@ -870,7 +1126,7 @@ class Spatial(bonsai.core.tool.Spatial):
start, end = tool.Drawing.extend_line(start, end, 0.05)
boundary_lines.append(shapely.LineString([start, end]))
return boundary_lines
return boundary_lines, bounding_elements
@classmethod
def get_gross_mesh_from_element(cls, visible_element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
@@ -1086,13 +1342,9 @@ class Spatial(bonsai.core.tool.Spatial):
curve = builder.polyline(coords_2d, closed=True)
item = builder.extrude(curve, magnitude=depth_ifc)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
context = old_body.ContextOfItems
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body)
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
else:
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
context = cls._remove_existing_body_representations(element)
if context is None:
context = cls._get_or_create_body_context(ifc_file)
new_body = builder.get_representation(context, item)
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
@@ -1111,13 +1363,104 @@ class Spatial(bonsai.core.tool.Spatial):
poly: Polygon,
h: float,
polygon_is_si: bool = True,
bounding_walls: Optional[list[ifcopenshell.entity_instance]] = None,
container: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""Create or replace the IFC body representation of a space from a polygon.
:param h: The height in SI (meters).
"""
# Remove collinear points introduced by the mesh bisection so the
# footprint polygon has a minimal vertex count.
poly = poly.simplify(0, preserve_topology=True)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
ifc_file = tool.Ifc.get()
x, y, z = obj.matrix_world.translation
origin = obj.matrix_world.translation # Blender SI
# The space builders expect base_z and polygon in SI (world) units.
base_z = z
poly_si = poly if polygon_is_si else shapely.affinity.scale(poly, unit_scale, unit_scale, origin=(0, 0))
# Ensure the IFC entity has an ObjectPlacement matching the Blender object,
# so the generated representation is in the correct local coordinate system.
bpy.context.view_layer.update()
matrix = np.array(obj.matrix_world)
ifcopenshell.api.geometry.edit_object_placement(
ifc_file,
product=element,
matrix=matrix,
is_si=True,
)
for b in list(element.BoundedBy or []):
ifcopenshell.api.boundary.remove_boundary(ifc_file, b)
cls._remove_existing_body_representations(element)
if cls.get_spatial_props().force_space_height:
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
return
if bounding_walls is None:
bounding_walls = []
if container is None:
container = ifcopenshell.util.element.get_container(element)
if container is not None:
for wall in ifc_file.by_type("IfcWall"):
if wall in ifcopenshell.util.element.get_decomposition(container):
bounding_walls.append(wall)
# Detect planes in world SI (same coordinate system as the geom cache).
strategy, top_planes, bottom_planes = cls.get_space_volume_strategy(poly_si, base_z, bounding_walls, container)
# Build the geometry in the space's local coordinate system so the IFC
# representation is relative to the object's ObjectPlacement.
# Use the full inverse of the object's placement matrix so rotated spaces
# keep the correct footprint orientation.
matrix_inv = np.array(obj.matrix_world.inverted())
# shapely.affine_transform expects [a, b, d, e, xoff, yoff]
# where x' = a*x + b*y + xoff, y' = d*x + e*y + yoff.
affine_params = [
matrix_inv[0, 0],
matrix_inv[0, 1],
matrix_inv[1, 0],
matrix_inv[1, 1],
matrix_inv[0, 3],
matrix_inv[1, 3],
]
local_poly_si = shapely.affinity.affine_transform(poly_si, affine_params)
local_base_z = base_z - origin.z
def localize_plane(plane):
point, normal = plane
local_point = matrix_inv @ np.array([*point, 1.0])
rotation_inv = matrix_inv[:3, :3]
local_normal = rotation_inv @ np.array(normal)
local_normal = local_normal / np.linalg.norm(local_normal)
return (local_point[:3], local_normal)
local_top_planes = [localize_plane(p) for p in (top_planes or [])]
local_bottom_planes = [localize_plane(p) for p in (bottom_planes or [])]
if strategy == "EXTRUDE_CLIP" and top_planes:
item = ifcopenshell.util.space.build_extruded_clipped_space(
ifc_file, local_poly_si, local_base_z, local_top_planes, local_bottom_planes
)
cls.set_brep_representation_from_mesh(obj, element, item)
else:
shapes = cls.get_or_build_geom_cache()["shapes"]
local_shapes = {}
for shape_id, shape_data in shapes.items():
local_shape_data = dict(shape_data)
local_shape_data["top_z"] = shape_data["top_z"] - origin.z
local_shape_data["bottom_z"] = shape_data["bottom_z"] - origin.z
local_shapes[shape_id] = local_shape_data
item = ifcopenshell.util.space.build_brep_space(
ifc_file, element, local_shapes, local_poly_si, local_base_z
)
if item is None:
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
else:
cls.set_brep_representation_from_mesh(obj, element, item)
@classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
+3 -10
View File
@@ -245,16 +245,9 @@ class Wall(bonsai.core.tool.Wall):
@classmethod
def iter_wall_slab_connections(cls, wall: ifcopenshell.entity_instance):
"""Yield ``(slab, rel)`` tuples for every ``IfcRelConnectsElements(TOP)``
connecting a slab to this wall — the rel kind ``extend_walls_to_underside``
creates. Walks ``wall.ConnectedFrom`` because the slab is the relating
side of the TOP rel."""
for rel in getattr(wall, "ConnectedFrom", []) or ():
if not rel.is_a("IfcRelConnectsElements") or rel.Description != "TOP":
continue
slab = rel.RelatingElement
if slab is None:
continue
yield slab, rel
connecting a slab to this wall. Delegates to
:func:`ifcopenshell.util.element.iter_top_connections`."""
yield from ifcopenshell.util.element.iter_top_connections(wall)
@classmethod
def iter_slab_wall_connections(cls, slab: ifcopenshell.entity_instance):
+112
View File
@@ -0,0 +1,112 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 pytest
import bonsai
import bonsai.core.covering as subject
import bonsai.core.tool
from test.core.bootstrap import Prophecy, ifc, root, spatial
# NOTE: The Prophecy mocking framework serialises call arguments as JSON,
# which means shapely geometry objects cannot be passed through mocked
# calls. We use the plain integer 42 as a serialisable stand-in for the
# polygon return value; the test verifies the unpack behaviour (that the
# polygon-like scalar 42 reaches set_covering_representation_from_polygon
# instead of the tuple (42, []) which old code would have passed).
@pytest.fixture
def covering():
prophet = Prophecy(bonsai.core.tool.Covering)
yield prophet
prophet.verify()
class TestAddInstanceFlooringCoveringFromCursor:
def test_run(self, ifc, root, spatial):
root.get_default_container().should_be_called().will_return("container")
spatial.get_active_obj().should_be_called().will_return(None)
spatial.get_selected_objects().should_be_called().will_return([])
spatial.get_relating_type_id().should_be_called().will_return(0)
spatial.get_x_y_z_h_mat_from_cursor().should_be_called().will_return((0, 0, 0, 3, None))
spatial.get_space_polygon_from_context_visible_objects(0, 0).should_be_called().will_return((42, []))
spatial.create_object("Covering").should_be_called().will_return("mock_obj")
spatial.set_obj_origin_to_cursor_position_and_zero_elevation("mock_obj").should_be_called()
spatial.translate_obj_to_z_location("mock_obj", 0).should_be_called()
spatial.assign_type_to_obj("mock_obj").should_be_called()
spatial.set_covering_representation_from_polygon("mock_obj", 42, polygon_is_si=True).should_be_called()
subject.add_instance_flooring_covering_from_cursor(ifc, root, spatial)
def test_raises_when_no_default_container(self, ifc, root, spatial):
root.get_default_container().should_be_called().will_return(None)
with pytest.raises(subject.NoDefaultContainer):
subject.add_instance_flooring_covering_from_cursor(ifc, root, spatial)
class TestAddInstanceCeilingCoveringFromCursor:
def test_run(self, ifc, root, covering, spatial):
root.get_default_container().should_be_called().will_return("container")
spatial.get_active_obj().should_be_called().will_return(None)
spatial.get_selected_objects().should_be_called().will_return([])
spatial.get_relating_type_id().should_be_called().will_return(0)
covering.get_z_from_ceiling_height().should_be_called().will_return(3.0)
spatial.get_x_y_z_h_mat_from_cursor().should_be_called().will_return((0, 0, 0, 3, None))
spatial.get_space_polygon_from_context_visible_objects(0, 0).should_be_called().will_return((42, []))
spatial.create_object("Covering").should_be_called().will_return("mock_obj")
spatial.set_obj_origin_to_cursor_position_and_zero_elevation("mock_obj").should_be_called()
spatial.translate_obj_to_z_location("mock_obj", 3.0).should_be_called()
spatial.assign_type_to_obj("mock_obj").should_be_called()
spatial.set_covering_representation_from_polygon("mock_obj", 42, polygon_is_si=True).should_be_called()
subject.add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial)
def test_raises_when_no_default_container(self, ifc, root, covering, spatial):
root.get_default_container().should_be_called().will_return(None)
with pytest.raises(subject.NoDefaultContainer):
subject.add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial)
class TestRegenSelectedCoveringObject:
def test_run(self, root, spatial):
root.get_default_container().should_be_called().will_return("container")
spatial.get_active_obj().should_be_called().will_return("active")
spatial.get_selected_objects().should_be_called().will_return(["active"])
spatial.get_x_y_z_h_mat_from_obj("active").should_be_called().will_return((2, 3, 1, 3, None))
spatial.get_space_polygon_from_context_visible_objects(2, 3).should_be_called().will_return((42, []))
spatial.set_covering_representation_from_polygon("active", 42, polygon_is_si=True).should_be_called()
subject.regen_selected_covering_object(root, spatial)
def test_raises_when_no_default_container(self, root, spatial):
root.get_default_container().should_be_called().will_return(None)
with pytest.raises(subject.NoDefaultContainer):
subject.regen_selected_covering_object(root, spatial)
def test_raises_when_no_active_selected(self, root, spatial):
root.get_default_container().should_be_called().will_return("container")
spatial.get_active_obj().should_be_called().will_return(None)
spatial.get_selected_objects().should_be_called().will_return([])
with pytest.raises(AssertionError):
subject.regen_selected_covering_object(root, spatial)
+39
View File
@@ -32,6 +32,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import numpy as np
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from mathutils import Matrix
import bonsai.core.tool
import bonsai.tool as tool
@@ -1044,3 +1045,41 @@ class TestGetSiblingOccurrenceCount(NewFile):
ifcopenshell.api.type.assign_type(ifc, related_objects=occurrences, relating_type=wall_type)
assert subject.get_sibling_occurrence_count(wall_type) == 2
class TestConvertCurveToMesh(NewFile):
def test_closed_polyline_converts_to_closed_loop(self):
"""A closed IfcPolyline must produce the full edge loop.
Before the fix (cls.edges[-1] = … overwrite) the closing edge
replaced the real last segment, leaving every loop open by one
edge — e.g. a quad got only 3 edges.
"""
ifc = ifcopenshell.file()
# Closed quad: 4 unique points + closing repeat = 5 points
p0 = ifc.createIfcCartesianPoint((0.0, 0.0))
p1 = ifc.createIfcCartesianPoint((1.0, 0.0))
p2 = ifc.createIfcCartesianPoint((1.0, 1.0))
p3 = ifc.createIfcCartesianPoint((0.0, 1.0))
polyline = ifc.createIfcPolyline((p0, p1, p2, p3, p0))
subject.vertices = []
subject.edges = []
subject.arcs = []
subject.circles = []
subject.unit_scale = 1.0
subject.convert_curve_to_mesh(None, Matrix(), polyline)
assert len(subject.vertices) == 4, f"Expected 4 vertices, got {len(subject.vertices)}"
assert len(subject.edges) == 4, f"Expected 4 edges, got {len(subject.edges)}"
# Every vertex must appear in exactly 2 edges (closed loop)
from collections import defaultdict
counts = defaultdict(int)
for e in subject.edges:
counts[e[0]] += 1
counts[e[1]] += 1
for v_idx, cnt in counts.items():
assert cnt == 2, f"Vertex {v_idx} has {cnt} incident edges (expected 2)"
+546 -16
View File
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
import bpy
import ifcopenshell
import ifcopenshell.api
@@ -24,12 +26,16 @@ import ifcopenshell.api.feature
import ifcopenshell.api.nest
import ifcopenshell.api.root
import ifcopenshell.api.spatial
import ifcopenshell.util.representation
import numpy as np
from mathutils import Matrix
import pytest
import shapely
from mathutils import Matrix, Vector
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.tool.spatial import Spatial as subject
from bonsai.tool.spatial import _bump_geom_cache_token
from test.bim.bootstrap import NewFile
@@ -258,17 +264,59 @@ class TestSelectProducts(NewFile):
assert obj in bpy.context.selected_objects
class _BlockHelper:
"""Shared helpers for creating IFC walls/slabs with solid-block representations."""
@staticmethod
def create_wall(ifc, height=10.0):
"""Create an IFC wall with a 10x10x{height} block representation from z=0."""
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, 10.0, 10.0)
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([0.0, 0.0, 0.0]))
extrusion = ifc.createIfcExtrudedAreaSolid(
profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), height
)
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
wall.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
return wall, extrusion
@staticmethod
def create_thin_wall(ifc, cx, cy, width, depth, height=10.0):
"""Create an IFC wall with a thin block representation centered at (cx, cy)."""
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, width, depth)
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([cx, cy, 0.0]))
extrusion = ifc.createIfcExtrudedAreaSolid(
profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), height
)
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
wall.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
return wall
@staticmethod
def create_slab(ifc, z=4.0):
"""Create an IfcSlab with a 12x12x1.0 block representation at bottom_z={z}."""
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
slab = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSlab")
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, 12.0, 12.0)
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([0.0, 0.0, z]))
extrusion = ifc.createIfcExtrudedAreaSolid(profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), 1.0)
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
slab.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
class TestGenerateSpace(NewFile):
def test_generate_space_at_cursor(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
scene = bpy.context.scene
product = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
scene.collection.objects.link(obj)
tool.Ifc.link(product, obj)
scene.cursor.location = (0, 0, 0)
# The wall block spans z=0..10, bisects to a 10x10 polygon at cut_z.
_BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
@@ -292,13 +340,8 @@ class TestGenerateSpace(NewFile):
def test_regenerate_space_preserves_z_location(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
scene = bpy.context.scene
product = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
scene.collection.objects.link(obj)
tool.Ifc.link(product, obj)
scene.cursor.location = (0, 0, 0)
_BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
@@ -307,8 +350,495 @@ class TestGenerateSpace(NewFile):
bpy.context.view_layer.objects.active = space
space.select_set(True)
obj.select_set(False)
bpy.ops.bim.generate_space()
assert np.isclose(space.location.z, 5), f"Expected z=5, got {space.location.z}"
def test_auto_space_height_from_slab_above(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
_BlockHelper.create_slab(ifc, z=4.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
assert np.isclose(space.dimensions.z, 4, atol=0.1), f"Expected height ~4, got {space.dimensions.z}"
def test_forced_space_height(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
spatial_props = tool.Spatial.get_spatial_props()
spatial_props.force_space_height = True
spatial_props.space_height = 5
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
assert np.isclose(space.dimensions.z, 5, atol=0.1), f"Expected height 5, got {space.dimensions.z}"
def test_auto_space_height_fallback_no_slab(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
spatial_props = tool.Spatial.get_spatial_props()
spatial_props.force_space_height = False
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
assert space.dimensions.z > 0, f"Expected positive height, got {space.dimensions.z}"
def test_apply_space_height_to_selection(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
spatial_props = tool.Spatial.get_spatial_props()
spatial_props.space_height = 6
bpy.context.view_layer.objects.active = space
space.hide_viewport = False
space.select_set(True)
bpy.ops.bim.apply_space_height_to_selection()
bpy.context.view_layer.update()
assert np.isclose(space.dimensions.z, 6, atol=0.1), f"Expected height 6, got {space.dimensions.z}"
def test_cache_survives_second_generation(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space1 = bpy.data.objects["IfcSpace/Space"]
height1 = space1.dimensions.z
bpy.ops.bim.generate_space()
space2 = bpy.data.objects["IfcSpace/Space"]
height2 = space2.dimensions.z
assert np.isclose(height1, height2, atol=0.1), f"Cache changed height: {height1} vs {height2}"
def test_regenerate_after_wall_height_change(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
wall, extrusion = _BlockHelper.create_wall(ifc, height=10.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
original_height = space.dimensions.z
# Modify the IFC representation to change the wall height.
extrusion.Depth = 15.0
_bump_geom_cache_token()
bpy.context.view_layer.objects.active = space
space.select_set(True)
bpy.ops.bim.generate_space()
new_height = space.dimensions.z
assert new_height != original_height or new_height > 0
def test_regenerate_space_from_centered_cube_representation(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
scene = bpy.context.scene
scene.cursor.location = (0, 0, 0)
# Create a space with a unit cube PolygonalFaceSet centered at local origin.
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
points = ifc.createIfcCartesianPointList3D(
[
(-0.5, -0.5, -0.5),
(-0.5, -0.5, 0.5),
(-0.5, 0.5, -0.5),
(-0.5, 0.5, 0.5),
(0.5, -0.5, -0.5),
(0.5, -0.5, 0.5),
(0.5, 0.5, -0.5),
(0.5, 0.5, 0.5),
]
)
faces = [
ifc.createIfcIndexedPolygonalFace([1, 2, 4, 3]),
ifc.createIfcIndexedPolygonalFace([3, 4, 8, 7]),
ifc.createIfcIndexedPolygonalFace([7, 8, 6, 5]),
ifc.createIfcIndexedPolygonalFace([5, 6, 2, 1]),
ifc.createIfcIndexedPolygonalFace([3, 7, 5, 1]),
ifc.createIfcIndexedPolygonalFace([8, 4, 2, 6]),
]
face_set = ifc.createIfcPolygonalFaceSet(points, True, faces)
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "Tessellation", [face_set])
space_element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSpace")
space_element.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 5))
obj = bpy.data.objects["Cube"]
scene.collection.objects.link(obj)
tool.Ifc.link(space_element, obj)
bpy.context.view_layer.update()
obj.name = "MySpace"
# Check the cube's world bottom Z before regeneration.
bottom_z = (obj.matrix_world @ Vector(obj.bound_box[0])).z
assert np.isclose(bottom_z, 4.5), f"Expected bottom_z=4.5, got {bottom_z}"
# Regenerate the space.
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.bim.generate_space()
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
world_verts = [obj.matrix_world @ v.co for v in mesh.vertices]
world_zs = [v.z for v in world_verts]
assert min(world_zs) >= -0.1, f"Expected space world bottom near z>=0, got {min(world_zs)}"
assert max(world_zs) > 0, f"Expected space to have positive height, got {max(world_zs)}"
assert np.isclose(obj.location.z, 5.0, atol=0.01), f"Expected location.z=5.0, got {obj.location.z}"
class TestGenerateSpaceSlopedRoof(NewFile):
def _create_shed_roof(self, ifc, z=4.0, rise=3.0):
"""Create an IfcRoof whose underside is a sloped plane across the footprint.
Triangular prism: vertical profile (in the y-z plane) extruded along +x.
Profile points (u, v) with placement loc=(-5, 0, z), axis=(1,0,0),
ref=(0,0,1). The local frame maps u to world +z (u=0 -> z, u=rise ->
z+rise) and v to world -y (v=-5 -> y=+5, v=+5 -> y=-5):
(0,-5) -> world (-5, +5, z) eave (low) at north
(rise,-5) -> world (-5, +5, z+rise) vertical edge
(rise,5) -> world (-5, -5, z+rise) ridge at south
The underside is the sloped face from (y=+5, z) to (y=-5, z+rise).
ExtrudedDirection (0,0,1) is local, mapping to world +x; depth 10 spans
x in [-5, 5].
"""
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
roof = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcRoof")
pts = [
ifc.createIfcCartesianPoint((0.0, -5.0)),
ifc.createIfcCartesianPoint((float(rise), -5.0)),
ifc.createIfcCartesianPoint((float(rise), 5.0)),
]
polyline = ifc.createIfcPolyline(pts)
profile = ifc.createIfcArbitraryClosedProfileDef(ProfileType="CURVE", OuterCurve=polyline)
placement = ifc.createIfcAxis2Placement3D(
ifc.createIfcCartesianPoint((-5.0, 0.0, z)),
ifc.createIfcDirection((1.0, 0.0, 0.0)),
ifc.createIfcDirection((0.0, 0.0, 1.0)),
)
extrude_dir = ifc.createIfcDirection((0.0, 0.0, 1.0))
solid = ifc.createIfcExtrudedAreaSolid(profile, placement, extrude_dir, 10.0)
rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [solid])
ifcopenshell.api.geometry.assign_representation(ifc, product=roof, representation=rep)
return roof
def test_generate_space_under_shed_roof(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_thin_wall(ifc, 0.0, 4.8, 10.0, 0.4)
_BlockHelper.create_thin_wall(ifc, 0.0, -4.8, 10.0, 0.4)
_BlockHelper.create_thin_wall(ifc, 4.8, 0.0, 0.4, 10.0)
_BlockHelper.create_thin_wall(ifc, -4.8, 0.0, 0.4, 10.0)
self._create_shed_roof(ifc, z=4.0, rise=3.0)
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
mesh = space.data
assert isinstance(mesh, bpy.types.Mesh)
verts = np.array([v.co for v in mesh.vertices])
min_z = verts[:, 2].min()
max_z = verts[:, 2].max()
assert min_z >= -0.1
assert max_z > 0
top_z_north = max([v[2] for v in verts if v[1] > 1])
top_z_south = max([v[2] for v in verts if v[1] < -1])
assert abs(top_z_north - top_z_south) > 0.05, f"Top should slope along y: {top_z_north} vs {top_z_south}"
class TestSpaceVolumeStrategy(NewFile):
def test_vertical_box_returns_extrude_clip(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
_BlockHelper.create_wall(ifc, height=10.0)
_BlockHelper.create_slab(ifc, z=4.0)
space_polygon = shapely.box(-5, -5, 5, 5)
strategy, top, bottom = subject.get_space_volume_strategy(space_polygon, 0.0, [ifc.by_type("IfcWall")[0]])
assert strategy == "EXTRUDE_CLIP"
assert len(top) == 1
assert len(bottom) == 0
@staticmethod
def _create_sloped_slab(ifc, z=4.0, rise=3.0):
"""Create an IfcSlab whose underside is a sloped plane across the footprint."""
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
slab = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSlab")
pts = [
ifc.createIfcCartesianPoint((0.0, -5.0)),
ifc.createIfcCartesianPoint((float(rise), -5.0)),
ifc.createIfcCartesianPoint((float(rise), 5.0)),
]
polyline = ifc.createIfcPolyline(pts)
profile = ifc.createIfcArbitraryClosedProfileDef(ProfileType="CURVE", OuterCurve=polyline)
placement = ifc.createIfcAxis2Placement3D(
ifc.createIfcCartesianPoint((-5.0, 0.0, z)),
ifc.createIfcDirection((1.0, 0.0, 0.0)),
ifc.createIfcDirection((0.0, 0.0, 1.0)),
)
extrude_dir = ifc.createIfcDirection((0.0, 0.0, 1.0))
solid = ifc.createIfcExtrudedAreaSolid(profile, placement, extrude_dir, 10.0)
rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [solid])
ifcopenshell.api.geometry.assign_representation(ifc, product=slab, representation=rep)
return slab
def test_sloped_slab_returns_extrude_clip(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
self._create_sloped_slab(ifc, z=4.0, rise=3.0)
space_polygon = shapely.box(-5, -5, 5, 5)
strategy, top, bottom = subject.get_space_volume_strategy(space_polygon, 0.0, [])
assert strategy == "EXTRUDE_CLIP"
assert len(top) == 1
assert len(bottom) == 0
class TestRegenerateSpaceFromRealIfc2x3(NewFile):
def load_house_with_garage(self):
filepath = (
Path(__file__).parents[3]
/ "ifcopenshell-python"
/ "test"
/ "IfcRelSpaceBoundary_TestFiles"
/ "IfcRelSpaceBoundary2ndLevel"
/ "HouseWithGarage_AC22_IFC2X3.ifc"
).resolve()
bpy.ops.bim.load_project(filepath=filepath.as_posix())
ifc = tool.Ifc.get()
return ifc
def _regenerate_space(self, ifc, space_id):
space = ifc.by_id(space_id)
obj = tool.Ifc.get_object(space)
assert obj
import numpy as np
original_verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices])
original_bounds = (
original_verts[:, 0].min(),
original_verts[:, 0].max(),
original_verts[:, 1].min(),
original_verts[:, 1].max(),
original_verts[:, 2].min(),
original_verts[:, 2].max(),
)
original_origin = obj.matrix_world.translation.copy()
# Delete existing related IfcRelSpaceBoundary as in the manual repro.
for b in list(space.BoundedBy or []):
ifcopenshell.api.boundary.remove_boundary(ifc, b)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.context.view_layer.update()
# Patch Spatial helpers so generate_space uses the active IfcSpace.
original_get_selected_objects = tool.Spatial.get_selected_objects
original_get_active_obj = tool.Spatial.get_active_obj
try:
tool.Spatial.get_selected_objects = classmethod(lambda cls: [obj])
tool.Spatial.get_active_obj = classmethod(lambda cls: obj)
bpy.ops.bim.generate_space()
finally:
tool.Spatial.get_selected_objects = original_get_selected_objects
tool.Spatial.get_active_obj = original_get_active_obj
regen_verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices])
regen_bounds = (
regen_verts[:, 0].min(),
regen_verts[:, 0].max(),
regen_verts[:, 1].min(),
regen_verts[:, 1].max(),
regen_verts[:, 2].min(),
regen_verts[:, 2].max(),
)
regen_origin = obj.matrix_world.translation.copy()
return (original_bounds, original_origin), (regen_bounds, regen_origin)
def test_regenerate_space_5710_keeps_world_location(self):
ifc = self.load_house_with_garage()
(original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 5710)
assert (regen_origin - original_origin).length < 0.02
for o, r in zip(original_bounds, regen_bounds):
assert r == pytest.approx(o, abs=0.02)
def test_regenerate_space_2363_keeps_world_location(self):
ifc = self.load_house_with_garage()
(original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 2363)
assert (regen_origin - original_origin).length < 0.02
# X and Y stable; Z may differ because the regenerated space detects the
# sloped roof and clips the extrusion.
for j in (0, 1, 2, 3, 4):
assert regen_bounds[j] == pytest.approx(original_bounds[j], abs=0.02)
# Verify the regenerated body contains boolean clipping (roof clipping).
space = ifc.by_id(2363)
body = ifcopenshell.util.representation.get_representation(space, "Model", "Body", "MODEL_VIEW")
assert body is not None
boolean_items = [i for i in (body.Items or []) if i.is_a("IfcBooleanClippingResult")]
assert len(boolean_items) >= 1, "Expected roof clipping but got no boolean result"
def test_regenerate_space_twice_does_not_duplicate_half_spaces(self):
ifc = self.load_house_with_garage()
space = ifc.by_id(2363)
obj = tool.Ifc.get_object(space)
assert obj
for b in list(space.BoundedBy or []):
ifcopenshell.api.boundary.remove_boundary(ifc, b)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.context.view_layer.update()
original_get_selected_objects = tool.Spatial.get_selected_objects
original_get_active_obj = tool.Spatial.get_active_obj
try:
tool.Spatial.get_selected_objects = classmethod(lambda cls: [obj])
tool.Spatial.get_active_obj = classmethod(lambda cls: obj)
bpy.ops.bim.generate_space()
bpy.ops.bim.generate_space()
finally:
tool.Spatial.get_selected_objects = original_get_selected_objects
tool.Spatial.get_active_obj = original_get_active_obj
body_reps = [r for r in (space.Representation.Representations or []) if r.RepresentationIdentifier == "Body"]
assert len(body_reps) == 1
rep = body_reps[0]
boolean_chains = [item for item in rep.Items if item.is_a("IfcBooleanClippingResult")]
assert len(boolean_chains) <= 1
if boolean_chains:
half_space_ids = set()
for item in ifc.traverse(boolean_chains[0]):
if item.is_a("IfcHalfSpaceSolid"):
assert item.id() not in half_space_ids, "Duplicate half-space solid in boolean chain"
half_space_ids.add(item.id())
def test_regenerate_space_after_moving_roof_updates_shape(self):
ifc = self.load_house_with_garage()
space = ifc.by_id(2363)
space_obj = tool.Ifc.get_object(space)
assert space_obj
roof = ifc.by_id(5773)
roof_obj = tool.Ifc.get_object(roof)
assert roof_obj
for b in list(space.BoundedBy or []):
ifcopenshell.api.boundary.remove_boundary(ifc, b)
bpy.context.view_layer.objects.active = space_obj
bpy.ops.object.select_all(action="DESELECT")
space_obj.select_set(True)
bpy.context.view_layer.update()
original_get_selected_objects = tool.Spatial.get_selected_objects
original_get_active_obj = tool.Spatial.get_active_obj
try:
tool.Spatial.get_selected_objects = classmethod(lambda cls: [space_obj])
tool.Spatial.get_active_obj = classmethod(lambda cls: space_obj)
bpy.ops.bim.generate_space()
roof_obj.hide_set(False)
roof_obj.location.z += 1.0
bpy.context.view_layer.update()
tool.Geometry.commit_placement_if_moved(roof_obj)
bpy.ops.bim.generate_space()
finally:
tool.Spatial.get_selected_objects = original_get_selected_objects
tool.Spatial.get_active_obj = original_get_active_obj
body_reps = [r for r in (space.Representation.Representations or []) if r.RepresentationIdentifier == "Body"]
assert len(body_reps) == 1
def test_regenerate_space_is_stable_across_multiple_iterations(self):
"""Regenerating the same space 5+ times must produce identical Z and bounds."""
ifc = self.load_house_with_garage()
space = ifc.by_id(2363)
obj = tool.Ifc.get_object(space)
assert obj
for b in list(space.BoundedBy or []):
ifcopenshell.api.boundary.remove_boundary(ifc, b)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.context.view_layer.update()
original_get_selected_objects = tool.Spatial.get_selected_objects
original_get_active_obj = tool.Spatial.get_active_obj
def snapshot():
verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices], dtype=float)
return (
obj.matrix_world.translation.copy(),
(
float(verts[:, 0].min()),
float(verts[:, 0].max()),
float(verts[:, 1].min()),
float(verts[:, 1].max()),
float(verts[:, 2].min()),
float(verts[:, 2].max()),
),
)
snapshots = []
try:
tool.Spatial.get_selected_objects = classmethod(lambda cls: [obj])
tool.Spatial.get_active_obj = classmethod(lambda cls: obj)
for _ in range(5):
bpy.ops.bim.generate_space()
snapshots.append(snapshot())
finally:
tool.Spatial.get_selected_objects = original_get_selected_objects
tool.Spatial.get_active_obj = original_get_active_obj
ref_origin, ref_bounds = snapshots[0]
for i, (origin, bounds) in enumerate(snapshots[1:], start=1):
assert (
origin - ref_origin
).length < 0.02, f"Iteration {i}: Z drifted from {list(ref_origin)} to {list(origin)}"
for j, (o, r) in enumerate(zip(ref_bounds, bounds)):
assert r == pytest.approx(
o, abs=0.02
), f"Iteration {i} axis {j}: {o} != {r} full ref={ref_bounds} cur={bounds}"
class TestGenerateSpaceLocation(NewFile):
def test_generate_space_at_non_zero_cursor_location(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
# 4 thin walls forming a hollow box around (10, 20).
_BlockHelper.create_thin_wall(ifc, 10.0, 20.0 + 4.8, 10.0, 0.4)
_BlockHelper.create_thin_wall(ifc, 10.0, 20.0 - 4.8, 10.0, 0.4)
_BlockHelper.create_thin_wall(ifc, 10.0 + 4.8, 20.0, 0.4, 10.0)
_BlockHelper.create_thin_wall(ifc, 10.0 - 4.8, 20.0, 0.4, 10.0)
bpy.context.scene.cursor.location = (10, 20, 0)
bpy.ops.bim.generate_space()
space = bpy.data.objects["IfcSpace/Space"]
mesh = space.data
assert isinstance(mesh, bpy.types.Mesh)
world_verts = np.array([space.matrix_world @ v.co for v in mesh.vertices])
center = (world_verts.min(axis=0) + world_verts.max(axis=0)) / 2
assert center[0] == pytest.approx(10.0, abs=0.1)
assert center[1] == pytest.approx(20.0, abs=0.1)
+6 -6
View File
@@ -76,12 +76,12 @@ int main() {
// By adding a building, a hierarchy has been automatically created that consists of the following
// structure: IfcProject > IfcSite > IfcBuilding
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
// project, which has been created automatically.
file.getSingle<IfcSchema::IfcProject>().setName("IfcAdvancedHouse"s);
// To demonstrate the ability to serialize arbitrary opencascade solids a building envelope is
// constructed by applying boolean operations. Naturally, in IFC, building elements should be
// constructed by applying boolean operations. Naturally, in IFC, building elements should be
// modeled separately, with rich parametric and relational semantics. Creating geometry in this
// way does not preserve any history and is merely a demonstration of technical capabilities.
TopoDS_Shape outer = BRepPrimAPI_MakeBox(gp_Pnt(-5000., -180., -2000.), gp_Pnt(5000., 5180., 3000.)).Shape();
@@ -102,7 +102,7 @@ int main() {
// to the IFC4 model and with `advanced` set to `true` which introduces IfcAdvancedFace. It would
// return `0` otherwise.
auto building_shape = ifcopenshell::geom::serialise(file, building_shell, false).as<IfcSchema::IfcProductDefinitionShape>();
file.add_entity(building_shape);
auto building_representations = building_shape.Representations();
building_representations.front().setContextOfItems(file.getRepresentationContext("model"));
@@ -122,7 +122,7 @@ int main() {
ground_representation = ifcopenshell::geom::tesselate(file, shape, 100.);
}
file.getSingle<IfcSchema::IfcSite>().setRepresentation(ground_representation.as<IfcSchema::IfcProductDefinitionShape>());
auto ground_reps = file.getSingle<IfcSchema::IfcSite>().Representation().Representations();
for (auto& rep : ground_reps) {
rep.setContextOfItems(file.getRepresentationContext("Model"));
@@ -175,10 +175,10 @@ void createGroundShape(TopoDS_Shape& shape) {
cv.SetValue(4, 4, gp_Pnt( 10000, 10000, -8130));
TColStd_Array1OfReal knots(0, 1);
knots(0) = 0;
knots(1) = 1;
knots(1) = 1;
TColStd_Array1OfInteger mult(0, 1);
mult(0) = 5;
mult(1) = 5;
mult(1) = 5;
Handle(Geom_BSplineSurface) surf = new Geom_BSplineSurface(cv, knots, knots, mult, mult, 4, 4);
#if OCC_VERSION_HEX < 0x60502
shape = BRepBuilderAPI_MakeFace(surf);
+6 -6
View File
@@ -70,7 +70,7 @@ Schema::IfcProject setup_project(hierarchy_helper<Schema>& file) {
dimensions.setThermodynamicTemperatureExponent(0);
dimensions.setAmountOfSubstanceExponent(0);
dimensions.setLuminousIntensityExponent(0);
auto conversion_factor = file.create<Schema::IfcMeasureWithUnit>();
auto length = file.create<Schema::IfcLengthMeasure>();
length.set_attribute_value(0, 304.80);
@@ -82,7 +82,7 @@ Schema::IfcProject setup_project(hierarchy_helper<Schema>& file) {
conversion_based_unit.setUnitType(Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT);
conversion_based_unit.setName("FEET");
conversion_based_unit.setConversionFactor(conversion_factor);
units.erase(std::remove(units.begin(), units.end(), unit)); // remove the millimeter unit
units.push_back(conversion_based_unit); // add the feet unit
units_in_context.setUnits(units); // update the UnitsInContext
@@ -386,7 +386,7 @@ int main() {
nests_horizontal_segments.setName("Nests horizontal alignment segments with horizontal alignment");
nests_horizontal_segments.setRelatingObject(horizontal_alignment);
nests_horizontal_segments.setRelatedObjects(horizontal_segments);
//
// Create plan view footprint model representation for the horizontal alignment
//
@@ -403,7 +403,7 @@ int main() {
footprint_shape_representation.setRepresentationType("Curve2D");
// the composite curve is a representation item
footprint_shape_representation.setItems({composite_curve});
//
// Define vertical profile segments
//
@@ -539,7 +539,7 @@ int main() {
nests_alignment_layouts.setName("Nest horizontal and vertical alignment layouts with the alignment");
nests_alignment_layouts.setRelatingObject(alignment);
nests_alignment_layouts.setRelatedObjects({horizontal_alignment, vertical_profile});
// Define the relationship with the project
// IFC 4.1.4.1.1 "Every IfcAlignment must be related to IfcProject using the IfcRelAggregates relationship"
@@ -550,7 +550,7 @@ int main() {
aggregate_alignments_with_project.setName("Alignments in project");
aggregate_alignments_with_project.setRelatingObject(project);
aggregate_alignments_with_project.setRelatedObjects({alignment});
// Define the spatial structure of the alignment with respect to the site
// IFC 4.1.5.1 alignment is referenced in spatial structure of an IfcSpatialElement. In this case IfcSite is the highest level IfcSpatialElement
+20 -20
View File
@@ -55,7 +55,7 @@
using namespace std::string_literals;
// Some convenience typedefs and definitions.
// Some convenience typedefs and definitions.
typedef ifcopenshell::global_id guid;
typedef std::pair<double, double> XY;
#ifdef SCHEMA_HAS_IfcPresentationStyleAssignment
@@ -295,9 +295,9 @@ int main() {
west_void.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
west_void.setRelatingBuildingElement(west_wall);
west_void.setRelatedOpeningElement(west_opening_copy);
// Up until now we have only used simple extrusions for the creation of the geometry. For the
// ground mesh of the IfcSite we will use a Nurbs surface created in Open Cascade. The surface
// Up until now we have only used simple extrusions for the creation of the geometry. For the
// ground mesh of the IfcSite we will use a Nurbs surface created in Open Cascade. The surface
// will be tessellated using the deflection specified.
TopoDS_Shape shape;
createGroundShape(shape);
@@ -325,7 +325,7 @@ int main() {
site_prop.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
site_prop.setRelatedObjects({file.getSingle<IfcSchema::IfcSite>()});
site_prop.setRelatingPropertyDefinition(pset);
auto ground_reps = file.getSingle<IfcSchema::IfcSite>().Representation().Representations();
for (auto& rep : ground_reps) {
rep.setContextOfItems(file.getRepresentationContext("Model"));
@@ -334,11 +334,11 @@ int main() {
setSurfaceColour(file,ground_representation, 0.15, 0.25, 0.05);
// According to the Ifc2x3 schema an IfcWallStandardCase needs to have an IfcMaterialLayerSet
// assigned. Note that this material definition is independent of the surface styles we have
// been assigning to the walls already. The surface styles determine the colour in the
// assigned. Note that this material definition is independent of the surface styles we have
// been assigning to the walls already. The surface styles determine the colour in the
// '3D viewport' of most applications.
// Some BIM authoring applications, such as Autodesk Revit, ignore the geometrical representation
// by and large and construct native walls using the layer thickness and reference line offset
// by and large and construct native walls using the layer thickness and reference line offset
// provided here.
auto material = file.create<IfcSchema::IfcMaterial>();
material.setName("Brick");
@@ -422,7 +422,7 @@ int main() {
#endif
door.setRepresentation(file.addBox(80, 80, 2120, IfcSchema::IfcAxis2Placement2D{}, file.addPlacement3d(460, 0, 0)));
auto door_representations = door.Representation().Representations();
IfcSchema::IfcShapeRepresentation door_body;
for (auto& rep : door_representations) {
@@ -465,9 +465,9 @@ int main() {
#endif
// Surface styles are assigned to representation items, hence there is no real limitation to
// assign different colours within the same representation. However, some viewers have
// difficulties rendering products with representation items with different surface styles.
// Therefore we will construct the window as a decomposition of beams and a plate, in which
// assign different colours within the same representation. However, some viewers have
// difficulties rendering products with representation items with different surface styles.
// Therefore we will construct the window as a decomposition of beams and a plate, in which
// only the plate will have a transparent material assigned.
// The window frame will consists of four separate beams.
@@ -476,7 +476,7 @@ int main() {
// match the bounding box of the representation. Furthermore, the window placement needs
// to align with the lowerleft corner of the constituent parts.
std::vector<IfcSchema::IfcShapeRepresentation> frame_representations;
auto horizontal_bar = file.addEmptyRepresentation();
auto vertical_bar = file.addEmptyRepresentation();
file.addBox(horizontal_bar, 1860, 90, 90);
@@ -498,7 +498,7 @@ int main() {
// Because of the duplication the iterator is incremented twice
}
// This window will be placed at five locations within the building. A list of placements is
// This window will be placed at five locations within the building. A list of placements is
// created and is iterated over to create all window instances.
std::vector<IfcSchema::IfcLocalPlacement> window_placements;
window_placements.push_back(file.addLocalPlacement(storey_placement, 2*-1770-430-930, -45, 400));
@@ -506,7 +506,7 @@ int main() {
window_placements.push_back(file.addLocalPlacement(storey_placement, -430-930, -45, 400));
window_placements.push_back(file.addLocalPlacement(storey_placement, 3000-930, -45, 400));
window_placements.push_back(file.addLocalPlacement(storey_placement, -4855+45, 885-930, 400, 0, 0, 1, 0, 1, 0));
for (auto& place : window_placements) {
// Create the window at the current location
@@ -520,7 +520,7 @@ int main() {
window.setPredefinedType(IfcSchema::IfcWindowTypeEnum::IfcWindowType_WINDOW);
window.setPartitioningType(IfcSchema::IfcWindowTypePartitioningEnum::IfcWindowTypePartitioning_SINGLE_PANEL);
#endif
file.addBuildingProduct(window);
file.addBuildingProduct(window);
// Initialize a list of parts for the window to be composed of
std::vector<IfcSchema::IfcObjectDefinition> window_parts;
@@ -532,7 +532,7 @@ int main() {
frame_placements.push_back(file.addLocalPlacement(storey_placement, 930, 45, 1510));
frame_placements.push_back(file.addLocalPlacement(storey_placement, -885+930, 45, 90));
frame_placements.push_back(file.addLocalPlacement(storey_placement, 885+930, 45, 90));
// Now iterate over the placements and representations of the beam and add them to list of parts
std::vector<IfcSchema::IfcLocalPlacement>::const_iterator frame_placement;
std::vector<IfcSchema::IfcShapeRepresentation>::const_iterator frame_representation;
@@ -565,7 +565,7 @@ int main() {
window_parts.push_back(glass_part);
file.relatePlacements(window, glass_part);
setSurfaceColour(file, glass_part.Representation(), 0.6, 0.7, 0.75, 0.1);
// Now create a decomposition relation between the window and the parts. Most viewers and authoring
// tools will consider the window a single entity that can be selected as a whole.
{
@@ -612,10 +612,10 @@ void createGroundShape(TopoDS_Shape& shape) {
cv.SetValue(4, 4, gp_Pnt( 10000, 10000, -8130));
TColStd_Array1OfReal knots(0, 1);
knots(0) = 0;
knots(1) = 1;
knots(1) = 1;
TColStd_Array1OfInteger mult(0, 1);
mult(0) = 5;
mult(1) = 5;
mult(1) = 5;
Handle(Geom_BSplineSurface) surf = new Geom_BSplineSurface(cv, knots, knots, mult, mult, 4, 4);
#if OCC_VERSION_HEX < 0x60502
shape = BRepBuilderAPI_MakeFace(surf);
+2 -2
View File
@@ -30,7 +30,7 @@
#include INCLUDE_SCHEMA(ifcparse/schemas, IfcSchema)
#include INCLUDE_SCHEMA_DEFINITIONS(ifcparse/schemas, IfcSchema)
#ifdef _MSC_VER
#ifdef _MSC_VER
#define strcasecmp _stricmp
#endif
@@ -151,7 +151,7 @@ void process_pset(element_properties& props, const T& inst) {
template <typename Schema>
void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefinition& inst) {
// Extracts the property definitions for an IFC instance.
// Extracts the property definitions for an IFC instance.
if (auto tyob = inst.template as<typename Schema::IfcTypeObject>()) {
if (tyob.HasPropertySets()) {
auto defs = *tyob.HasPropertySets();
+28 -28
View File
@@ -336,7 +336,7 @@ int main(int argc, char** argv) {
std::string exterior_only_algo;
ifcopenshell::geom::settings settings;
po::options_description geom_options("Geometry options");
geom_options.add_options()
("kernel", po::value<std::string>(&geometry_kernel)->default_value(default_kernel),
@@ -389,7 +389,7 @@ int main(int argc, char** argv) {
("model", "Specifies whether to include surfaces and solids in the output result. "
"Typically these are representations of type Body or Facetation. ")
;
settings.define_options(geom_options);
std::string bounds;
@@ -466,7 +466,7 @@ int main(int argc, char** argv) {
num_threads = std::thread::hardware_concurrency();
logger.notice("SYS", 7, "Using " + std::to_string(num_threads) + " threads");
}
if (vmap.count("log-format") == 1) {
boost::to_lower(log_format);
if (log_format == "plain") {
@@ -479,7 +479,7 @@ int main(int argc, char** argv) {
return EXIT_FAILURE;
}
}
if (!filter_filename.empty()) {
size_t num_filters = read_filters_from_file(ifcopenshell::path::to_utf8(filter_filename), include_filter, include_traverse_filter, exclude_filter, exclude_traverse_filter);
if (num_filters) {
@@ -524,10 +524,10 @@ int main(int argc, char** argv) {
// If no output filename is specified a Wavefront OBJ file will be output
// to maintain backwards compatibility with the obsolete IfcObj executable.
const path_t output_filename = vmap.count("output-file") == 1
const path_t output_filename = vmap.count("output-file") == 1
? vmap["output-file"].as<path_t>()
: change_extension(input_filename, ifcopenshell::path::from_utf8(DEFAULT_EXTENSION));
if (output_filename.size() < 5) {
cerr_ << "[error] Invalid or unsupported output file '" << output_filename << "' given" << std::endl;
print_usage();
@@ -572,13 +572,13 @@ int main(int argc, char** argv) {
}
path_t output_temp_filename = output_filename + ifcopenshell::path::from_utf8(TEMP_FILE_EXTENSION);
std::vector<path_t> tokens;
split(tokens, output_filename, boost::is_any_of("."));
std::vector<path_t>::iterator tok_iter;
path_t ext = *(tokens.end() - 1);
path_t dot;
dot = '.';
dot = '.';
path_t output_extension = dot + ext;
boost::to_lower(output_extension);
@@ -785,7 +785,7 @@ int main(int argc, char** argv) {
time_t start,end;
time(&start);
// @nb last argument true -> bypass_properties which are not read by any of the geometry serializers
// Document serializers and IFC are already special-cased above
// SVG requires properties for IfcAnnotation/DRAWING properties
@@ -839,12 +839,12 @@ int main(int argc, char** argv) {
settings.get<ifcopenshell::geom::settings::ModelOffset>().value = offset;
}
if (is_tesselated && (center_model || center_model_geometry)) {
std::vector<double> offset(3);
ifcopenshell::geom::iterator tmp_context_iterator(ifcopenshell::geom::kernels::construct(ifc_file, geometry_kernel, settings, logger), settings, ifc_file, filter_funcs, num_threads, logger);
time_t bounds_start, bounds_end;
time(&bounds_start);
if (!quiet) logger.status("Computing bounds...");
@@ -860,7 +860,7 @@ int main(int argc, char** argv) {
return EXIT_FAILURE;
}
}
tmp_context_iterator.compute_bounds(center_model_geometry);
time(&bounds_end);
@@ -919,19 +919,19 @@ int main(int argc, char** argv) {
}
// The functions ifcopenshell::geom::iterator::get() and ifcopenshell::geom::iterator::next()
// wrap an iterator of all geometrical products in the Ifc file.
// wrap an iterator of all geometrical products in the Ifc file.
// ifcopenshell::geom::iterator::get() returns an ifcopenshell::geom::triangulation_element or
// -native_element pointer, based on current settings. (see iterator.h
// for definition) ifcopenshell::geom::iterator::next() is used to poll whether more
// geometrical entities are available. None of these functions throw
// exceptions, neither for parsing errors or geometrical errors. Upon
// calling next() the entity to be returned has already been processed, a
// non-null return value guarantees that a successfully processed product is
// available.
// geometrical entities are available. None of these functions throw
// exceptions, neither for parsing errors or geometrical errors. Upon
// calling next() the entity to be returned has already been processed, a
// non-null return value guarantees that a successfully processed product is
// available.
size_t num_created = 0;
while (true) {
auto geom_object = context_iterator->get();
if (is_tesselated)
@@ -967,7 +967,7 @@ int main(int argc, char** argv) {
if (!context_iterator->next()) {
break;
}
}
}
if (!no_progress && quiet) {
for (; old_progress < 100; ++old_progress) {
cout_ << ".";
@@ -1086,7 +1086,7 @@ bool init_input_file(const std::string& filename, ifcopenshell::file*& ifc_file,
ifc_file->bypass_type("IfcProfileProperties");
ifc_file->bypass_type("IfcPhysicalQuantity");
}
#ifdef USE_MMAP
if (mmap) {
ifc_file->initialize(filename, mmap);
@@ -1382,20 +1382,20 @@ void fix_quantities(ifcopenshell::file& f, bool no_progress, bool quiet, bool st
auto person = latebound_access::create(f, "IfcPerson");
latebound_access::set(person, "FamilyName", std::string("IfcOpenShell"));
latebound_access::set(person, "GivenName", std::string("IfcOpenShell"));
auto org = latebound_access::create(f, "IfcOrganization");
latebound_access::set(org, "Name", std::string("IfcOpenShell"));
auto pando = latebound_access::create(f, "IfcPersonAndOrganization");
latebound_access::set(pando, "ThePerson", person);
latebound_access::set(pando, "TheOrganization", org);
auto application = latebound_access::create(f, "IfcApplication");
latebound_access::set(application, "ApplicationDeveloper", org);
latebound_access::set(application, "Version", std::string(IFCOPENSHELL_VERSION));
latebound_access::set(application, "ApplicationFullName", std::string("IfcConvert"));
latebound_access::set(application, "ApplicationIdentifier", std::string("IfcConvert") + IFCOPENSHELL_VERSION);
auto ownerhist = latebound_access::create(f, "IfcOwnerHistory");
latebound_access::set(ownerhist, "OwningUser", pando);
latebound_access::set(ownerhist, "OwningApplication", application);
@@ -1440,7 +1440,7 @@ void fix_quantities(ifcopenshell::file& f, bool no_progress, bool quiet, bool st
latebound_access::set(quantity_area, "AreaValue", a);
quantities.push_back(quantity_area);
}
if (geom_object->geometry().calculate_volume(a)) {
auto quantity_volume = latebound_access::create(f, "IfcQuantityVolume");
latebound_access::set(quantity_volume, "Name", std::string("Volume"));
@@ -1461,13 +1461,13 @@ void fix_quantities(ifcopenshell::file& f, bool no_progress, bool quiet, bool st
std::vector<express::base> quantities_2;
for (auto& part : geom_object->geometry()) {
for (auto& part : geom_object->geometry()) {
auto quantity_count = latebound_access::create(f, "IfcQuantityCount");
latebound_access::set(quantity_count, "Name", std::string("Surface Genus"));
latebound_access::set(quantity_count, "Description", '#' + boost::lexical_cast<std::string>(part.ItemId()));
latebound_access::set(quantity_count, "CountValue", (int64_t) part.shape()->surface_genus());
quantities_2.push_back(quantity_count);
quantities_2.push_back(quantity_count);
}
latebound_access::set(quantity_complex, "HasQuantities", quantities_2);
@@ -41,7 +41,7 @@ void fix_storeycontainment(ifcopenshell::file& f, bool no_progress, bool quiet,
elem_to_storey[*it] = storey;
}
}
});
});
auto storeys = f.instances_by_type("IfcBuildingStorey");
std::vector<const ifcopenshell::IfcBaseClass*> storeys_sorted(storeys->begin(), storeys->end());
@@ -98,7 +98,7 @@ void fix_storeycontainment(ifcopenshell::file& f, bool no_progress, bool quiet,
std::wcout << "---" << std::endl;
}
*/
if (!context_iterator.initialize()) {
return;
}
@@ -22,7 +22,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
settings.get<ifcopenshell::geom::settings::DisableOpeningSubtractions>().value = true;
settings.get<ifcopenshell::geom::settings::OutputDimensionality>().value = ifcopenshell::geom::settings::CURVES;
ifcopenshell::geom::converter c(ifcopenshell::geom::kernels::construct(&f, "cgal", settings, logger), &f, settings, logger);
auto rels = f.instances_by_type("IfcRelConnectsPathElements");
@@ -54,7 +54,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
if (!a_is_relating) {
std::swap(a_type, b_type);
}
}
}
#if 0
auto a_poly = ifcopenshell::geom::utils::create_polyhedron(a.handle()->second);
@@ -123,7 +123,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
} else {
auto p0 = boost::get<taxonomy::point3::ptr>(first_vertex);
auto p1 = boost::get<taxonomy::point3::ptr>(last_vertex);
auto v0 = taxonomy::cast<taxonomy::geom_item>(item)->matrix->ccomponents() * p0->ccomponents().homogeneous();
auto v1 = taxonomy::cast<taxonomy::geom_item>(item)->matrix->ccomponents() * p1->ccomponents().homogeneous();
@@ -142,7 +142,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
auto pit = std::minmax_element(parameters.begin(), parameters.end());
return std::make_pair(len, std::make_pair(CGAL::to_double(*pit.first), CGAL::to_double(*pit.second)));
}
}
}
const auto& nan = std::numeric_limits<double>::quiet_NaN();
return std::make_pair(nan, std::make_pair(nan, nan));
+2 -2
View File
@@ -37,7 +37,7 @@ inline static bool ALMOST_THE_SAME(const T& a, const T& b, double tolerance = AL
return fabs(a - b) < tolerance;
}
namespace ifcopenshell {
namespace ifcopenshell {
#if defined(_MSC_VER)
#pragma warning(push)
@@ -70,7 +70,7 @@ namespace ifcopenshell {
public:
bool propagate_exceptions = false;
bool partial_success_is_success = true;
abstract_kernel(const std::string& geometry_library, const ifcopenshell::geom::settings& settings, ifcopenshell::logger& logger = ifcopenshell::logger::root())
: geometry_library_(geometry_library)
, settings_(settings)
+2 -2
View File
@@ -44,7 +44,7 @@ namespace geom {
/// should return true if the geometry for the product is wanted to be included in the output.
/// http://www.boost.org/doc/libs/1_62_0/doc/html/function/tutorial.html
typedef std::function<bool(const express::base&)> filter_function;
class IFC_GEOM_API abstract_mapping {
protected:
ifcopenshell::geom::settings settings_;
@@ -106,7 +106,7 @@ namespace geom {
IFC_GEOM_API mapping_factory_implementation& mapping_implementations();
}
}
}
+6 -6
View File
@@ -510,20 +510,20 @@ namespace ifcopenshell::geom {
virtual void triangulate(ifcopenshell::geom::settings settings, const ifcopenshell::geom::taxonomy::matrix4& place, triangulation* t, int item_id, int surface_style_id, ifcopenshell::logger& logger = ifcopenshell::logger::root()) const = 0;
ifcopenshell::geom::triangulation* triangulate(const ifcopenshell::geom::settings& settings, ifcopenshell::logger& logger = ifcopenshell::logger::root()) const;
virtual void serialize(const ifcopenshell::geom::taxonomy::matrix4& place, std::string&) const = 0;
virtual int surface_genus() const = 0;
virtual bool is_manifold() const = 0;
virtual int num_vertices() const = 0;
virtual int num_edges() const = 0;
virtual int num_faces() const = 0;
// @todo choose one prototype
virtual double bounding_box(void*&) const = 0;
// @todo this must be something with a virtual dtor so that we can delete it.
virtual std::pair<opaque_coordinate<3>, opaque_coordinate<3>> bounding_box() const = 0;
virtual void set_box(void* b) = 0;
virtual opaque_number length() = 0;
virtual opaque_number area() = 0;
virtual opaque_number volume() = 0;
@@ -550,11 +550,11 @@ namespace ifcopenshell::geom {
virtual std::size_t map(opaque_coordinate<4>& from, opaque_coordinate<4>& to) = 0;
virtual std::size_t map(const std::vector<opaque_coordinate<4>>& from, const std::vector<opaque_coordinate<4>>& to) = 0;
virtual conversion_result_shape* moved(ifcopenshell::geom::taxonomy::matrix4::ptr) const = 0;
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geom::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const = 0;
virtual ~conversion_result_shape() {}
};
class IFC_GEOM_API conversion_result {
+1 -1
View File
@@ -498,7 +498,7 @@ namespace ifcopenshell {
static constexpr const char* const description = "Slight variation of --model-offset where large offsets are applied by negating existing large offsets to retain maximum precision. Requires --no-parallel-mapping.";
};
}
namespace impl {
template <typename T>
struct readable_name {
+3 -3
View File
@@ -19,7 +19,7 @@ ifcopenshell::geom::converter::~converter() {
ifcopenshell::geom::native_element* ifcopenshell::geom::converter::create_brep_for_representation_and_product(taxonomy::ptr representation_node, const express::base product_, const taxonomy::matrix4::ptr& place_) {
auto product = product_.as<express::entity>();
std::stringstream representation_id_builder;
auto place = place_;
@@ -32,7 +32,7 @@ ifcopenshell::geom::native_element* ifcopenshell::geom::converter::create_brep_f
if (!kernel_->convert(representation_node, shapes)) {
return 0;
}
if (settings_.get<ifcopenshell::geom::settings::ApplyLayerSets>().get()) {
ifcopenshell::geom::layerset_information layerinfo;
std::vector<ifcopenshell::geom::endpoint_connection> neighbours;
@@ -55,7 +55,7 @@ ifcopenshell::geom::native_element* ifcopenshell::geom::converter::create_brep_f
/*
if (util::flatten_shape_list(shapes, merge, false, getValue(GV_PRECISION))) {
if (util::count(merge, TopAbs_FACE) > 0) {
if (convert_layerset(product, layers, styles, thickness)) {
IfcSchema::IfcRelAssociates::list::ptr associations = product->HasAssociations();
+1 -1
View File
@@ -27,7 +27,7 @@ namespace ifcopenshell { namespace geom {
ifcopenshell::geom::kernels::abstract_kernel* kernel() { return &*kernel_; }
converter(std::unique_ptr<ifcopenshell::geom::kernels::abstract_kernel>&& geometry_library, ifcopenshell::file* file, ifcopenshell::geom::settings& settings, ifcopenshell::logger& logger = ifcopenshell::logger::root());
~converter();
ifcopenshell::geom::abstract_mapping* mapping() const { return mapping_; }
+2 -2
View File
@@ -53,7 +53,7 @@ namespace ifcopenshell::geom {
}
}
const ifcopenshell::geom::taxonomy::matrix4::ptr& data() const {
if (matrix_orig_units_) {
if (matrix_orig_units_) {
return matrix_orig_units_;
}
if (matrix_) {
@@ -131,7 +131,7 @@ namespace ifcopenshell::geom {
const std::string& guid, const std::string& context, const ifcopenshell::geom::taxonomy::matrix4::ptr& trsf, const express::entity& product)
: _id(id), _parent_id(parent_id), _name(name), _type(type), _guid(guid), _context(context), _transformation(settings, trsf)
, product_(product)
{
{
std::ostringstream oss;
if (type == "IfcProject") {
+1 -1
View File
@@ -1,2 +1,2 @@
// A purposely empty file so that the unrolled loop
// can overflow into an existing empty include file.
// can overflow into an existing empty include file.
+1 -1
View File
@@ -135,7 +135,7 @@ struct cant_fn_evaluator : public fn_evaluator {
auto g = gradient_evaluator_.evaluate(u);
auto c = cant_evaluator_.evaluate(u);
// curvature is stored in row 3 - capture it and remove it from the xy and uz matrices
// so the matrix operations (ie multiplication) works correctly
auto gradient_curvature = g.row(3);
+2 -2
View File
@@ -13,8 +13,8 @@ IFC_GEOM_API std::vector<double> helmert_curve_point(double A0, double A1, doubl
/// This is intended to be used from python side. Polylines are mapped to a loop, but when
/// representing an alignment they need to be a function_item so the can be evaluated by function_item_evaluator.
/// On the C++ side, the dcast operator take care of this, but dcast is not accessible on the python side.
/// @param loop
/// @return
/// @param loop
/// @return
inline taxonomy::function_item::ptr convert_loop_to_function_item(taxonomy::loop::ptr loop) {
return ifcopenshell::geom::taxonomy::dcast<taxonomy::function_item>(loop);
}
+4 -4
View File
@@ -78,7 +78,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
while (dist_along > *(profile_index + 1)) {
profile_index++;
if (profile_index == longitudes.end()) {
// @todo handle this?
// @todo handle this?
}
}
@@ -158,7 +158,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
}
interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along);
}
auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along);
if (rotation_a == rotation_b && rotation_a) {
// @todo we don't support an overridden rotation on only one of the placements
@@ -215,7 +215,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
std::vector<taxonomy::point3::ptr> points;
std::vector<std::set<std::string>> tags;
std::vector<std::string>::const_iterator tag_it;
if (!loop->closed.value_or(false)) {
points = {std::get<taxonomy::point3::ptr>(loop->children[0]->start)};
if (input_tags) {
@@ -352,7 +352,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
for (auto& x : tags_for_this_point_on_subsequent_profile) {
points.push_back(taxonomy::make<taxonomy::point3>(p3));
common_tags_vec.push_back(x);
}
}
}
} else {
for (auto tmp__ : boost::combine(w1_points, w2_points)) {
+1 -1
View File
@@ -9,7 +9,7 @@
namespace ifcopenshell {
namespace geom {
struct IFC_GEOM_API cross_section {
double dist_along;
taxonomy::geom_item::ptr section_geometry;
+4 -4
View File
@@ -624,7 +624,7 @@ std::unique_ptr<ifcopenshell::geom::element> ifcopenshell::geom::iterator::get()
hasParent = false;
}
}
// Add the previously found parent to the vector
hasParent = hasParent && parent_object->parent_id() != -1;
}
@@ -688,13 +688,13 @@ express::base ifcopenshell::geom::iterator::create() {
}
ifcopenshell::geom::taxonomy::direction3::ptr ifcopenshell::geom::iterator::remove_offset_() {
using namespace ifcopenshell::geom::taxonomy;
if (!settings_.get<ifcopenshell::geom::settings::MaxOffset>().has()) {
return nullptr;
}
if (!settings_.get<ifcopenshell::geom::settings::NoParallelMapping>().get()) {
throw std::runtime_error("remove_offset() can only be called with defer-processing-first-element and no-parallel-mapping settings");
}
+2 -2
View File
@@ -128,7 +128,7 @@ namespace ifcopenshell::geom {
bool task_result_ptr_initialized = false;
bool task_result_ptr_exhausted = false;
size_t async_elements_returned_ = 0;
ifcopenshell::geom::settings settings_;
ifcopenshell::file* ifc_file;
std::vector<ifcopenshell::geom::filter_function> filters_;
@@ -138,7 +138,7 @@ namespace ifcopenshell::geom {
// When single-threaded
ifcopenshell::geom::converter* converter_;
// When multi-threaded
std::vector<ifcopenshell::geom::converter*> kernel_pool;
std::vector<std::unique_ptr<ifcopenshell::logger>> worker_loggers_;
@@ -296,7 +296,7 @@ ifcopenshell::geom::cgal_shape::cgal_shape(const cgal_polyhedron& shape, bool co
};
std::vector<CGAL::Point_2<kernel_>> ps;
for (auto& he1 : CGAL::halfedges_around_face(face->halfedge(), poly)) {
const auto& source = he1->vertex()->point();
ps.push_back(transform_point(source));
@@ -345,7 +345,7 @@ void ifcopenshell::geom::cgal_shape::to_poly() const {
CGAL::Polygon_mesh_processing::orient_to_bound_a_volume(poly);
}
shape_ = poly;
// nef_->convert_to_polyhedron(*shape_);
}
}
@@ -384,7 +384,7 @@ void ifcopenshell::geom::cgal_shape::triangulate(ifcopenshell::geom::settings se
}
const bool setting_use_original_edges = settings.get<ifcopenshell::geom::settings::CgalEmitOriginalEdges>().get();
std::set<std::set<kernel_::Point_3>> original_edges;
if (setting_use_original_edges) {
for (auto it = shape_to_use->edges_begin(); it != shape_to_use->edges_end(); ++it) {
@@ -457,7 +457,7 @@ void ifcopenshell::geom::cgal_shape::triangulate(ifcopenshell::geom::settings se
// std::map<cgal_vertex_descriptor, kernel_::Vector_3> vertex_normals;
// boost::associative_property_map<std::map<cgal_vertex_descriptor, kernel_::Vector_3>> vertex_normals_map(vertex_normals);
// triangulate the shape and compute the normals
std::map<facet_const_handle, kernel_::Vector_3> face_normals;
boost::associative_property_map<std::map<facet_const_handle, kernel_::Vector_3>> face_normals_map(face_normals);
@@ -556,7 +556,7 @@ void ifcopenshell::geom::cgal_shape::triangulate(ifcopenshell::geom::settings se
is_face_boundary[i] = setting_use_original_edges
? original_edges.find({ current_halfedge->vertex()->point(), current_halfedge->prev()->vertex()->point() }) != original_edges.end()
: facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()];
++i;
++num_vertices;
++current_halfedge;
@@ -764,7 +764,7 @@ opaque_coordinate<3> ifcopenshell::geom::cgal_shape::position()
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(static_cast<double>(std::distance(shp.points_begin(), shp.points_end())));
for (int i = 0; i < 3; ++i) {
+9 -9
View File
@@ -84,7 +84,7 @@ CGAL::Polyhedron_3<kernel_> ifcopenshell::geom::utils::create_polyhedron(std::li
// fresult.close();
return CGAL::Polyhedron_3<kernel_>();
}
// std::cout << "After: " << polyhedron.size_of_vertices() << " vertices and " << polyhedron.size_of_facets() << " facets" << std::endl;
return polyhedron;
@@ -223,7 +223,7 @@ bool cgal_kernel::convert(const taxonomy::shell::ptr l, cgal_polyhedron& shape)
} else {
logger().message(ifcopenshell::logger::LOG_ERROR, "Failed to convert face:", f->instance);
return false;
}
}
}
// std::cout << "Face in ConnectedFaceSet: " << std::endl;
@@ -673,9 +673,9 @@ namespace {
namespace {
void face_to_poly_with_holes(const cgal_face& face, CGAL::Polygon_with_holes_2<kernel_>& pwh, CGAL::Aff_transformation_3<kernel_>& place) {
// static
// static
kernel_::Vector_3 Z(0, 0, 1);
// static
// static
kernel_::Vector_3 X(1, 0, 0);
auto refz = newell(face.outer);
@@ -912,7 +912,7 @@ bool ifcopenshell::geom::kernels::cgal_kernel::convert_openings(const express::b
#else
CGAL::Nef_nary_union_3<CGAL::Nef_polyhedron_3<kernel_>> second_operand_collector;
size_t second_operand_collector_size = 0;
std::list<std::pair<express::base, std::list<cgal_polyhedron>>> operands;
std::list<express::base> second_operand_instances;
@@ -1483,7 +1483,7 @@ bool cgal_kernel::preprocess_boolean_operand(const express::base& log_reference,
for (auto& nef : first_operands_nef) {
// @todo eliminate this copy (= to remove const)
auto nef_copy = nef;
auto tree = build_halfspace_tree_decomposed(nef_copy, planes_fixed);
auto tree = build_halfspace_tree_decomposed(nef_copy, planes_fixed);
}
{
// @nb we snap internally as well...
@@ -1551,7 +1551,7 @@ bool cgal_kernel::preprocess_boolean_operand(const express::base& log_reference,
}
}
/*
{
@@ -2026,7 +2026,7 @@ bool cgal_kernel::convert_impl(const taxonomy::boolean_result::ptr br, std::vect
if (!convert(face, fs) || fs.size() != 1) {
return false;
}
auto& w = fs.front().outer;
CGAL::Polygon_2<kernel_> ps;
for (auto& wire_point : w) {
@@ -2037,7 +2037,7 @@ bool cgal_kernel::convert_impl(const taxonomy::boolean_result::ptr br, std::vect
continue;
}
// static
// static
auto z = taxonomy::make<taxonomy::direction3>(0, 0, 1);
cgal_polyhedron poly;
process_extrusion(fs.front(), z, 200, poly);
+1 -1
View File
@@ -74,7 +74,7 @@ namespace ifcopenshell {
class IFC_GEOMLIBRARY_API cgal_kernel : public abstract_kernel {
private:
#ifndef IFOPSH_SIMPLE_KERNEL
enum boolean_operand_preprocess {
enum boolean_operand_preprocess {
PP_MINKOWSKY_DILATE,
PP_SNAP_POINTS_TO_FIRST_OPERAND,
PP_SNAP_PLANES_TO_FIRST_OPERAND,
@@ -95,7 +95,7 @@ std::string dump_facet(typename CGAL::Nef_polyhedron_3<Kernel>::Halffacet_const_
const auto& p = h->plane();
oss << "Facet plane=" << p << std::endl;
auto fc = h->facet_cycles_begin();
auto se = shalfedge_const_handle(fc);
CGAL_assertion(se != 0);
@@ -187,7 +187,7 @@ plane_map<Kernel> snap_halfspaces(const std::list<CGAL::Plane_3<Kernel>>& planes
fuzzy_sphere fs(query, search_radius, 0.);
// std::cout << "q " << query << std::endl;
std::list<point_d> results_pos, results_neg;
kdtree.search(std::back_inserter(results_pos), fs);
@@ -801,7 +801,7 @@ void bfs(graph<Kernel>& g, size_t start_vertex, Fn& fn) {
for (boost::tie(ei, ei_end) = boost::out_edges(cur, g); ei != ei_end; ++ei) {
auto s = boost::source(*ei, g);
auto t = boost::target(*ei, g);
// @todo is this necessary?
if (cur == t) {
std::swap(s, t);
@@ -863,10 +863,10 @@ std::unique_ptr<halfspace_tree<TreeKernel>> build_halfspace_tree(graph<Kernel>&
int largest_component_idx = -1;
int num_components = 0;
// @nb we don't just randomly start from an arbitrary seed, but we sort planes by d / | abc |
// for (size_t i = 0; i < boost::num_vertices(sub_graph_0); ++i) {
std::vector<size_t> sorted_verts;
for (size_t i = 0; i < boost::num_vertices(sub_graph_0); ++i) {
sorted_verts.push_back(i);
@@ -1232,7 +1232,7 @@ std::unique_ptr<halfspace_tree<TreeKernel>> build_halfspace_tree_decomposed(cons
// directly, so for now we need to isolate the individual volumes.
CGAL::Polyhedron_3<Kernel> P;
poly.convert_inner_shell_to_polyhedron(ci->shells_begin(), P);
CGAL::Nef_polyhedron_3<Kernel> Pnef(P);
CGAL::Nef_polyhedron_3<Kernel> Pnef(P);
for (auto it = Pnef.halffacets_begin(); it != Pnef.halffacets_end(); ++it) {
if (it->incident_volume()->mark()) {
@@ -1315,7 +1315,7 @@ size_t edge_contract(graph<Kernel>& G) {
bool exists = boost::edge(srcid, tt, G).second;
if (!exists) {
boost::add_edge(srcid, tt, G);
}
}
}
}
++n;
@@ -19,7 +19,7 @@ struct IFC_GEOMLIBRARY_API manifold_part {
auto copy = s;
copy.CalculateNormals(3);
mesh = copy.GetMeshGL64();
solid = s;
solid = s;
}
manifold_part(const manifold::MeshGL64& s) : mesh(s) {}
@@ -114,10 +114,10 @@ namespace {
mesh_type build() const {
mesh_type mesh;
mesh.numProp = 3;
std::vector<size_t> vertex_use_count(vertices.size(), 0);
std::vector<Eigen::Vector3d> vertex_normals(vertices.size(), Eigen::Vector3d::Zero());
for (size_t i = 0; i < tri_verts.size(); i += 3) {
for (size_t j = 0; j < 3; ++j) {
vertex_use_count[tri_verts[i + j]]++;
@@ -1242,13 +1242,13 @@ namespace {
}
std::optional<part> part_from_halfspace_solid(halfspace_build_state& state, const taxonomy::solid::ptr& solid, const taxonomy::face::ptr& face,const manifold::Box& reference_box, double precision, double dilation) {
auto plane = taxonomy::cast<taxonomy::plane>(face->basis);
// @todo verify order
const auto transform = matrix_or_identity(solid->matrix) * matrix_or_identity(plane->matrix);
const auto extrusion_dir = matrix_or_identity(solid->matrix).col(2).head<3>().eval();
Eigen::Vector3d x = transform.col(0).head<3>();
Eigen::Vector3d y = transform.col(1).head<3>();
Eigen::Vector3d normal = transform.col(2).head<3>();
@@ -1281,7 +1281,7 @@ namespace {
const auto delta = corner - origin;
const auto u = delta.dot(x);
const auto v = delta.dot(y);
u_min = std::min(u_min, u);
u_max = std::max(u_max, u);
v_min = std::min(v_min, v);
@@ -58,7 +58,7 @@ double ifcopenshell::geom::util::min_edge_length(const TopoDS_Shape & a) {
TopExp_Explorer exp(a, TopAbs_EDGE);
for (; exp.More(); exp.Next()) {
const TopoDS_Edge& e = TopoDS::Edge(exp.Current());
TopoDS_Vertex v0, v1;
TopExp::Vertices(e, v0, v1);
if (!v0.IsNull() && !v1.IsNull() && v0.IsSame(v1)) {
+10 -10
View File
@@ -43,8 +43,8 @@ bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
// With minor modifications to use gp_Vec type.
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
double& at, double& au, double& av,
bool cull, float enlarge) {
// Find vectors for two edges sharing vert0
@@ -147,7 +147,7 @@ void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
const double Denom = ADotA*BDotB - ADotB*ADotB;
double t; // We will clamp result so t is on the segment (p, a)
if(Denom!=0.0)
if(Denom!=0.0)
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
else
t = 0.0;
@@ -268,7 +268,7 @@ double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<
if(Tp[2]>Tp[index]) index = 2;
}
if(index >= 0)
if(index >= 0)
{
shown_disjoint = true;
@@ -297,7 +297,7 @@ double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
double Tnl = Tn.Dot(Tn);
if(Tnl>1e-15f)
{
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
@@ -317,7 +317,7 @@ double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<
}
if(index >= 0)
{
{
shown_disjoint = true;
const gp_Vec& pIndex = p[index];
@@ -525,11 +525,11 @@ bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_V
const double third = (1.0 / 3.0);
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
//difference is not very big and the coplanar case is not expected to be the most common case
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
return true;
@@ -557,7 +557,7 @@ bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, co
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
double p2_d = -a2.Dot(p2_n);
// const PxPlane p2(a2, b2, c2);
@@ -566,7 +566,7 @@ bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, co
const double p2ToC = c1.Dot(p2_n) + p2_d;
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
const double l2 = intersectionDirection.SquareMagnitude();
@@ -20,7 +20,7 @@ struct IFC_GEOMLIBRARY_API box {
IFC_GEOMLIBRARY_API bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
IFC_GEOMLIBRARY_API bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
double& at, double& au, double& av,
bool cull, float enlarge=0.0f);
@@ -35,7 +35,7 @@ bool open_cascade_kernel::convert(const taxonomy::extrusion::ptr extrusion, Topo
if (face.ShapeType() == TopAbs_COMPOUND) {
// For compounds (most likely the result of a IfcCompositeProfileDef)
// For compounds (most likely the result of a IfcCompositeProfileDef)
// create a compound solid shape.
TopExp_Explorer exp(face, TopAbs_FACE);
+1 -1
View File
@@ -247,7 +247,7 @@ namespace {
auto crv = get_curve(e->basis);
result = Handle(Geom_Surface)(new Geom_SurfaceOfRevolution(
crv, ax
crv, ax
));
result->Transform(tr);
@@ -70,7 +70,7 @@ namespace ifcopenshell::geom {
std::pair<wire_it, wire_it> inner_wires() const {
return { wires_.begin() + 1, wires_.end() };
}
};
};
}
}
+5 -5
View File
@@ -163,7 +163,7 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
return false;
}
}
NCollection_List<TopoDS_Shape> faces;
TopoDS_Compound comp;
BRep_Builder BB;
@@ -330,7 +330,7 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
std::array<std::vector<std::vector<std::set<std::string>>>::const_iterator, 2> tag_pairs = {
all_tags.begin() + std::distance(shps.begin(), it),
all_tags.begin() + std::distance(shps.begin(), jt)};
for (size_t i = 0; i < 2; ++i) {
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> ancestors;
const auto& wire = wp[i];
@@ -357,7 +357,7 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
for (NCollection_List<TopoDS_Shape>::Iterator edge_it(incidentEdges); edge_it.More(); edge_it.Next()) {
const TopoDS_Edge& e = TopoDS::Edge(edge_it.Value());
TopoDS_Vertex ev0, ev1;
TopExp::Vertices(e, ev0, ev1);
@@ -405,11 +405,11 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
++d;
} else {
throw std::runtime_error("Unable to construct surface");
}
}
}
continue;
}
}
for (auto& wp : ws) {
BRepTools_WireExplorer a(wp[0]);
+1 -1
View File
@@ -204,7 +204,7 @@ namespace {
BRep_Tool::Pnt(v1).DumpJson(oss);
auto osss = oss.str();
std::wcout << osss.c_str() << std::endl;
#endif
#endif
BRep_Builder B;
TopoDS_Wire W;
@@ -75,7 +75,7 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
// A 3x3 matrix to rotate the vertex normals
std::optional<gp_Mat> rotation_matrix;
if (place.components_) {
const auto& m = *place.components_;
rotation_matrix.emplace(
@@ -84,7 +84,7 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
m(2, 0), m(2, 1), m(2, 2)
);
}
// When welding vertices, vertex coords will be shared among faces so we need to per-shape set
// to keep track of which edges were already emitted.
std::set<std::pair<int, int>> emitted_edges;
@@ -119,9 +119,9 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
for (exp.Init(shape_, TopAbs_FACE); exp.More(); exp.Next(), ++num_faces) {
TopoDS_Face face = TopoDS::Face(exp.Current());
size_t num_bounds = 0;
size_t num_bounds = 0;
for (TopoDS_Iterator it(face); it.More(); it.Next(), ++num_bounds) {}
const bool is_planar = BRep_Tool::Surface(face) && BRep_Tool::Surface(face)->DynamicType() == STANDARD_TYPE(Geom_Plane);
const bool has_inner_bounds = num_bounds > 1;
@@ -314,7 +314,7 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
} else {
p = tessellater.Value(i).XYZ();
}
auto p_local = p;
taxonomy_transform(place.components_, p);
@@ -582,7 +582,7 @@ conversion_result_shape* ifcopenshell::geom::open_cascade_shape::concat(conversi
{
TopoDS_Compound compound;
BRep_Builder builder;
auto& left = shape_;
auto& right = ((ifcopenshell::geom::open_cascade_shape*)other)->shape_;
@@ -594,7 +594,7 @@ conversion_result_shape* ifcopenshell::geom::open_cascade_shape::concat(conversi
builder.MakeCompound(compound);
builder.Add(compound, left);
}
builder.Add(compound, right);
return new open_cascade_shape(std::move(compound));
@@ -84,7 +84,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_openings(const express::ba
// opening_trsf = relative;
std::vector<ifcopenshell::geom::conversion_result> opening_shapes;
// @todo
abstract_kernel::convert(op.first, opening_shapes);
@@ -309,13 +309,13 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// IfcSchema::IfcRelVoidsElement::list::ptr ifcopenshell::geom::Kernel::find_openings(IfcSchema::IfcProduct* product) {
// std::vector<IfcSchema::IfcRelVoidsElement*> rs;
//
//
// if (product->declaration().is(IfcSchema::IfcElement::Class()) && !product->declaration().is(IfcSchema::IfcOpeningElement::Class())) {
// IfcSchema::IfcElement* element = (IfcSchema::IfcElement*)product;
// auto rels = element->HasOpenings();
// rs.insert(rs.end(), rels->begin(), rels->end());
// }
//
//
// // Is the IfcElement a decomposition of an IfcElement with any IfcOpeningElements?
// IfcSchema::IfcObjectDefinition* obdef = product->as<IfcSchema::IfcObjectDefinition>();
// for (;;) {
@@ -327,10 +327,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// auto rels = element->HasOpenings();
// rs.insert(rs.end(), rels->begin(), rels->end());
// }
//
//
// obdef = rel_obdef;
// }
//
//
// // Filter openings in Reference view, solely marked as Reference.
// IfcSchema::IfcRelVoidsElement::list::ptr openings(new IfcSchema::IfcRelVoidsElement::list);
// std::for_each(rs.begin(), rs.end(), [&openings](IfcSchema::IfcRelVoidsElement* rel) {
@@ -341,17 +341,17 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// });
//
//
// return openings;
// }
//
//
// const IfcSchema::IfcMaterial* ifcopenshell::geom::Kernel::get_single_material_association(const IfcSchema::IfcProduct* product) {
// IfcSchema::IfcMaterial* single_material = 0;
// IfcSchema::IfcRelAssociatesMaterial::list::ptr associated_materials = product->HasAssociations()->as<IfcSchema::IfcRelAssociatesMaterial>();
// if (associated_materials->size() == 1) {
// IfcSchema::IfcMaterialSelect* associated_material = (*associated_materials->begin())->RelatingMaterial();
// single_material = associated_material->as<IfcSchema::IfcMaterial>();
//
//
// // NB: IfcMaterialLayerSets are also considered, regardless of --enable-layerset-slicing. Picking
// // the first material (in accordance with other viewers) when layerset-slicing is disabled.
// if (!single_material && associated_material->as<IfcSchema::IfcMaterialLayerSetUsage>()) {
@@ -366,21 +366,21 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// return single_material;
// }
//
//
// ifcopenshell::geom::native_element* ifcopenshell::geom::Kernel::create_brep_for_representation_and_product(
// const IteratorSettings& settings, IfcSchema::IfcRepresentation* representation, IfcSchema::IfcProduct* product)
// {
// std::stringstream representation_id_builder;
//
//
// representation_id_builder << representation->data().id();
//
//
// ifcopenshell::geom::native* shape;
// std::vector<ifcopenshell::geom::conversion_result> shapes, shapes2;
//
//
// if (!convert_shapes(representation, shapes)) {
// return 0;
// }
//
//
// if (settings.get(IteratorSettings::APPLY_LAYERSETS)) {
// TopoDS_Shape merge;
// if (util::flatten_shape_list(shapes, merge, false, getValue(GV_PRECISION))) {
@@ -390,7 +390,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// std::vector< std::vector<Handle_Geom_Surface> > folded_layers;
// std::vector<std::shared_ptr<const SurfaceStyle>> styles;
// if (convert_layerset(product, layers, styles, thickness)) {
//
//
// IfcSchema::IfcRelAssociates::list::ptr associations = product->HasAssociations();
// for (IfcSchema::IfcRelAssociates::list::it it = associations->begin(); it != associations->end(); ++it) {
// IfcSchema::IfcRelAssociatesMaterial* associates_material = (**it).as<IfcSchema::IfcRelAssociatesMaterial>();
@@ -400,7 +400,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// break;
// }
// }
//
//
// if (styles.size() > 1) {
// // If there's only a single layer there is no need to manipulate geometries.
// bool success = true;
@@ -415,7 +415,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// success = true;
// }
// }
//
//
// if (!success) {
// ifcopenshell::logger::root().error("Failed processing layerset");
// }
@@ -424,9 +424,9 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// bool material_style_applied = false;
//
//
// const IfcSchema::IfcMaterial* single_material = get_single_material_association(product);
// if (single_material) {
// auto s = get_style(single_material);
@@ -448,11 +448,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// ifcopenshell::logger::root().warning("No material and surface styles for:", product);
// }
// }
//
//
// if (material_style_applied) {
// representation_id_builder << "-material-" << single_material->data().id();
// }
//
//
// if (settings.force_space_transparency() >= 0. && product->declaration().is("IfcSpace")) {
// for (auto& s : shapes) {
// if (s.hasStyle()) {
@@ -464,7 +464,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// int parent_id = -1;
// try {
// express::entity* parent_object = get_decomposing_entity(product);
@@ -474,10 +474,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// } catch (const std::exception& e) {
// ifcopenshell::logger::root().error(e);
// }
//
//
// const std::string name = product->Name().value_or("");
// const std::string guid = product->GlobalId();
//
//
// gp_Trsf trsf;
// try {
// if (product->ObjectPlacement()) {
@@ -488,20 +488,20 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// } catch (...) {
// ifcopenshell::logger::root().error("Failed to construct placement");
// }
//
//
// // Does the IfcElement have any IfcOpenings?
// // Note that openings for IfcOpeningElements are not processed
// IfcSchema::IfcRelVoidsElement::list::ptr openings = find_openings(product);
//
//
// const std::string product_type = product->declaration().name();
// ElementSettings element_settings(settings, getValue(GV_LENGTH_UNIT), product_type);
//
//
// if (!settings.get(ifcopenshell::geom::IteratorSettings::DISABLE_OPENING_SUBTRACTIONS) && openings && openings->size()) {
// representation_id_builder << "-openings";
// for (IfcSchema::IfcRelVoidsElement::list::it it = openings->begin(); it != openings->end(); ++it) {
// representation_id_builder << "-" << (*it)->data().id();
// }
//
//
// std::vector<ifcopenshell::geom::conversion_result> opened_shapes;
// bool caught_error = false;
// try {
@@ -512,11 +512,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// } catch (...) {
// ifcopenshell::logger::root().message(ifcopenshell::logger::LOG_ERROR, "error processing openings for:", product);
// }
//
//
// if (caught_error && opened_shapes.size() < shapes.size()) {
// opened_shapes = shapes;
// }
//
//
// if (settings.get(IteratorSettings::USE_WORLD_COORDS)) {
// for (std::vector<ifcopenshell::geom::conversion_result>::iterator it = opened_shapes.begin(); it != opened_shapes.end(); ++it) {
// it->prepend(trsf);
@@ -535,14 +535,14 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// } else {
// shape = new ifcopenshell::geom::native(element_settings, representation_id_builder.str(), shapes);
// }
//
//
// std::string context_string = "";
// if (representation->RepresentationIdentifier()) {
// context_string = *representation->RepresentationIdentifier();
// } else if (representation->ContextOfItems()->ContextType()) {
// context_string = *representation->ContextOfItems()->ContextType();
// }
//
//
// auto elem = new native_element(
// product->data().id(),
// parent_id,
@@ -554,7 +554,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// std::shared_ptr<ifcopenshell::geom::native>(shape),
// product
// );
//
//
// if (settings.get(IteratorSettings::VALIDATE_QUANTITIES)) {
// auto rels = product->IsDefinedBy();
// for (auto& rel : *rels) {
@@ -623,10 +623,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// return elem;
// }
//
//
// IfcSchema::IfcRepresentation* ifcopenshell::geom::Kernel::representation_mapped_to(const IfcSchema::IfcRepresentation* representation) {
// IfcSchema::IfcRepresentation* representation_mapped_to = 0;
// try {
@@ -651,36 +651,36 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// return representation_mapped_to;
// }
//
//
// IfcSchema::IfcProduct::list::ptr ifcopenshell::geom::Kernel::products_represented_by(const IfcSchema::IfcRepresentation* representation) {
// IfcSchema::IfcProduct::list::ptr products(new IfcSchema::IfcProduct::list);
//
//
// IfcSchema::IfcProductRepresentation::list::ptr prodreps = representation->OfProductRepresentation();
//
//
// for (IfcSchema::IfcProductRepresentation::list::it it = prodreps->begin(); it != prodreps->end(); ++it) {
// // http://buildingsmart-tech.org/ifc/IFC2x3/TC1/html/ifcrepresentationresource/lexical/ifcproductrepresentation.htm
// // IFC2x Edition 3 NOTE Users should not instantiate the entity IfcProductRepresentation from IFC2x Edition 3 onwards.
// // It will be changed into an ABSTRACT supertype in future releases of IFC.
//
//
// // IfcProductRepresentation also lacks the INVERSE relation to IfcProduct
// // Let's find the IfcProducts that reference the IfcProductRepresentation anyway
// products->push((*it)->data().get_inverse((&IfcSchema::IfcProduct::Class()), -1)->as<IfcSchema::IfcProduct>());
// }
//
//
// IfcSchema::IfcRepresentationMap::list::ptr maps = representation->RepresentationMap();
//
//
// if (products->size() && maps->size()) {
// ifcopenshell::logger::root().warning("Representation used by IfcRepresentationMap and IfcProductDefinitionShape", representation);
// }
//
//
// if (prodreps->size() > 1) {
// ifcopenshell::logger::root().warning("Multiple IfcProductDefinitionShapes for representation", representation);
// }
//
//
// if (maps->size() > 1) {
// ifcopenshell::logger::root().warning("Multiple IfcRepresentationMaps for representation", representation);
// }
//
//
// if (maps->size() == 1) {
// IfcSchema::IfcRepresentationMap* map = *maps->begin();
// if (is_identity_transform(map->MappingOrigin())) {
@@ -688,11 +688,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// for (IfcSchema::IfcMappedItem::list::it it = items->begin(); it != items->end(); ++it) {
// IfcSchema::IfcMappedptr item = *it;
// if (item->StyledByItem()->size() != 0) continue;
//
//
// if (!is_identity_transform(item->MappingTarget())) {
// continue;
// }
//
//
// IfcSchema::IfcRepresentation::list::ptr reps = item->data().get_inverse((&IfcSchema::IfcRepresentation::Class()), -1)->as<IfcSchema::IfcRepresentation>();
// for (IfcSchema::IfcRepresentation::list::it jt = reps->begin(); jt != reps->end(); ++jt) {
// IfcSchema::IfcRepresentation* rep = *jt;
@@ -706,10 +706,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// return products;
// }
//
//
// ifcopenshell::geom::native_element* ifcopenshell::geom::Kernel::create_brep_for_processed_representation(
// const IteratorSettings& /*settings*/, IfcSchema::IfcRepresentation* representation, IfcSchema::IfcProduct* product,
// ifcopenshell::geom::native_element* brep)
@@ -723,10 +723,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// } catch (const std::exception& e) {
// ifcopenshell::logger::root().error(e);
// }
//
//
// const std::string name = product->Name().value_or("");
// const std::string guid = product->GlobalId();
//
//
// gp_Trsf trsf;
// try {
// if (product->ObjectPlacement()) {
@@ -737,16 +737,16 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// } catch (...) {
// ifcopenshell::logger::root().error("Failed to construct placement");
// }
//
//
// std::string context_string = "";
// if (representation->RepresentationIdentifier()) {
// context_string = *representation->RepresentationIdentifier();
// } else if (representation->ContextOfItems()->ContextType()) {
// context_string = *representation->ContextOfItems()->ContextType();
// }
//
//
// const std::string product_type = product->declaration().name();
//
//
// return new native_element(
// product->data().id(),
// parent_id,
@@ -759,24 +759,24 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// product
// );
// }
//
//
// bool ifcopenshell::geom::Kernel::convert_layerset(const IfcSchema::IfcProduct* product, std::vector<Handle_Geom_Surface>& surfaces, std::vector<std::shared_ptr<const SurfaceStyle>>& styles, std::vector<double>& thicknesses) {
//
//
// }
//
//
// bool ifcopenshell::geom::Kernel::find_wall_end_points(const IfcSchema::IfcWall* wall, gp_Pnt& start, gp_Pnt& end) {
// IfcSchema::IfcRepresentation* axis_representation = find_representation(wall, "Axis");
// if (!axis_representation) {
// return false;
// }
//
//
// std::vector<conversion_result> items;
// {
// Kernel temp = *this;
// temp.setValue(GV_DIMENSIONALITY, -1.);
// temp.convert_shapes(axis_representation, items);
// }
//
//
// TopoDS_Vertex a, b;
// for (std::vector<conversion_result>::const_iterator it = items.begin(); it != items.end(); ++it) {
// TopExp_Explorer exp(it->shape(), TopAbs_VERTEX);
@@ -787,36 +787,36 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// if (a.IsNull() || b.IsNull()) {
// return false;
// }
//
//
// start = BRep_Tool::Pnt(a);
// end = BRep_Tool::Pnt(b);
//
//
// return true;
// }
//
//
// bool ifcopenshell::geom::Kernel::fold_layers(const IfcSchema::IfcWall* wall, const std::vector<conversion_result>& items, const std::vector<Handle_Geom_Surface>& surfaces, const std::vector<double>& thicknesses, std::vector< std::vector<Handle_Geom_Surface> >& result) {
// /*
// * @todo isn't it easier to do this based on the non-folded surfaces of
// * the connected walls and fold both pairs of layersets simultaneously?
// */
//
//
// bool folds_made = false;
//
//
// IfcSchema::IfcRelConnectsPathElements::list::ptr connections(new IfcSchema::IfcRelConnectsPathElements::list);
// connections->push(wall->ConnectedFrom()->as<IfcSchema::IfcRelConnectsPathElements>());
// connections->push(wall->ConnectedTo()->as<IfcSchema::IfcRelConnectsPathElements>());
//
//
// typedef std::vector<Handle_Geom_Surface> surfaces_t;
// typedef std::pair<Handle_Geom_Surface, Handle_Geom_Curve> curve_on_surface;
// typedef std::vector<curve_on_surface> curves_on_surfaces_t;
// typedef std::vector< std::pair< std::pair<IfcSchema::IfcConnectionTypeEnum::Value, IfcSchema::IfcConnectionTypeEnum::Value>, const IfcSchema::IfcProduct*> > endpoint_connections_t;
// typedef std::vector< std::vector<Handle_Geom_Surface> > result_t;
// endpoint_connections_t endpoint_connections;
//
//
// // Find the semantic connections to other wall elements when they are not connected 'AT_PATH' because
// // in that latter case no folds need to be made.
// for (IfcSchema::IfcRelConnectsPathElements::list::it it = connections->begin(); it != connections->end(); ++it) {
@@ -838,18 +838,18 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// if (endpoint_connections.size() == 0) {
// return false;
// }
//
//
// // Count how many connections are made AT_START and AT_END respectively
// int connection_type_count[2] = { 0,0 };
// for (endpoint_connections_t::const_iterator it = endpoint_connections.begin(); it != endpoint_connections.end(); ++it) {
// const int idx = it->first.first == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART;
// connection_type_count[idx] ++;
// }
//
//
// gp_Trsf local;
// if (wall->ObjectPlacement()) {
// if (!convert(wall->ObjectPlacement(), local)) {
@@ -857,7 +857,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// local.Invert();
//
//
// {
// // Copy the unfolded surfaces
// result.resize(surfaces.size());
@@ -867,25 +867,25 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// result_it->push_back(*input_it);
// }
// }
//
//
// const double total_thickness = std::accumulate(thicknesses.begin(), thicknesses.end(), 0.);
//
//
// gp_Pnt own_axis_start, own_axis_end;
// find_wall_end_points(wall, own_axis_start, own_axis_end);
//
//
// // Sometimes duplicate IfcRelConnectsPathElements exist. These are detected
// // and the counts of connections are decremented accordingly.
// for (int idx = 0; idx < 2; ++idx) {
// if (connection_type_count[idx] <= 1) {
// continue;
// }
//
//
// /*
// IfcSchema::IfcConnectionTypeEnum::Value connection_type = idx == 1
// ? IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART
// : IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATEND;
// */
//
//
// std::set<const IfcSchema::IfcProduct*> others;
// endpoint_connections_t::iterator it = endpoint_connections.begin();
// while (it != endpoint_connections.end()) {
@@ -899,38 +899,38 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// // Check whether the end points are of the wall are really ~1 LayerThickness away from each other
// /*
// for (endpoint_connections_t::const_iterator it = endpoint_connections.begin(); it != endpoint_connections.end(); ++it) {
// IfcSchema::IfcConnectionTypeEnum::Value own_type = it->first.first;
// IfcSchema::IfcConnectionTypeEnum::Value other_type = it->first.second;
//
//
// gp_Pnt other_axis_start, other_axis_end;
// find_wall_end_points(it->second->as<IfcSchema::IfcWall>(), other_axis_start, other_axis_end);
//
//
// gp_Trsf other;
// if (!convert(it->second->ObjectPlacement(), other)) {
// continue;
// }
//
//
// other.Transforms(other_axis_start.ChangeCoord());
// local.Transforms(other_axis_start.ChangeCoord());
// other.Transforms(other_axis_end.ChangeCoord());
// local.Transforms(other_axis_end.ChangeCoord());
//
//
// const gp_Pnt& a = own_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART
// ? own_axis_start
// : own_axis_end;
//
//
// const gp_Pnt& b = other_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART
// ? other_axis_start
// : other_axis_end;
//
//
// const double d = a.Distance(b);
// }
// */
//
//
// const double length_required = endpoint_connections.size() * total_thickness;
// // @todo this is not precisely the distance in case of curved walls. Also, it's safer
// // to first reproject the body onto the axis to get the precise curve parametrization
@@ -940,20 +940,20 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// ifcopenshell::logger::root().warning("The wall axis is not long enough to accommodate the fold points");
// return false;
// }
//
//
// for (endpoint_connections_t::const_iterator it = endpoint_connections.begin(); it != endpoint_connections.end(); ++it) {
// IfcSchema::IfcConnectionTypeEnum::Value connection_type = it->first.first;
//
//
// // If more than one wall connects to this start/end -point assume layers do not need to be folded
// const int idx = connection_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART;
// if (connection_type_count[idx] > 1) continue;
//
//
// // Pick the corresponding point from the axis
// const gp_Pnt& own_end_point = connection_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATEND
// ? own_axis_end
// : own_axis_start;
// const IfcSchema::IfcProduct* other_wall = it->second;
//
//
// gp_Trsf other;
// if (other_wall->ObjectPlacement()) {
// if (!convert(other_wall->ObjectPlacement(), other)) {
@@ -961,32 +961,32 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// continue;
// }
// }
//
//
// IfcSchema::IfcRepresentation* axis_representation = find_representation(other_wall, "Axis");
//
//
// if (!axis_representation) {
// ifcopenshell::logger::root().warning("Joined wall has no axis representation", other_wall);
// continue;
// }
//
//
// std::vector<conversion_result> axis_items;
// {
// Kernel temp = *this;
// temp.setValue(GV_DIMENSIONALITY, -1.);
// temp.convert_shapes(axis_representation, axis_items);
// }
//
//
// TopoDS_Shape axis_shape;
// util::flatten_shape_list(axis_items, axis_shape, false, getValue(GV_PRECISION));
//
//
// // local and other are IfcLocalPlacements and therefore have a unit
// // scale factor that can be applied by means of TopoDS_Shape::Move()
// axis_shape.Move(other);
// axis_shape.Move(local);
//
//
// TopoDS_Shape body_shape;
// util::flatten_shape_list(items, body_shape, false, getValue(GV_PRECISION));
//
//
// // Create a single paremetric range over a single curve
// // that represents the entire 1d domain of the other wall
// // Sometimes there are multiple edges in the Axis shape
@@ -998,19 +998,19 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// if (!exp.More()) {
// return false;
// }
//
//
// TopoDS_Edge axis_edge = TopoDS::Edge(exp.Current());
// other_axis_curve = BRep_Tool::Curve(axis_edge, axis_u1, axis_u2);
//
//
// gp_Pnt other_a_1, other_a_2;
// other_axis_curve->D0(axis_u1, other_a_1);
// other_axis_curve->D0(axis_u2, other_a_2);
//
//
// if (axis_u2 < axis_u1) {
// std::swap(axis_u1, axis_u2);
// }
// exp.Next();
//
//
// for (; exp.More(); exp.Next()) {
// TopoDS_Edge axis_edge2 = TopoDS::Edge(exp.Current());
// TopExp_Explorer exp2(axis_edge2, TopAbs_VERTEX);
@@ -1025,22 +1025,22 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// double layer_offset = 0;
//
//
// std::vector<double>::const_iterator thickness = thicknesses.begin();
// result_t::iterator result_vector = result.begin() + 1;
//
//
// // nb The first layer is never folded, because it corresponds
// // to one of the longitudinal faces of the wall. Hence the +1
// for (surfaces_t::const_iterator jt = surfaces.begin() + 1; jt != surfaces.end() - 1; ++jt, ++result_vector) {
// layer_offset += *thickness++;
//
//
// bool found_intersection = false, parallel = false;
// std::optional<gp_Pnt> point_outside_param_range;
//
//
// const Handle_Geom_Surface& surface = *jt;
//
//
// // Find the intersection point between the layerset surface
// // and the other axis curve. If it's within the parametric
// // range of the other wall it means the walls are connected
@@ -1048,16 +1048,16 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// GeomAPI_IntCS intersections(other_axis_curve, surface);
// if (intersections.IsDone() && intersections.NbPoints() == 1) {
// const gp_Pnt& p = intersections.Point(1);
//
//
// double u, v, w;
// intersections.Parameters(1, u, v, w);
//
//
// gp_Pnt Pc, Ps;
// gp_Vec Vc, Vs1, Vs2;
// other_axis_curve->D1(w, Pc, Vc);
// surface->D1(u, v, Ps, Vs1, Vs2);
// Vs1.Cross(Vs2);
//
//
// if (Vs1.IsNormal(Vc, 1.e-5)) {
// ifcopenshell::logger::root().warning("Connected walls are parallel");
// parallel = true;
@@ -1069,9 +1069,9 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// break;
// }
// }
//
//
// if (!parallel && !found_intersection && point_outside_param_range) {
//
//
// /*
// Is there a bug in Open Cascade related to the intersection
// of offset surfaces constructed from linear extrusions?
@@ -1083,13 +1083,13 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// Handle_Geom_Surface yz2 = new Geom_OffsetSurface(yz, 1.);
// intersect(xy, yz2);
// */
//
//
// Handle_Geom_Surface plane = new Geom_Plane(*point_outside_param_range, gp::DZ());
//
//
// // vertical edges at wall end point face.
// curves_on_surfaces_t layer_ends;
// util::intersect(surface, body_shape, layer_ends);
//
//
// Handle_Geom_Curve layer_body_intersection;
// Handle_Geom_Surface body_surface;
// double mind = std::numeric_limits<double>::infinity();
@@ -1111,9 +1111,9 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// if (d < total_thickness * 3 && d < mind) {
// GeomAdaptor_Curve GAC(other_axis_curve);
// GeomAdaptor_Surface GAS(kt->first);
//
//
// Extrema_ExtCS x(GAC, GAS, getValue(GV_PRECISION), getValue(GV_PRECISION));
//
//
// if (x.IsParallel()) {
// body_surface = kt->first;
// layer_body_intersection = kt->second;
@@ -1122,16 +1122,16 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// if (body_surface.IsNull()) {
// continue;
// }
//
//
// // Intersect vertical edge with ground plane for point.
// GeomAPI_IntCS intersection2(layer_body_intersection, plane);
// if (intersection2.IsDone() && intersection2.NbPoints() == 1) {
// const gp_Pnt& layer_end_point = intersection2.Point(1);
//
//
// // Intersect layerset surface with ground plane
// GeomAPI_IntSS intersection3(surface, plane, 1.e-7);
// if (intersection3.IsDone() && intersection3.NbLines() == 1) {
@@ -1140,14 +1140,14 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// ShapeAnalysis_Curve sac;
// gp_Pnt layer_end_point_projected; double layer_end_point_param;
// sac.Project(layer_line, layer_end_point, 1e-3, layer_end_point_projected, layer_end_point_param, false);
//
//
// // Move point inwards by distance from other layerset
// GCPnts_AbscissaPoint dst(layer_line_adaptor, layer_offset, layer_end_point_param);
// if (dst.IsDone()) {
// // Convert parameter to point
// gp_Pnt layer_fold_point;
// layer_line->D0(dst.Parameter(), layer_fold_point);
//
//
// GeomAPI_IntSS intersection4(body_surface, plane, 1.e-7);
// if (intersection4.IsDone() && intersection4.NbLines() == 1) {
// Handle_Geom_Curve body_trim_curve = intersection4.Line(1);
@@ -1155,7 +1155,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// gp_Pnt layer_fold_point_projected; double layer_fold_point_param;
// sac2.Project(body_trim_curve, layer_fold_point, 1.e-7, layer_fold_point_projected, layer_fold_point_param, false);
// Handle_Geom_Curve fold_curve = new Geom_OffsetCurve(body_trim_curve->Reversed(), layer_fold_point_projected.Distance(layer_fold_point), gp::DZ());
//
//
// Handle_Geom_Surface fold_surface = new Geom_SurfaceOfLinearExtrusion(fold_curve, gp::DZ());
// result_vector->push_back(fold_surface);
// folds_made = true;
@@ -1163,15 +1163,15 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// }
// }
//
//
// }
//
//
// }
// }
//
//
// return folds_made;
// }
//
//
// IfcSchema::IfcRepresentation* ifcopenshell::geom::Kernel::find_representation(const IfcSchema::IfcProduct* product, const std::string& identifier) {
// if (!product->Representation()) return 0;
// IfcSchema::IfcProductRepresentation* prod_rep = product->Representation();
@@ -1183,12 +1183,12 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// return 0;
// }
//
//
// const IfcSchema::IfcRepresentationptr ifcopenshell::geom::Kernel::find_item_carrying_style(const IfcSchema::IfcRepresentationptr item) {
// if (item->StyledByItem()->size()) {
// return item;
// }
//
//
// while (item->declaration().is(IfcSchema::IfcBooleanResult::Class())) {
// // All instantiations of IfcBooleanOperand (type of FirstOperand) are subtypes of
// // IfcGeometricRepresentationItem
@@ -1197,24 +1197,24 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// return item;
// }
// }
//
//
// // TODO: Ideally this would be done for other entities (such as IfcCsgSolid) as well.
// // But neither are these very prevalent, nor does the current IfcOpenShell style
// // mechanism enable to conveniently style subshapes, which would be necessary for
// // distinctly styled union operands.
//
//
// return item;
// }
//
//
// bool ifcopenshell::geom::Kernel::is_identity_transform(ifcopenshell::IfcBaseInterface* l) {
// IfcSchema::IfcAxis2Placement2D* ax2d;
// IfcSchema::IfcAxis2Placement3D* ax3d;
//
//
// IfcSchema::IfcCartesianTransformationOperator2D* op2d;
// IfcSchema::IfcCartesianTransformationOperator3D* op3d;
// IfcSchema::IfcCartesianTransformationOperator2DnonUniform* op2dnonu;
// IfcSchema::IfcCartesianTransformationOperator3DnonUniform* op3dnonu;
//
//
// if ((op2dnonu = l->as<IfcSchema::IfcCartesianTransformationOperator2DnonUniform>()) != 0) {
// gp_GTrsf2d gtrsf2d;
// convert(op2dnonu, gtrsf2d);
@@ -1243,18 +1243,18 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// throw ifcopenshell::exception("Invalid valuation for IfcAxis2Placement / IfcCartesianTransformationOperator");
// }
// }
//
//
// void ifcopenshell::geom::Kernel::set_conversion_placement_rel_to_type(const ifcopenshell::declaration* type) {
// placement_rel_to_type_ = type;
// }
//
//
// void ifcopenshell::geom::Kernel::set_conversion_placement_rel_to_instance(const express::entity* instance) {
// placement_rel_to_instance_ = instance;
// }
//
//
//
//
// namespace {
//
//
// bool process_colour(IfcSchema::IfcColourRgb* colour, double* rgb) {
// if (colour != 0) {
// rgb[0] = colour->Red();
@@ -1263,7 +1263,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// return colour != 0;
// }
//
//
// bool process_colour(IfcSchema::IfcNormalisedRatioMeasure* factor, double* rgb) {
// if (factor != 0) {
// const double f = *factor;
@@ -1271,7 +1271,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// return factor != 0;
// }
//
//
// bool process_colour(IfcSchema::IfcColourOrFactor* colour_or_factor, double* rgb) {
// if (colour_or_factor == 0) {
// return false;
@@ -1283,11 +1283,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// return false;
// }
// }
//
//
// }
//
//
// #define Kernel POSTFIX_SCHEMA(Kernel)
//
//
// std::shared_ptr<const ifcopenshell::geom::SurfaceStyle> ifcopenshell::geom::Kernel::internalize_surface_style(const std::pair<express::base, express::base>& shading_styles) {
// if (shading_styles.second == 0) {
// return 0;
@@ -1297,22 +1297,22 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// if (it != style_cache.end()) {
// return it->second;
// }
//
//
//
//
// IfcSchema::IfcSurfaceStyle* style = shading_styles.first->as<IfcSchema::IfcSurfaceStyle>();
// IfcSchema::IfcSurfaceStyleShading* shading = shading_styles.second->as<IfcSchema::IfcSurfaceStyleShading>();
//
//
// std::shared_ptr<SurfaceStyle> surface_style_ptr;
//
//
// if (style->Name()) {
// surface_style_ptr.reset(new SurfaceStyle(surface_style_id, *style->Name()));
// } else {
// surface_style_ptr.reset(new SurfaceStyle(surface_style_id));
// }
//
//
// std::shared_ptr<const SurfaceStyle> surface_style_ptr_const = std::const_pointer_cast<const SurfaceStyle>(surface_style_ptr);
// SurfaceStyle& surface_style = *surface_style_ptr;
//
//
// double rgb[3];
// if (process_colour(shading->SurfaceColour(), rgb)) {
// surface_style.Diffuse().reset(SurfaceStyle::ColorComponent(rgb[0], rgb[1], rgb[2]));
@@ -1353,11 +1353,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// }
// return style_cache[surface_style_id] = surface_style_ptr_const;
// }
//
//
// std::shared_ptr<const ifcopenshell::geom::SurfaceStyle> ifcopenshell::geom::Kernel::get_style(const IfcSchema::IfcRepresentationptr item) {
// return internalize_surface_style(get_surface_style<IfcSchema::IfcSurfaceStyleShading>(item));
// }
//
//
// std::shared_ptr<const ifcopenshell::geom::SurfaceStyle> ifcopenshell::geom::Kernel::get_style(const IfcSchema::IfcMaterial* material) {
// IfcSchema::IfcMaterialDefinitionRepresentation::list::ptr defs = material->HasRepresentation();
// for (IfcSchema::IfcMaterialDefinitionRepresentation::list::it jt = defs->begin(); jt != defs->end(); ++jt) {
@@ -1376,14 +1376,14 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// auto material_style = std::make_shared<ifcopenshell::geom::SurfaceStyle>(material->data().id(), material->Name());
// return style_cache[material->data().id()] = material_style;
// }
//
//
// void ifcopenshell::geom::Kernel::apply_layerset(std::vector<ifcopenshell::geom::conversion_result>& r, const ifcopenshell::geom::layerset_information& info) {
// convert(info.layers);
//
//
// if (info.layers.empty()) {
// return;
// }
//
//
// if (axis_curve->DynamicType() == STANDARD_TYPE(Geom_Line)) {
// Handle_Geom_Line axis_line = Handle_Geom_Line::DownCast(axis_curve);
// // @todo note that this creates an offset into the wrong order, the cross product arguments should be
@@ -1397,7 +1397,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
// ifcopenshell::logger::root().message(ifcopenshell::logger::LOG_ERROR, "Unsupported underlying curve of Axis representation:", product);
// return false;
// }
//
//
// std::vector<ifcopenshell::geom::conversion_result> r2;
// if (ifcopenshell::geom::util::apply_layerset(r, const std::vector<ifcopenshell::geom::taxonomy::style>&, std::vector<conversion_result>& r2, double tol)) {
// std::swap(r, r2)
@@ -90,7 +90,7 @@ private:
std::map<std::pair<int, int>, TopoDS_Edge> edges_;
double eps_;
bool non_manifold_;
void loop_(const ifcopenshell::geom::taxonomy::loop::ptr ps, const std::function<void(int, int, bool)>& callback);
public:
faceset_helper(open_cascade_kernel* kernel, const ifcopenshell::geom::taxonomy::shell::ptr l);
@@ -99,7 +99,7 @@ private:
bool non_manifold() const { return non_manifold_; }
bool& non_manifold() { return non_manifold_; }
double epsilon() const { return eps_; }
bool edge(int A, int B, TopoDS_Edge& e);
bool wire(const ifcopenshell::geom::taxonomy::loop::ptr loop, TopoDS_Wire& wire);
+2 -2
View File
@@ -64,8 +64,8 @@ bool open_cascade_kernel::convert(const taxonomy::solid::ptr solid, TopoDS_Shape
result = halfspace;
return true;
} else if (solid->children.size() == 1
&& solid->children[0]->children.size() == 1
} else if (solid->children.size() == 1
&& solid->children[0]->children.size() == 1
&& solid->children[0]->children[0]->basis
&& solid->children[0]->children[0]->basis->kind() == taxonomy::SPHERE)
{
@@ -126,7 +126,7 @@ bool open_cascade_kernel::convert(const taxonomy::sweep_along_curve::ptr scs, To
}
}
}
// Build the wire from curve, which is the directrix offset toward the origin
// when applied_temporary_offset is set. Using scs->curve here left the wire
// far from the origin yet still translated the result back by +mean, which
@@ -155,11 +155,11 @@ bool open_cascade_kernel::convert(const taxonomy::sweep_along_curve::ptr scs, To
} else {
return false;
}
Handle(Geom_Surface) surface;
if (scs->surface) {
surface = convert_surface(scs->surface);
}
}
gp_Trsf directrix;
TopoDS_Wire wire = std::get<TopoDS_Wire>(w);
+18 -18
View File
@@ -201,12 +201,12 @@ namespace ifcopenshell::geom {
box.corners[1][1] = static_cast<float>(max_point[1] + 1e-5);
box.corners[1][2] = static_cast<float>(max_point[2] + 1e-5);
/*
std::cout << "Ray "
std::cout << "Ray "
<< v_ray.origin[0] << " "
<< v_ray.origin[1] << " "
<< v_ray.origin[2] << " "
<< std::endl;
std::cout << "Box "
std::cout << "Box "
<< min_point[0] << " "
<< min_point[1] << " "
<< min_point[2] << " "
@@ -864,7 +864,7 @@ namespace ifcopenshell::geom {
if (dss.Value() <= extend) {
distances_.push_back(dss.Value());
protrusion_distances_.push_back(max_distance_inside(B, A));
}
}
return dss.Value() <= extend;
}
} else {
@@ -925,8 +925,8 @@ namespace ifcopenshell::geom {
// Gap is assumed to be positive throughout the codebase,
// but at least for IsOut() in the selector a negative
// Gap should work as well.
b.SetGap(b.GetGap() + extend);
b.SetGap(b.GetGap() + extend);
return select_box(b, completely_within);
}
@@ -956,7 +956,7 @@ namespace ifcopenshell::geom {
double gap = B.GetGap();
gp_Pnt p1(x1 - gap, y1 - gap, z1 - gap);
gp_Pnt p2(x2 + gap, y2 + gap, z2 + gap);
if (!b.IsOut(p1) && !b.IsOut(p2)) {
ts_filtered.push_back(*it);
}
@@ -1389,7 +1389,7 @@ namespace ifcopenshell::geom {
if (extend > 0.0) {
BRepExtrema_DistShapeShape dss(v, B);
if (dss.Perform() && dss.NbSolution() >= 1 && dss.Value() <= extend) {
distances_.push_back(dss.Value());
distances_.push_back(dss.Value());
protrusion_distances_.push_back(max_distance_inside(B, v));
ts_filtered.push_back(*it);
@@ -1416,9 +1416,9 @@ namespace ifcopenshell::geom {
spatial_tree tree_;
shape_map shapes_;
std::map<T, Bnd_Box> aabbs_;
std::map<T, Bnd_OBB> obbs_;
std::map<T, double> max_protrusions_;
std::map<T, opencascade::handle<BVH_Tree<double, 3, BVH_BinaryTree>>> bvhs_;
std::map<T, Bnd_OBB> obbs_;
std::map<T, double> max_protrusions_;
std::map<T, opencascade::handle<BVH_Tree<double, 3, BVH_BinaryTree>>> bvhs_;
std::unordered_map<T, bool> is_manifold_;
std::unordered_map<T, std::vector<std::array<int, 3>>> tris_;
std::unordered_map<T, std::vector<gp_Pnt>> verts_;
@@ -1433,7 +1433,7 @@ namespace ifcopenshell::geom {
std::map<std::string, std::vector<int>> local_faces_;
std::map<std::string, std::vector<ifcopenshell::geom::taxonomy::style::ptr>> local_materials_;
std::map<std::string, std::vector<int>> local_material_ids_;
bool enable_face_styles_ = false;
class selector : public spatial_tree::Selector
@@ -1480,7 +1480,7 @@ namespace ifcopenshell::geom {
opencascade_tree(ifcopenshell::geom::iterator& it) {
add_file(it);
}
}
void add_file(ifcopenshell::file& f, ifcopenshell::geom::settings settings) {
ifcopenshell::geom::settings settings_ = settings;
@@ -1509,7 +1509,7 @@ namespace ifcopenshell::geom {
result.reserve(flat_list.size());
for (size_t i = 0; i < flat_list.size(); i += 3) {
vin <<
vin <<
flat_list[i],
flat_list[i + 1],
flat_list[i + 2];
@@ -1576,7 +1576,7 @@ namespace ifcopenshell::geom {
vs_transformed.push_back(p.Transformed(tr));
aabb.Add(vs_transformed.back());
}
std::unordered_map<std::tuple<int, int, int>, std::vector<size_t>, boost::hash<std::tuple<int, int, int>>> quantized_normal_counts;
std::vector<double> tri_areas;
@@ -1631,7 +1631,7 @@ namespace ifcopenshell::geom {
gp_Ax3 ax3;
gp_Trsf trsf2;
for (size_t attempt = 0; attempt < 2; ++attempt) {
if (candidates.empty() || attempt == 1) {
@@ -1673,7 +1673,7 @@ namespace ifcopenshell::geom {
obb.SetZComponent(ax3.Direction(), halfsize.Z());
obb.SetCenter(cent.Transformed(trsf2.Inverted()));
}
const auto& t = elem->product();
const auto& matrix = elem->transformation().data();
const std::vector<double>& elem_verts_local = elem->geometry().verts();
@@ -1744,7 +1744,7 @@ namespace ifcopenshell::geom {
obbs_[t] = obb;
max_protrusions_[t] = std::min(std::min(obb.XHSize(), obb.YHSize()), obb.ZHSize()) * 2;
}
void add_element(ifcopenshell::geom::native_element* elem) {
if (!elem) {
return;
@@ -1753,7 +1753,7 @@ namespace ifcopenshell::geom {
auto compound_generic = (ifcopenshell::geom::open_cascade_shape*)elem->geometry().as_compound();
TopoDS_Shape compound(std::move(compound_generic->shape()));
delete compound_generic;
const auto& m = elem->transformation().data()->ccomponents();
gp_Trsf tr;
tr.SetValues(
@@ -381,7 +381,7 @@ namespace {
bool ifcopenshell::geom::util::wire_intersections(const TopoDS_Wire& wire, NCollection_List<TopoDS_Shape>& wires, const wire_tolerance_settings& settings) {
double eps = get_wire_intersection_tolerance(settings, wire);
double eps_real = settings.precision;
if (!wire.Closed()) {
wires.Append(wire);
return false;
@@ -576,7 +576,7 @@ void ifcopenshell::geom::util::select_largest(const NCollection_List<TopoDS_Shap
BRepBndLib::AddClose(it.Value(), bb);
double xyz_min[3], xyz_max[3];
bb.Get(xyz_min[0], xyz_min[1], xyz_min[2], xyz_max[0], xyz_max[1], xyz_max[2]);
// @todo hard coded precision.
// @todo this is a really strange measure for wire size. Why not use newell's
// method to project to plane and then calculate size of the 2d bbox?
@@ -45,7 +45,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcArbitraryClosedProfileDef& i
}
}
}
return face;
} else {
return nullptr;
+1 -1
View File
@@ -35,7 +35,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis1Placement& inst) {
taxonomy::direction3::ptr v = taxonomy::cast<taxonomy::direction3>(map(inst.Axis()));
axis = *v->components_;
}
// @todo not sure what to do with ref, we're probably never reading it,
// because we just created an Axis1 again from it in the kernel, but
// to this constructor we need to supply something valid.
@@ -27,12 +27,12 @@ using namespace ifcopenshell::geom;
#ifdef SCHEMA_HAS_IfcBSplineCurveWithKnots
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBSplineCurveWithKnots& inst) {
auto bc = taxonomy::make<taxonomy::bspline_curve>();
const std::vector<IfcSchema::IfcCartesianPoint> cps = inst.ControlPointsList();
std::vector<taxonomy::point3::ptr> points;
std::transform(cps.begin(), cps.end(), std::back_inserter(points), [this](const IfcSchema::IfcCartesianPoint& cp) { return taxonomy::cast<taxonomy::point3>(map(cp)); });
bc->control_points = points;
auto knot_multiplicities = inst.KnotMultiplicities();
bc->multiplicities.assign(knot_multiplicities.begin(), knot_multiplicities.end());
bc->knots = inst.Knots();
@@ -73,7 +73,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCenterLineProfileDef&) {
mw.Add(BRepBuilderAPI_MakeEdge(c1a, c2a));
mw.Add(BRepBuilderAPI_MakeEdge(c2));
mw.Add(BRepBuilderAPI_MakeEdge(c2b, c1b));
face = BRepBuilderAPI_MakeFace(mw.Wire());
} else {
BRepOffsetAPI_MakeOffset offset(BRepBuilderAPI_MakeFace(gp_Pln(gp::Origin(), gp::DZ())));
+1 -1
View File
@@ -23,7 +23,7 @@ using namespace ifcopenshell::geom;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCircle& inst) {
const double r = inst.Radius() * length_unit_;
if (r < settings_.get<settings::Precision>().get()) {
if (r < settings_.get<settings::Precision>().get()) {
logger_.message(ifcopenshell::logger::LOG_ERROR, "GEO", 237, "Radius not greater than zero for:", inst);
return nullptr;
}
+1 -1
View File
@@ -31,7 +31,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCompositeCurve& inst) {
#else
std::vector<IfcSchema::IfcCompositeCurveSegment> segments = inst.Segments();
#endif
for (auto& segment : segments) {
if (segment.as<IfcSchema::IfcCompositeCurveSegment>() && segment.as<IfcSchema::IfcCompositeCurveSegment>().ParentCurve().as<IfcSchema::IfcLine>()) {
logger_.notice("GEO", 238, "Infinite IfcLine used as ParentCurve of segment, treating as a segment", segment);
+1 -1
View File
@@ -25,7 +25,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCurveBoundedPlane& inst) {
taxonomy::plane::ptr pl = taxonomy::cast<taxonomy::plane>(map(inst.BasisSurface()));
auto f = taxonomy::make<taxonomy::face>();
f->children.push_back(taxonomy::cast<taxonomy::loop>(map(inst.OuterBoundary())));
std::vector<IfcSchema::IfcCurve> boundaries = inst.InnerBoundaries();
for (auto& b : boundaries) {
+10 -10
View File
@@ -133,7 +133,7 @@ struct spiral_parent_curve : public parent_curve_function {
// this is the piecewise curve segment function for horizontal and vertical
struct curve_segment_function {
curve_segment_function(const Eigen::Matrix4d& curve_segment_placement, const Eigen::Matrix4d& parent_curve_normalization, std::shared_ptr<parent_curve_function> parent_curve_fn) :
curve_segment_function(const Eigen::Matrix4d& curve_segment_placement, const Eigen::Matrix4d& parent_curve_normalization, std::shared_ptr<parent_curve_function> parent_curve_fn) :
curve_segment_placement_(curve_segment_placement),
parent_curve_normalization_(parent_curve_normalization),
parent_curve_fn_(parent_curve_fn) {
@@ -153,7 +153,7 @@ struct curve_segment_function {
// this is the piecewise curve segment function for cant
struct cant_curve_segment_function {
cant_curve_segment_function(const Eigen::Matrix4d& curve_segment_placement, const Eigen::Matrix4d& parent_curve_start_point, std::shared_ptr<parent_curve_function> parent_curve_fn) :
cant_curve_segment_function(const Eigen::Matrix4d& curve_segment_placement, const Eigen::Matrix4d& parent_curve_start_point, std::shared_ptr<parent_curve_function> parent_curve_fn) :
curve_segment_placement_(curve_segment_placement),
parent_curve_start_point_(parent_curve_start_point),
parent_curve_fn_(parent_curve_fn) {
@@ -354,7 +354,7 @@ class curve_segment_evaluator {
} else {
// The parent curve function returns the 4x4 matrix for the parent curve.
// Normalize the parent curve so that the trim start point and tangent direction at the start point
// are aligned with the origin. This is accomplished with a normalization matrix that subtracts the
// are aligned with the origin. This is accomplished with a normalization matrix that subtracts the
// incremental parent curve start point and applies a rotation. Apply the incremental
// translation and rotation to the curve_segment_placement to get the curve_segment_point
@@ -623,7 +623,7 @@ class curve_segment_evaluator {
} else if (segment_type_ == ST_CANT) {
std::optional<std::function<double(double)>> super, slope;
std::tie(super, slope) = get_superelevation_functions();
auto cant = [constant_term, cosine_term, L](double t) -> double {
auto a0 = constant_term.has_value() ? 1 / constant_term.value() : 0.0;
auto a1 = (1 / cosine_term) * cos(PI * t / L);
@@ -690,7 +690,7 @@ class curve_segment_evaluator {
} else if (segment_type_ == ST_CANT) {
std::optional<std::function<double(double)>> super, slope;
std::tie(super, slope) = get_superelevation_functions();
auto cant = [constant_term, linear_term, sine_term, L](double t) -> double {
auto a0 = constant_term.has_value() ? 1 / constant_term.value() : 0.0;
auto a1 = linear_term.has_value() ? (linear_term.value()/fabs(linear_term.value())) * pow(1 / linear_term.value(), 2.0) * t : 0.0;
@@ -825,7 +825,7 @@ class curve_segment_evaluator {
#else
A3 = c.QubicTerm();
#endif
if (segment_type_ == ST_CANT) {
polynomial_cant_spiral(A0, A1, A2, A3, A4, A5, A6, A7);
} else {
@@ -983,7 +983,7 @@ class curve_segment_evaluator {
[](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
}
}
void operator()(const IfcSchema::IfcLine& l) {
projected_length_ = length_;
@@ -995,8 +995,8 @@ class curve_segment_evaluator {
// 8.9.3.30 IfcDirection https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/lexical/IfcDirection.htm
// "The IfcDirection does not imply a vector length, and the direction ratios does not have to be normalized."
//
// Therefore, the direction ratios need to be normalized to compute points on the line.
//
// Therefore, the direction ratios need to be normalized to compute points on the line.
//
// Magnitude is not used because it relates to the parameterization of the line, which isn't currently done for IfcCurveSegment
// @todo - parameterization was recently added so Magnitude needs to be taking into consideration
auto dr = l.Dir().Orientation().DirectionRatios();
@@ -1030,7 +1030,7 @@ class curve_segment_evaluator {
auto pcDZy = curve_segment_placement_ ? (*curve_segment_placement_)(1, 2) : 0.;
auto pcDZz = curve_segment_placement_ ? (*curve_segment_placement_)(2, 2) : 1.;
parent_curve_fn_ = std::make_shared<line_parent_curve>(
[segment_type = segment_type_,pcX, pcY, pcDXx, pcDXy, pcDZy, pcDZz, convert_u](double u)->Eigen::Matrix4d {
u = convert_u(u);
@@ -56,7 +56,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcExtrudedAreaSolidTapered& in
if (has_position) {
matrix = taxonomy::cast<taxonomy::matrix4>(map(inst.Position()));
}
loft->matrix = matrix;
return loft;
+2 -2
View File
@@ -24,7 +24,7 @@ using namespace ifcopenshell::geom;
#ifdef SCHEMA_HAS_IfcIndexedPolyCurve
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcIndexedPolyCurve& inst) {
auto point_list = inst.Points();
std::vector< std::vector<double> > coordinates;
if (point_list.as<IfcSchema::IfcCartesianPointList2D>()) {
@@ -100,7 +100,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcIndexedPolyCurve& inst) {
previous = current;
}
}
return loop;
}
+1 -1
View File
@@ -32,7 +32,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcLShapeProfileDef& inst) {
const double x = inst.Width().value_or(inst.Depth()) / 2.0f * length_unit_;
const double d = inst.Thickness() * length_unit_;
const double slope = inst.LegSlope().value_or(0.) * angle_unit_;
double f1 = 0.0f;
double f2 = 0.0f;
if (doFillet) {
@@ -84,11 +84,11 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances& inst
double pz = first_offset_value.OffsetVertical().value_or(0.0);
py *= length_unit_;
pz *= length_unit_;
auto fn = [py, pz](double /*u*/) -> Eigen::Matrix4d {
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m.col(3)(1) = py;
m.col(3)(2) = pz;
auto fn = [py, pz](double /*u*/) -> Eigen::Matrix4d {
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m.col(3)(1) = py;
m.col(3)(2) = pz;
return m; };
offset_spans.emplace_back(taxonomy::make<taxonomy::functor_item>(first_distance, fn));
}
@@ -146,7 +146,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances& inst
zn = zn_at_end;
}
auto fn = [yp, yn, zp, zn, l](double u) -> Eigen::Matrix4d {
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m.col(3)(1) = (l == 0.0 ? yp : (yp + (yn - yp) * u / l));
@@ -171,10 +171,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances& inst
py *= length_unit_;
pz *= length_unit_;
double l = basis_curve_length - last_distance;
auto fn = [py, pz](double /*u*/) -> Eigen::Matrix4d {
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m.col(3)(1) = py;
m.col(3)(2) = pz;
auto fn = [py, pz](double /*u*/) -> Eigen::Matrix4d {
Eigen::Matrix4d m = Eigen::Matrix4d::Identity();
m.col(3)(1) = py;
m.col(3)(2) = pz;
return m; };
offset_spans.emplace_back(taxonomy::make<taxonomy::functor_item>(l, fn));
@@ -87,7 +87,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOpenCrossProfileDef& inst) {
auto r = taxonomy::loop::ptr((taxonomy::loop*)mapped->clone_());
r->closed = false;
return r;
}*/
}*/
mapped->closed = false;
mapped->tags = tags;
+1 -1
View File
@@ -24,5 +24,5 @@ using namespace ifcopenshell::geom;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPlane& inst) {
auto p = taxonomy::make<taxonomy::plane>();
p->matrix = taxonomy::cast<taxonomy::matrix4>(map(inst.Position()));
return p;
return p;
}
@@ -45,7 +45,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPointByDistanceExpression& i
if (inst.OffsetLateral().has_value()) {
auto offset_lateral = inst.OffsetLateral().value() * length_unit_;
auto y = Eigen::Vector3d(m.col(1)(0), m.col(1)(1), m.col(1)(2));
auto y = Eigen::Vector3d(m.col(1)(0), m.col(1)(1), m.col(1)(2));
o += offset_lateral * y;
}
+1 -1
View File
@@ -50,7 +50,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolyLoop& inst) {
int count = polygon.size();
if (original_count - count != 0) {
std::stringstream ss; ss << (original_count - count) << " edges removed for:";
std::stringstream ss; ss << (original_count - count) << " edges removed for:";
logger_.message(ifcopenshell::logger::LOG_WARNING, "GEO", 280, ss.str(), inst);
}
+1 -1
View File
@@ -61,7 +61,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet& inst) {
for (auto& f : polygonal_faces) {
auto fa = taxonomy::make<taxonomy::face>();
shell->children.push_back(fa);
{
auto loop = taxonomy::make<taxonomy::loop>();
fa->children = { loop };
+2 -2
View File
@@ -44,7 +44,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcProduct& inst) {
}
if (openings->size() && !settings_.get(IteratorSettings::DISABLE_OPENING_SUBTRACTIONS) && use_body) {
Eigen::Matrix4d ci;
if (c->matrix.components_) {
ci = c->matrix.components_->inverse();
@@ -69,7 +69,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcProduct& inst) {
c->children = { child };
} else {
delete c;
return nullptr;
return nullptr;
}
}
@@ -33,7 +33,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcRectangleHollowProfileDef& i
const double r1 = fr1 ? (*inst.OuterFilletRadius()) * length_unit_ : 0.;
const double r2 = fr2 ? (*inst.InnerFilletRadius()) * length_unit_ : 0.;
const double tol = settings_.get<settings::Precision>().get();
if (x < tol || y < tol) {
@@ -42,7 +42,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcRectangleProfileDef& inst) {
if (has_position) {
m4 = taxonomy::cast<taxonomy::matrix4>(map(inst.Position()));
}
return profile_helper(m4, {
{{-x,-y}},
{{x,-y}},
+1 -1
View File
@@ -27,7 +27,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcRevolvedAreaSolid& inst) {
const double ang = inst.Angle() * angle_unit_;
taxonomy::cast<taxonomy::face>(map(inst.SweptArea()));
std::optional<double> angle;
taxonomy::matrix4::ptr matrix;
@@ -32,7 +32,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcRightCircularCylinder&) {
BRepPrimAPI_MakeCylinder builder(r, h);
gp_Trsf trsf;
ifcopenshell::geom::Kernel::convert(inst.Position(),trsf);
// IfcCsgPrimitive3D.Position has unit scale factor
shape = builder.Solid().Moved(trsf);
@@ -38,7 +38,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal& in
return nullptr;
}
{
{
auto css = inst.CrossSections();
auto csps = inst.CrossSectionPositions();
std::vector<taxonomy::face::ptr> faces;
@@ -47,7 +47,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal& in
// reference frame along a certain curve location (b) the longitude.
// The longitudes determine the range of the sweep and the offsets are interpolated in between
// sweep segments.
// sweep segments.
std::vector<Eigen::Vector3d> profile_offsets;
std::vector<std::optional<Eigen::Matrix3d>> profile_rotations;
std::vector<double> longitudes;
+2 -2
View File
@@ -39,7 +39,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface& inst) {
}
{
{
auto css = inst.CrossSections();
auto csps = inst.CrossSectionPositions();
std::vector<taxonomy::geom_item::ptr> faces;
@@ -48,7 +48,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface& inst) {
// reference frame along a certain curve location (b) the longitude.
// The longitudes determine the range of the sweep and the offsets are interpolated in between
// sweep segments.
// sweep segments.
std::vector<Eigen::Vector3d> profile_offsets;
std::vector<std::optional<Eigen::Matrix3d>> profile_rotations;
std::vector<double> longitudes;
@@ -28,7 +28,7 @@ using namespace ifcopenshell::geom;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve& inst) {
if (!inst.BaseCurve().as<IfcSchema::IfcGradientCurve>())
logger_.warning("GEO", 291, "Expected IfcSegmentedReferenceCurve.BaseCurve to be IfcGradient", inst); // CT 4.1.7.1.1.3
auto segments = inst.Segments();
taxonomy::piecewise_function::span_list spans;
+6 -6
View File
@@ -97,9 +97,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid& inst) {
return taxonomy::make<taxonomy::sweep_along_curve>(taxonomy::make<taxonomy::matrix4>(), f, nullptr, loop);
/*
TopoDS_Wire wire, section1, section2;
bool hasInnerRadius = !!inst.InnerRadius();
@@ -107,7 +107,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid& inst) {
if (!convert_wire(inst.Directrix(), wire)) {
return false;
}
if (util::count(wire, TopAbs_EDGE) == 1 && sp && ep) {
@@ -119,7 +119,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid& inst) {
double a, b;
auto crv = BRep_Tool::Curve(e, a, b);
if ((crv->DynamicType() == STANDARD_TYPE(Geom_Circle)) ||
(crv->DynamicType() == STANDARD_TYPE(Geom_Ellipse)))
(crv->DynamicType() == STANDARD_TYPE(Geom_Ellipse)))
{
BRepBuilderAPI_MakeEdge me(crv, *sp, *ep);
if (me.IsDone()) {
@@ -227,7 +227,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid& inst) {
j += 1;
} else {
ifcopenshell::logger::root().error("Unexpected amount of fillet edges generated");
}
}
} else {
ifcopenshell::logger::root().error("Unable to build fillet, probably edge too short");
}
@@ -258,7 +258,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid& inst) {
// NB: Note that StartParam and EndParam param are ignored and the assumption is
// made that the parametric range over which to be swept matches the IfcCurve in
// its entirety.
util::process_sweep(wire, inst.Radius() * length_unit_, shape);
if (shape.IsNull()) {
+1 -1
View File
@@ -43,7 +43,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcTShapeProfileDef& inst) {
logger_.message(ifcopenshell::logger::LOG_NOTICE, "GEO", 296, "Skipping zero sized profile:", inst);
return nullptr;
}
double dy1 = 0.0f;
double dy2 = 0.0f;
double dx1 = 0.0f;
+4 -4
View File
@@ -29,21 +29,21 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcTrimmedCurve& inst) {
auto basis_curve = inst.BasisCurve();
bool isConic = basis_curve.declaration().is(IfcSchema::IfcConic::Class());
double parameterFactor = isConic ? angle_unit_ : length_unit_;
auto tc = taxonomy::make<taxonomy::edge>();
tc->basis = map(inst.BasisCurve());
bool trim_cartesian = inst.MasterRepresentation() != IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER;
auto trims1 = inst.Trim1();
auto trims2 = inst.Trim2();
// reversed orientation handling happens in geometry kernel
unsigned sense_agreement = 0;
double flts[2];
taxonomy::point3::ptr pnts[2];
bool has_flts[2] = {false,false};
bool has_pnts[2] = {false,false};
tc->curve_sense = inst.SenseAgreement();
for (auto it = trims1.begin(); it != trims1.end(); it ++) {
+1 -1
View File
@@ -33,7 +33,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcUShapeProfileDef& inst) {
const double d1 = inst.WebThickness() * length_unit_;
const double d2 = inst.FlangeThickness() * length_unit_;
const double slope = inst.FlangeSlope().value_or(0.) * angle_unit_;
double dy1 = 0.0f;
double dy2 = 0.0f;
double f1 = 0.0f;
+1 -1
View File
@@ -31,7 +31,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcZShapeProfileDef& inst) {
bool doFillet = !!inst.FilletRadius();
bool doEdgeFillet = !!inst.EdgeRadius();
double f1 = 0.;
double f2 = 0.;
+8 -8
View File
@@ -69,7 +69,7 @@ std::vector<IfcSchema::IfcProduct> mapping::products_represented_by(const IfcSch
auto invs = prodrep.file()->get_inverse(prodrep.id(), &IfcSchema::IfcProduct::Class(), -1);
for (auto& inv : invs) {
products.push_back(inv.as<IfcSchema::IfcProduct>());
}
}
}
if (only_direct) {
@@ -169,7 +169,7 @@ bool mapping::reuse_ok_(const std::vector<IfcSchema::IfcProduct>& products) {
std::vector<express::base> mapping::find_openings(const express::base& inst) {
std::vector<express::base> openings;
if (auto rep = inst.as<IfcSchema::IfcRepresentation>()) {
// @todo this is essentially only for hybrid kernel trying to guess
// when not to use a simple kernel.
@@ -246,11 +246,11 @@ void mapping::get_representations(std::vector<geometry_conversion_task>& tasks,
int task_index = 0;
std::set<IfcSchema::IfcProduct> products_seen;
for (auto representation : representations) {
IfcSchema::IfcRepresentationMap rmap;
std::vector<IfcSchema::IfcProduct> ifcproducts = filter_products(products_represented_by(representation, rmap, false), filters);
if (ifcproducts.empty()) {
continue;
}
@@ -838,7 +838,7 @@ express::base mapping::get_decomposing_entity(const express::base& inst, bool in
for (auto it = parents.begin(); it != parents.end(); ++it) {
IfcSchema::IfcRelDecomposes decompose = (*it).as<IfcSchema::IfcRelDecomposes>();
express::base ifc_objectdef;
ifc_objectdef = get_RelatingObject(decompose);
if (!ifc_objectdef || product == ifc_objectdef) continue;
@@ -871,7 +871,7 @@ void mapping::initialize_units_() {
length_unit_ = 1.;
angle_unit_ = -1.;
length_unit_name_ = "METER";
#ifdef SCHEMA_HAS_IfcContext
auto projects = file_->instances_by_type<IfcSchema::IfcContext>();
#else
@@ -1133,7 +1133,7 @@ bool mapping::get_layerset_information(const express::base& p, layerset_informat
std::vector<IfcSchema::IfcExtrudedAreaSolid> extrusions;
for (auto& r : resources) {
if (auto ex = r.as<IfcSchema::IfcExtrudedAreaSolid>()) {
extrusions.push_back(ex);
extrusions.push_back(ex);
}
}
@@ -1209,7 +1209,7 @@ bool mapping::get_layerset_information(const express::base& p, layerset_informat
}
return true;
}
+3 -3
View File
@@ -16,7 +16,7 @@
namespace ifcopenshell {
namespace geom {
class POSTFIX_SCHEMA(mapping) : public abstract_mapping {
private:
ifcopenshell::file* file_;
@@ -30,7 +30,7 @@ namespace geom {
const express::base placement_rel_to_instance_;
Eigen::Matrix4d offset_and_rotation_ = Eigen::Matrix4d::Identity();
void initialize_units_();
void addRepresentationsFromContextIds(std::vector<IfcSchema::IfcRepresentation>&);
void addRepresentationsFromPriorities(std::vector<IfcSchema::IfcRepresentation>&);
@@ -136,7 +136,7 @@ namespace geom {
template <>
struct element_type<taxonomy::solid> {
typedef taxonomy::shell type;
};
};
template <typename U = taxonomy::collection, typename T>
typename U::ptr map_to_collection(POSTFIX_SCHEMA(mapping)* m, const T& ts) {
+1 -1
View File
@@ -182,4 +182,4 @@ BIND(IfcSurfaceStyle); // -> style
#ifdef SCHEMA_HAS_IfcCurveSegment
BIND(IfcCurveSegment);
#endif
#endif
+3 -3
View File
@@ -7,8 +7,8 @@ taxonomy::loop::ptr ifcopenshell::geom::fillet_loop(taxonomy::loop::ptr loop, do
const auto child_count = static_cast<int>(loop->children.size());
for (int b = 0; b < child_count; ++b) {
int c = (b + child_count - 1) % child_count;
pps[b] = {
std::get<taxonomy::point3::ptr>(loop->children[c]->start)->ccomponents(),
pps[b] = {
std::get<taxonomy::point3::ptr>(loop->children[c]->start)->ccomponents(),
radius, loop->children[c], loop->children[b]
};
}
@@ -227,7 +227,7 @@ std::pair<std::vector<taxonomy::point3::ptr>, std::vector<std::set<std::string>>
if (equal) {
// do not remove the first or last point to
// maintain connectivity with other wires
/*
// Only removing direct equality so does not impact connectivity
if ((closed && j == 0) || (!closed && j == (n - 1))) {

Some files were not shown because too many files have changed in this diff Show More