From 1cd32be2fc7e2d59eb296c1fd34ae6e8ce164d7e Mon Sep 17 00:00:00 2001 From: krande Date: Wed, 27 Aug 2025 19:37:47 +0200 Subject: [PATCH] Refactor `test_sweeps.py`: remove `_is_swept_shape`, restructure test directories, update test fixtures, and adjust assertions for geometry validation. Extend `sweep_along_curve` constructor to support fixed reference direction. --- .../IfcFixedReferenceSweptAreaSolid.cpp | 7 +- src/ifcgeom/taxonomy.h | 4 +- .../test/fixtures/geom}/pipe.ifc | 4 +- .../test/fixtures/geom}/simple_sweep_1.ifc | 20 +- .../test/fixtures/geom}/simple_sweep_2.ifc | 28 +-- .../test/geom/test_sweeps.py | 138 ++++++++++ test/test_sweeps.py | 235 ------------------ 7 files changed, 168 insertions(+), 268 deletions(-) rename {test/input_temp => src/ifcopenshell-python/test/fixtures/geom}/pipe.ifc (98%) rename {test/input_temp => src/ifcopenshell-python/test/fixtures/geom}/simple_sweep_1.ifc (84%) rename {test/input_temp => src/ifcopenshell-python/test/fixtures/geom}/simple_sweep_2.ifc (70%) create mode 100644 src/ifcopenshell-python/test/geom/test_sweeps.py delete mode 100644 test/test_sweeps.py diff --git a/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp b/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp index 6506705605..e462fc75e0 100644 --- a/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp +++ b/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp @@ -123,11 +123,8 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid auto sweep = taxonomy::make( matrix, // matrix4::ptr - no transformation needed profile, // face::ptr - the profile to sweep - dir // item::ptr curve - the directrix curve - ); - - // Set the fixed reference direction for the sweep - sweep->direction = ref; // The fixed reference direction + dir, // item::ptr curve - the directrix curve + ref); return sweep; } diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index d099579cc9..94e2e7bf52 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -1215,10 +1215,10 @@ typedef item const* ptr; virtual sweep_along_curve* clone_() const { return new sweep_along_curve(*this); } virtual kinds kind() const { return SWEEP_ALONG_CURVE; } - sweep_along_curve(matrix4::ptr m, face::ptr basis, item::ptr surf, item::ptr crv) : sweep(m, basis), surface(surf), curve(crv) {} + sweep_along_curve(matrix4::ptr m, face::ptr basis, item::ptr surf, item::ptr crv) : sweep(m, basis), surface(surf), curve(crv), direction(nullptr) {} // New constructor for fixed reference swept area solid - sweep_along_curve(matrix4::ptr m, face::ptr profile, item::ptr directrix) : sweep(m, profile), surface(nullptr), curve(directrix) { } + sweep_along_curve(matrix4::ptr m, face::ptr profile, item::ptr directrix, direction3::ptr ref) : sweep(m, profile), surface(nullptr), curve(directrix), direction(ref) { } virtual size_t calc_hash() const { auto v = std::make_tuple(static_cast(SWEEP_ALONG_CURVE), matrix->hash_components(), basis->calc_hash(), surface->calc_hash(), curve->calc_hash()); diff --git a/test/input_temp/pipe.ifc b/src/ifcopenshell-python/test/fixtures/geom/pipe.ifc similarity index 98% rename from test/input_temp/pipe.ifc rename to src/ifcopenshell-python/test/fixtures/geom/pipe.ifc index 724ad84230..2c49742dda 100644 --- a/test/input_temp/pipe.ifc +++ b/src/ifcopenshell-python/test/fixtures/geom/pipe.ifc @@ -30,8 +30,8 @@ DATA; #30=IFCCARTESIANPOINT((0.,0.)); #31=IFCAXIS2PLACEMENT2D(#30,$); #32=IFCCIRCLEPROFILEDEF(.AREA.,$,#31,0.179); -#33=IFCCARTESIANPOINT((289080.818499209,5822851.36621952,118.886)); -#34=IFCCARTESIANPOINT((289081.570466963,5822857.54453541,118.886)); +#33=IFCCARTESIANPOINT((289.080,582.2,118.886)); +#34=IFCCARTESIANPOINT((290.081,582.9,118.886)); #35=IFCPOLYLINE((#33,#34)); #36=IFCDIRECTION((1.,0.,0.)); #37=IFCFIXEDREFERENCESWEPTAREASOLID(#32,$,#35,$,$,#36); diff --git a/test/input_temp/simple_sweep_1.ifc b/src/ifcopenshell-python/test/fixtures/geom/simple_sweep_1.ifc similarity index 84% rename from test/input_temp/simple_sweep_1.ifc rename to src/ifcopenshell-python/test/fixtures/geom/simple_sweep_1.ifc index 780d436bd7..0afe5fc07e 100644 --- a/test/input_temp/simple_sweep_1.ifc +++ b/src/ifcopenshell-python/test/fixtures/geom/simple_sweep_1.ifc @@ -1,11 +1,11 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); -FILE_NAME('/dev/null','2025-08-26T16:01:38+02:00',('AdaUser'),('AdaOrg'),'IfcOpenShell 0.8.2','IfcOpenShell 0.8.2','Nobody'); +FILE_NAME('/dev/null','2025-08-27T14:53:55+02:00',('AdaUser'),('AdaOrg'),'IfcOpenShell 0.8.2','IfcOpenShell 0.8.2','Nobody'); FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; -#1=IFCPROJECT('1TMwI763XBx9E7h3kqXonB',$,'AdaProject',$,$,$,$,(#11),#6); +#1=IFCPROJECT('2MG118AczBnBRhfOi7rLGC',$,'AdaProject',$,$,$,$,(#11),#6); #2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); #3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); #4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); @@ -24,18 +24,18 @@ DATA; #17=IFCORGANIZATION('ADA','Assembly For Design and Analysis',$,$,$); #18=IFCPERSONANDORGANIZATION(#16,#17,$); #19=IFCAPPLICATION(#17,'XXX','ADA','ADA'); -#20=IFCOWNERHISTORY(#18,#19,.READWRITE.,$,$,#18,#19,1756216898); +#20=IFCOWNERHISTORY(#18,#19,.READWRITE.,$,$,#18,#19,1756299235); #21=IFCDIRECTION((0.,0.,1.)); #22=IFCDIRECTION((1.,0.,0.)); #23=IFCCARTESIANPOINT((0.,0.,0.)); #24=IFCAXIS2PLACEMENT3D(#23,#21,#22); #25=IFCLOCALPLACEMENT($,#24); -#26=IFCSITE('301mrn5JHCxRMMvjp9uGM1',#20,'Ada',$,$,#25,$,$,.ELEMENT.,$,$,$,$,$); -#27=IFCRELAGGREGATES('0RnSPGnurA1RhM0uNbRZ8k',#20,'Project Container',$,#1,(#26)); +#26=IFCSITE('2V9fV9zKnAP9J5CfXLkqps',#20,'Ada',$,$,#25,$,$,.ELEMENT.,$,$,$,$,$); +#27=IFCRELAGGREGATES('12FX4aagX3HQdiLOLDvGX_',#20,'Project Container',$,#1,(#26)); #28=IFCPROPERTYSINGLEVALUE('project',$,IFCTEXT('AdaProject'),$); #29=IFCPROPERTYSINGLEVALUE('schema',$,IFCTEXT('IFC4X3_add2'),$); -#30=IFCPROPERTYSET('1Yj9$v$uH0ZQk9AOoWx4hD',#20,'Properties',$,(#28,#29)); -#31=IFCRELDEFINESBYPROPERTIES('2zKQjOOuP0VwvxPEpnJusI',#20,'Properties',$,(#26),#30); +#30=IFCPROPERTYSET('2yltt4JCTAOAyhAFFKmDBW',#20,'Properties',$,(#28,#29)); +#31=IFCRELDEFINESBYPROPERTIES('3U0CyjG8XALx2oClwZMPg9',#20,'Properties',$,(#26),#30); #32=IFCMATERIAL('S355',$,'Steel'); #33=IFCPROPERTYSINGLEVALUE('Grade',$,IFCTEXT('S355'),$); #34=IFCPROPERTYSINGLEVALUE('YieldStress',$,IFCPRESSUREMEASURE(355000000.),$); @@ -45,7 +45,7 @@ DATA; #38=IFCPROPERTYSINGLEVALUE('SpecificHeatCapacity',$,IFCSPECIFICHEATCAPACITYMEASURE(0.03),$); #39=IFCPROPERTYSINGLEVALUE('MassDensity',$,IFCMASSDENSITYMEASURE(7850.),$); #40=IFCMATERIALPROPERTIES('MaterialMechanical','A Material property description',(#33,#34,#35,#36,#37,#38,#39),#32); -#41=IFCRELASSOCIATESMATERIAL('13SLSaNyz4PwOBYuDhXcWi',#20,'S355','Objects related to S355',(#64),#32); +#41=IFCRELASSOCIATESMATERIAL('3__DXGCfr15fk06ugmrrR4',#20,'S355','Objects related to S355',(#64),#32); #42=IFCDIRECTION((0.,0.,1.)); #43=IFCDIRECTION((1.,0.,0.)); #44=IFCCARTESIANPOINT((0.,0.,0.)); @@ -68,7 +68,7 @@ DATA; #61=IFCSURFACESTYLESHADING(#60,0.); #62=IFCSURFACESTYLE('Color1',.BOTH.,(#61)); #63=IFCSTYLEDITEM(#57,(#62),'Color1'); -#64=IFCBUILDINGELEMENTPROXY('1R8AUxDeX02RhatZPFgzSt',#20,'sweep1',$,$,#46,#59,$,$); -#65=IFCRELCONTAINEDINSPATIALSTRUCTURE('0HNU_Fs7r0SRBiQ9Xqgv$k',#20,'Physical model',$,(#64),#26); +#64=IFCBUILDINGELEMENTPROXY('07ngBzsO5BK8C3rDUqQfSD',#20,'sweep1',$,$,#46,#59,$,$); +#65=IFCRELCONTAINEDINSPATIALSTRUCTURE('1_r$SsBj1C3wgAjnhfmkqs',#20,'Physical model',$,(#64),#26); ENDSEC; END-ISO-10303-21; diff --git a/test/input_temp/simple_sweep_2.ifc b/src/ifcopenshell-python/test/fixtures/geom/simple_sweep_2.ifc similarity index 70% rename from test/input_temp/simple_sweep_2.ifc rename to src/ifcopenshell-python/test/fixtures/geom/simple_sweep_2.ifc index 11d70bcf81..7181f44337 100644 --- a/test/input_temp/simple_sweep_2.ifc +++ b/src/ifcopenshell-python/test/fixtures/geom/simple_sweep_2.ifc @@ -1,16 +1,16 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); -FILE_NAME('/dev/null','2025-08-27T11:12:44+02:00',('AdaUser'),('AdaOrg'),'IfcOpenShell 0.8.2','IfcOpenShell 0.8.2','Nobody'); +FILE_NAME('/dev/null','2025-08-27T15:03:01+02:00',('AdaUser'),('AdaOrg'),'IfcOpenShell 0.8.2','IfcOpenShell 0.8.2','Nobody'); FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; -#1=IFCPROJECT('0bfgwqvaX7Vvg12DS5E4TH',$,'AdaProject',$,$,$,$,(#11),#6); +#1=IFCPROJECT('1tjHZhewf3AROX1xjpvefY',$,'AdaProject',$,$,$,$,(#11),#6); #2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); #3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); #4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); #5=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); -#6=IFCUNITASSIGNMENT((#4,#2,#5,#3)); +#6=IFCUNITASSIGNMENT((#5,#4,#2,#3)); #7=IFCCARTESIANPOINT((0.,0.,0.)); #8=IFCDIRECTION((0.,0.,1.)); #9=IFCDIRECTION((1.,0.,0.)); @@ -24,18 +24,18 @@ DATA; #17=IFCORGANIZATION('ADA','Assembly For Design and Analysis',$,$,$); #18=IFCPERSONANDORGANIZATION(#16,#17,$); #19=IFCAPPLICATION(#17,'XXX','ADA','ADA'); -#20=IFCOWNERHISTORY(#18,#19,.READWRITE.,$,$,#18,#19,1756285964); +#20=IFCOWNERHISTORY(#18,#19,.READWRITE.,$,$,#18,#19,1756299781); #21=IFCDIRECTION((0.,0.,1.)); #22=IFCDIRECTION((1.,0.,0.)); #23=IFCCARTESIANPOINT((0.,0.,0.)); #24=IFCAXIS2PLACEMENT3D(#23,#21,#22); #25=IFCLOCALPLACEMENT($,#24); -#26=IFCSITE('1GDQO$O1L1QOKEaI81COHL',#20,'Ada',$,$,#25,$,$,.ELEMENT.,$,$,$,$,$); -#27=IFCRELAGGREGATES('34vxVzLRfCB8P7OThopnom',#20,'Project Container',$,#1,(#26)); +#26=IFCSITE('1$h1D7pKLBhQ2roRvKz9Kq',#20,'Ada',$,$,#25,$,$,.ELEMENT.,$,$,$,$,$); +#27=IFCRELAGGREGATES('1cbS0eMMz5482GelhPUH3D',#20,'Project Container',$,#1,(#26)); #28=IFCPROPERTYSINGLEVALUE('project',$,IFCTEXT('AdaProject'),$); #29=IFCPROPERTYSINGLEVALUE('schema',$,IFCTEXT('IFC4X3_add2'),$); -#30=IFCPROPERTYSET('2hTgvJbh1B7RR4nwsOfRDy',#20,'Properties',$,(#28,#29)); -#31=IFCRELDEFINESBYPROPERTIES('2wzYfhPqT9hwqI5vOO4yex',#20,'Properties',$,(#26),#30); +#30=IFCPROPERTYSET('2Jj1ZXLcTFjxFj$8jn9r9Z',#20,'Properties',$,(#28,#29)); +#31=IFCRELDEFINESBYPROPERTIES('30tu3wf014T9Gbje7aeoNI',#20,'Properties',$,(#26),#30); #32=IFCMATERIAL('S355',$,'Steel'); #33=IFCPROPERTYSINGLEVALUE('Grade',$,IFCTEXT('S355'),$); #34=IFCPROPERTYSINGLEVALUE('YieldStress',$,IFCPRESSUREMEASURE(355000000.),$); @@ -45,16 +45,16 @@ DATA; #38=IFCPROPERTYSINGLEVALUE('SpecificHeatCapacity',$,IFCSPECIFICHEATCAPACITYMEASURE(0.03),$); #39=IFCPROPERTYSINGLEVALUE('MassDensity',$,IFCMASSDENSITYMEASURE(7850.),$); #40=IFCMATERIALPROPERTIES('MaterialMechanical','A Material property description',(#33,#34,#35,#36,#37,#38,#39),#32); -#41=IFCRELASSOCIATESMATERIAL('0vr_O6t$T7vPWIa1EqEt6b',#20,'S355','Objects related to S355',(#64),#32); +#41=IFCRELASSOCIATESMATERIAL('1kAam6k_5CO9mOkVDyk7FN',#20,'S355','Objects related to S355',(#64),#32); #42=IFCDIRECTION((0.,0.,1.)); #43=IFCDIRECTION((1.,0.,0.)); #44=IFCCARTESIANPOINT((0.,0.,0.)); #45=IFCAXIS2PLACEMENT3D(#44,#42,#43); #46=IFCLOCALPLACEMENT(#25,#45); -#47=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,0.1),(0.1,0.),(0.1,0.1)),$); -#48=IFCINDEXEDPOLYCURVE(#47,(IFCLINEINDEX((3,1)),IFCLINEINDEX((1,2)),IFCLINEINDEX((2,4)),IFCLINEINDEX((4,3))),$); +#47=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.1,0.),(0.1,0.05)),$); +#48=IFCINDEXEDPOLYCURVE(#47,(IFCLINEINDEX((3,1)),IFCLINEINDEX((1,2)),IFCLINEINDEX((2,3))),$); #49=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#48); -#50=IFCCARTESIANPOINTLIST3D(((-100.,-50.,199.610854),(-100.,-50.,200.),(-99.974928791787,-49.9996866104928,199.513897412378),(-99.9060013418686,-49.998825019006,199.441251),(-99.3499585838692,-49.9918744730554,199.093724161494),(-99.2406758662487,-49.9607719931374,198.987781372709),(-99.2000000393918,-49.8803717417563,198.861230109006),(-99.2000000393918,-49.3859696738752,198.235404705615),(-99.1121322022443,-49.2544691526923,198.068948349419),(-98.9000000393918,-49.2000000638752,198.000000135615),(-98.3,-49.1999999172672,197.999999950035)),$); +#50=IFCCARTESIANPOINTLIST3D(((100.,50.,-200.389146),(100.,50.,-200.),(100.025071208213,50.0003133895072,-200.486102587622),(100.093998658131,50.001174980994,-200.558749),(100.650041416131,50.0081255269446,-200.906275838506),(100.759324133751,50.0392280068626,-201.012218627291),(100.799999960608,50.1196282582437,-201.138769890994),(100.799999960608,50.6140303261248,-201.764595294385),(100.887867797756,50.7455308473077,-201.931051650581),(101.099999960608,50.7999999361248,-201.999999864385),(101.7,50.8000000827328,-202.000000049965)),$); #51=IFCINDEXEDPOLYCURVE(#50,(IFCLINEINDEX((2,1)),IFCARCINDEX((1,3,4)),IFCLINEINDEX((4,5)),IFCARCINDEX((5,6,7)),IFCLINEINDEX((7,8)),IFCARCINDEX((8,9,10)),IFCLINEINDEX((10,11))),$); #52=IFCDIRECTION((1.,-0.,0.)); #53=IFCCARTESIANPOINT((0.,0.,0.)); @@ -68,7 +68,7 @@ DATA; #61=IFCSURFACESTYLESHADING(#60,0.); #62=IFCSURFACESTYLE('Color1',.BOTH.,(#61)); #63=IFCSTYLEDITEM(#57,(#62),'Color1'); -#64=IFCBUILDINGELEMENTPROXY('0pDZhwPyT3dRMnPGA47kAn',#20,'sweep1',$,$,#46,#59,$,$); -#65=IFCRELCONTAINEDINSPATIALSTRUCTURE('196Q7zPfr4LxqeArD0gmJa',#20,'Physical model',$,(#64),#26); +#64=IFCBUILDINGELEMENTPROXY('34gqNt7M58Z972x$etr9V2',#20,'sweep2',$,$,#46,#59,$,$); +#65=IFCRELCONTAINEDINSPATIALSTRUCTURE('1kbNj7znLCygBdHO9SJygX',#20,'Physical model',$,(#64),#26); ENDSEC; END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/geom/test_sweeps.py b/src/ifcopenshell-python/test/geom/test_sweeps.py new file mode 100644 index 0000000000..6f0ea5b1a8 --- /dev/null +++ b/src/ifcopenshell-python/test/geom/test_sweeps.py @@ -0,0 +1,138 @@ +import pathlib +from typing import List, Sequence, Tuple + +import ifcopenshell +import pytest +from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Compound + + +def _bbox_from_vertices(verts: List[Tuple[float, float, float]]): + if not verts: + return (0, 0, 0), (0, 0, 0) + xs = [v[0] for v in verts] + ys = [v[1] for v in verts] + zs = [v[2] for v in verts] + mn = (min(xs), min(ys), min(zs)) + mx = (max(xs), max(ys), max(zs)) + return mn, mx + + +def _size_from_bbox(mn, mx): + return (mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]) + + +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)] + + +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): + """Load first product's mesh from IFC using ifcopenshell.geom and return bbox and size.""" + 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) + + f = ifcopenshell.open(ifc_path) + # Prefer the proxy we created, otherwise take 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 validation") + + shape = geom.create_shape(settings, target) + verts = _triples(shape.geometry.verts) + mn, mx = _bbox_from_vertices(verts) + return mn, mx, _size_from_bbox(mn, mx) + + +@pytest.fixture +def geom_dir(): + return pathlib.Path(__file__).parent.parent.resolve().absolute() / "fixtures/geom" + + +def test_simple_sweep_1(geom_dir): + ifc_file_path = geom_dir / "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((0.8957825463853046, 0.1, 1.1)) + assert ifc_mn == pytest.approx((0.0, 0, -0.1)) + assert ifc_mx == pytest.approx((0.8957825463853046, 0.1, 1.0)) + + +def test_simple_sweep_2(geom_dir): + ifc_file_path = geom_dir / "simple_sweep_2.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((0.8764374444355525, 1.800000587893038, 2.095849252263605)) + assert ifc_mn == pytest.approx((50.0, 99.9, 200.0)) + assert ifc_mx == pytest.approx((50.87643744443555, 101.70000058789304, 202.0958492522636)) + + +def test_pipe_12d(geom_dir): + ifc_file_path = geom_dir / "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.205888147422229, 0.9929900508137735, 0.35776115971654576)) + assert ifc_mn == pytest.approx((288.9774190979147, 582.0537006391681, 118.70711942014172)) + assert ifc_mx == pytest.approx((290.18330724533695, 583.0466906899819, 119.06488057985827)) diff --git a/test/test_sweeps.py b/test/test_sweeps.py deleted file mode 100644 index e78fb9499f..0000000000 --- a/test/test_sweeps.py +++ /dev/null @@ -1,235 +0,0 @@ -import pathlib -from typing import List, Sequence, Tuple - -import ifcopenshell -import pytest -from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Compound - - -def _bbox_from_vertices(verts: List[Tuple[float, float, float]]): - if not verts: - return (0, 0, 0), (0, 0, 0) - xs = [v[0] for v in verts] - ys = [v[1] for v in verts] - zs = [v[2] for v in verts] - mn = (min(xs), min(ys), min(zs)) - mx = (max(xs), max(ys), max(zs)) - return mn, mx - - -def _size_from_bbox(mn, mx): - return (mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]) - - -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)] - - -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: - 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): - """Load first product's mesh from IFC using ifcopenshell.geom and return bbox and size.""" - 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) - - f = ifcopenshell.open(ifc_path) - # Prefer the proxy we created, otherwise take 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 validation") - - shape = geom.create_shape(settings, target) - verts = _triples(shape.geometry.verts) - mn, mx = _bbox_from_vertices(verts) - return mn, mx, _size_from_bbox(mn, mx) - - -@pytest.fixture -def test_dir(): - return pathlib.Path(__file__).parent.resolve().absolute() - - -def test_simple_sweep_1(test_dir): - 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_mn == pytest.approx((-1.0, -1.3877787807814457e-17, 0.0)) - assert ifc_mx == pytest.approx((0.10000000000000002, 0.1, 0.8957825463853046)) - - -def test_simple_sweep_2(test_dir): - ifc_file_path = test_dir / "input_temp/simple_sweep_2.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.800000679914902, 0.9243618756667757, 2.0958492522636902)) - assert ifc_mn == pytest.approx((-100.1, -50.0, 197.9041507477363)) - assert ifc_mx == pytest.approx((-98.29999932008509, -49.075638124333224, 200.0)) - - -def test_pipe_12d(test_dir): - 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_mn == pytest.approx((289080.64128449163, 5822851.344592881, 118.70711942014172)) - assert ifc_mx == pytest.approx((289081.7481557032, 5822857.566104354, 119.06488057985827))