mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 11:33:38 +00:00
Improve IfcFixedReferenceSweptAreaSolid handling and add swept shape tests
Enhances geometry processing for swept area solids by integrating matrix transformations. Includes new utility `_is_swept_shape` for topology analysis, extends testing with `load_ifc_occ_shape`, and updates schema versions in CMake presets.
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
"BUILD_IFCMAX": "OFF",
|
"BUILD_IFCMAX": "OFF",
|
||||||
"IFCXML_SUPPORT": "ON",
|
"IFCXML_SUPPORT": "ON",
|
||||||
"HDF5_SUPPORT": "ON",
|
"HDF5_SUPPORT": "ON",
|
||||||
"SCHEMA_VERSIONS": "4x3_add2",
|
"SCHEMA_VERSIONS": "4;4x3_add2",
|
||||||
"CITYJSON_SUPPORT": "OFF",
|
"CITYJSON_SUPPORT": "OFF",
|
||||||
"CMAKE_GENERATOR_PLATFORM": "",
|
"CMAKE_GENERATOR_PLATFORM": "",
|
||||||
"CMAKE_GENERATOR_TOOLSET": ""
|
"CMAKE_GENERATOR_TOOLSET": ""
|
||||||
|
|||||||
@@ -115,9 +115,16 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
taxonomy::matrix4::ptr matrix;
|
||||||
|
bool has_position = true;
|
||||||
|
has_position = inst->Position() != nullptr;
|
||||||
|
auto pos = inst->Position();
|
||||||
|
if (has_position) {
|
||||||
|
matrix = taxonomy::cast<taxonomy::matrix4>(map(inst->Position()));
|
||||||
|
}
|
||||||
// TODO: Implement handling for non-alignment curves using sweep_along_curve
|
// TODO: Implement handling for non-alignment curves using sweep_along_curve
|
||||||
auto sweep = taxonomy::make<taxonomy::sweep_along_curve>(
|
auto sweep = taxonomy::make<taxonomy::sweep_along_curve>(
|
||||||
nullptr, // matrix4::ptr - no transformation needed
|
matrix, // matrix4::ptr - no transformation needed
|
||||||
profile, // face::ptr - the profile to sweep
|
profile, // face::ptr - the profile to sweep
|
||||||
nullptr, // item::ptr surface - not used for fixed reference sweep
|
nullptr, // item::ptr surface - not used for fixed reference sweep
|
||||||
dir // item::ptr curve - the directrix curve
|
dir // item::ptr curve - the directrix curve
|
||||||
|
|||||||
+146
-3
@@ -3,6 +3,8 @@ from typing import List, Sequence, Tuple
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import pytest
|
import pytest
|
||||||
|
from OCC.Core.BRep import BRep_Tool
|
||||||
|
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Compound
|
||||||
|
|
||||||
|
|
||||||
def _bbox_from_vertices(verts: List[Tuple[float, float, float]]):
|
def _bbox_from_vertices(verts: List[Tuple[float, float, float]]):
|
||||||
@@ -23,8 +25,143 @@ def _size_from_bbox(mn, mx):
|
|||||||
def _triples(flat: Sequence[float]) -> List[Tuple[float, float, float]]:
|
def _triples(flat: Sequence[float]) -> List[Tuple[float, float, float]]:
|
||||||
return [(float(flat[i]), float(flat[i + 1]), float(flat[i + 2])) for i in range(0, len(flat), 3)]
|
return [(float(flat[i]), float(flat[i + 1]), float(flat[i + 2])) for i in range(0, len(flat), 3)]
|
||||||
|
|
||||||
|
def _is_swept_shape(occ_shape: TopoDS_Shape) -> bool:
|
||||||
|
"""Analyze if the given OpenCASCADE shape represents a swept shape using topology exploration."""
|
||||||
|
try:
|
||||||
|
from OCC.Core.TopExp import TopExp_Explorer
|
||||||
|
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_WIRE
|
||||||
|
from OCC.Core.BRep_Tool import BRep_Tool
|
||||||
|
from OCC.Core.GeomLProp_SLProps import GeomLProp_SLProps
|
||||||
|
from OCC.Core.BRepAdaptor_Surface import BRepAdaptor_Surface
|
||||||
|
from OCC.Core.GeomAbs import GeomAbs_Cylinder, GeomAbs_Plane, GeomAbs_SurfaceOfExtrusion, GeomAbs_SurfaceOfRevolution
|
||||||
|
from OCC.Core.gp import gp_Vec
|
||||||
|
import math
|
||||||
|
except ImportError as e:
|
||||||
|
raise RuntimeError("pythonocc-core not available for topology analysis") from e
|
||||||
|
|
||||||
|
if occ_shape.IsNull():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Explore faces to look for swept surface characteristics
|
||||||
|
face_explorer = TopExp_Explorer(occ_shape, TopAbs_FACE)
|
||||||
|
swept_indicators = 0
|
||||||
|
total_faces = 0
|
||||||
|
|
||||||
|
while face_explorer.More():
|
||||||
|
face = face_explorer.Current()
|
||||||
|
total_faces += 1
|
||||||
|
|
||||||
|
# Get the surface adaptor for this face
|
||||||
|
surface_adaptor = BRepAdaptor_Surface(face)
|
||||||
|
surface_type = surface_adaptor.GetType()
|
||||||
|
|
||||||
|
# Check for surface types that indicate swept geometry
|
||||||
|
if surface_type in [GeomAbs_Cylinder, GeomAbs_SurfaceOfExtrusion, GeomAbs_SurfaceOfRevolution]:
|
||||||
|
swept_indicators += 1
|
||||||
|
|
||||||
|
# For planar surfaces, check if they form a pattern consistent with swept geometry
|
||||||
|
elif surface_type == GeomAbs_Plane:
|
||||||
|
# Analyze if planar faces are arranged in a swept pattern
|
||||||
|
# This is a simplified check - could be enhanced further
|
||||||
|
if _has_parallel_opposite_faces(occ_shape, face):
|
||||||
|
swept_indicators += 0.5 # Partial indicator
|
||||||
|
|
||||||
|
face_explorer.Next()
|
||||||
|
|
||||||
|
# Additional check: analyze edge patterns for swept characteristics
|
||||||
|
edge_analysis = _analyze_edge_patterns(occ_shape)
|
||||||
|
|
||||||
|
# Determine if this is likely a swept shape
|
||||||
|
# If more than 50% of faces show swept characteristics, or we have strong edge patterns
|
||||||
|
swept_ratio = swept_indicators / max(total_faces, 1)
|
||||||
|
is_swept = swept_ratio > 0.5 or edge_analysis
|
||||||
|
|
||||||
|
if is_swept:
|
||||||
|
print(f"Swept shape analysis: {swept_indicators}/{total_faces} faces show swept characteristics")
|
||||||
|
if edge_analysis:
|
||||||
|
print("Edge pattern analysis also indicates swept geometry")
|
||||||
|
|
||||||
|
return is_swept
|
||||||
|
|
||||||
|
|
||||||
|
def _has_parallel_opposite_faces(shape: TopoDS_Shape, reference_face) -> bool:
|
||||||
|
"""Check if the shape has faces parallel to the reference face (indicating extrusion)."""
|
||||||
|
try:
|
||||||
|
from OCC.Core.TopExp import TopExp_Explorer
|
||||||
|
from OCC.Core.TopAbs import TopAbs_FACE
|
||||||
|
from OCC.Core.BRepAdaptor_Surface import BRepAdaptor_Surface
|
||||||
|
from OCC.Core.GeomAbs import GeomAbs_Plane
|
||||||
|
from OCC.Core.gp import gp_Vec
|
||||||
|
import math
|
||||||
|
|
||||||
|
ref_adaptor = BRepAdaptor_Surface(reference_face)
|
||||||
|
if ref_adaptor.GetType() != GeomAbs_Plane:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ref_normal = ref_adaptor.Plane().Axis().Direction()
|
||||||
|
|
||||||
|
face_explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||||
|
while face_explorer.More():
|
||||||
|
face = face_explorer.Current()
|
||||||
|
if not face.IsSame(reference_face):
|
||||||
|
face_adaptor = BRepAdaptor_Surface(face)
|
||||||
|
if face_adaptor.GetType() == GeomAbs_Plane:
|
||||||
|
face_normal = face_adaptor.Plane().Axis().Direction()
|
||||||
|
# Check if normals are parallel (dot product close to ±1)
|
||||||
|
dot_product = abs(ref_normal.Dot(face_normal))
|
||||||
|
if dot_product > 0.99: # Very close to parallel
|
||||||
|
return True
|
||||||
|
face_explorer.Next()
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def load_ifc_occ_shape(ifc_path: str) -> TopoDS_Shape:
|
def load_ifc_occ_shape(ifc_path: str) -> TopoDS_Shape:
|
||||||
...
|
try:
|
||||||
|
import ifcopenshell.geom as geom
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError("ifcopenshell.geom not available: cannot validate IFC geometry") from e
|
||||||
|
|
||||||
|
settings = geom.settings()
|
||||||
|
settings.set(settings.USE_WORLD_COORDS, True)
|
||||||
|
settings.set("use-python-opencascade", True)
|
||||||
|
|
||||||
|
# Open the IFC file
|
||||||
|
f = ifcopenshell.open(ifc_path)
|
||||||
|
|
||||||
|
# Find the first representable product (prefer IfcBuildingElementProxy, then any product with representation)
|
||||||
|
products = f.by_type("IfcProduct")
|
||||||
|
target = None
|
||||||
|
for p in products:
|
||||||
|
if p.is_a("IfcBuildingElementProxy") and getattr(p, "Representation", None):
|
||||||
|
target = p
|
||||||
|
break
|
||||||
|
if target is None:
|
||||||
|
for p in products:
|
||||||
|
if getattr(p, "Representation", None):
|
||||||
|
target = p
|
||||||
|
break
|
||||||
|
if target is None:
|
||||||
|
raise RuntimeError("No representable product found in IFC for shape extraction")
|
||||||
|
|
||||||
|
# Create the shape using ifcopenshell.geom with opencascade geometry library
|
||||||
|
shape_result = geom.create_shape(settings, target, geometry_library="opencascade")
|
||||||
|
|
||||||
|
# Extract the OpenCASCADE TopoDS_Shape from the result
|
||||||
|
# The create_shape function returns an object with an occ_shape attribute when using opencascade
|
||||||
|
if hasattr(shape_result, 'geometry') and shape_result.geometry:
|
||||||
|
occ_shape = shape_result.geometry
|
||||||
|
if isinstance(occ_shape, TopoDS_Compound):
|
||||||
|
json_data = occ_shape.DumpJson()
|
||||||
|
print(json_data)
|
||||||
|
else:
|
||||||
|
raise NotImplemented(f"Unsupported shape type: {type(occ_shape)}")
|
||||||
|
|
||||||
|
return occ_shape
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Failed to extract OpenCASCADE shape from IFC geometry")
|
||||||
|
|
||||||
|
|
||||||
def load_ifc_mesh_bbox(ifc_path: str):
|
def load_ifc_mesh_bbox(ifc_path: str):
|
||||||
"""Load first product's mesh from IFC using ifcopenshell.geom and return bbox and size."""
|
"""Load first product's mesh from IFC using ifcopenshell.geom and return bbox and size."""
|
||||||
@@ -61,11 +198,17 @@ def load_ifc_mesh_bbox(ifc_path: str):
|
|||||||
def test_dir():
|
def test_dir():
|
||||||
return pathlib.Path(__file__).parent.resolve().absolute()
|
return pathlib.Path(__file__).parent.resolve().absolute()
|
||||||
def test_simple_sweep_1(test_dir):
|
def test_simple_sweep_1(test_dir):
|
||||||
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(test_dir / "input_temp/simple_sweep_1.ifc")
|
ifc_file_path = test_dir / "input_temp/simple_sweep_1.ifc"
|
||||||
|
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(ifc_file_path)
|
||||||
|
occ_shape = load_ifc_occ_shape(ifc_file_path)
|
||||||
|
assert occ_shape is not None
|
||||||
assert ifc_sz == pytest.approx((1.1, 0.1, 0.89578254))
|
assert ifc_sz == pytest.approx((1.1, 0.1, 0.89578254))
|
||||||
|
|
||||||
def test_pipe_12d(test_dir):
|
def test_pipe_12d(test_dir):
|
||||||
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(test_dir / "input_temp/pipe.ifc")
|
ifc_file_path = test_dir / "input_temp/pipe.ifc"
|
||||||
|
ifc_mn, ifc_mx, ifc_sz = load_ifc_mesh_bbox(ifc_file_path)
|
||||||
|
occ_shape = load_ifc_occ_shape(ifc_file_path)
|
||||||
|
assert occ_shape is not None
|
||||||
assert ifc_sz == pytest.approx((1.1068712115520611, 6.2215114729478955, 0.35776115971654576))
|
assert ifc_sz == pytest.approx((1.1068712115520611, 6.2215114729478955, 0.35776115971654576))
|
||||||
assert ifc_mn == pytest.approx((289080.64128449163, 5822851.344592881, 118.70711942014172))
|
assert ifc_mn == pytest.approx((289080.64128449163, 5822851.344592881, 118.70711942014172))
|
||||||
assert ifc_mx == pytest.approx((289081.7481557032, 5822857.566104354, 119.06488057985827))
|
assert ifc_mx == pytest.approx((289081.7481557032, 5822857.566104354, 119.06488057985827))
|
||||||
|
|||||||
Reference in New Issue
Block a user