From ab73550059a3ad209ef0a44d02dc04dac39459b9 Mon Sep 17 00:00:00 2001 From: Ghesselink Date: Mon, 4 May 2026 15:33:34 +0000 Subject: [PATCH 01/12] unblock voxel schema loading, add test for express --- .../ifcopenshell/express/schema_class.py | 17 +++- .../test/test_express_aggregate_bounds.py | 80 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/ifcopenshell-python/test/test_express_aggregate_bounds.py diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index d7595ad6cb..dc399b53d0 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -420,7 +420,13 @@ class SchemaClass(codegen.Base): if isinstance(type, nodes.AggregationType): aggr_type = type.aggregate_type - make_bound = lambda b: -1 if b == "?" else int(b) + def make_bound(b): + # `?` and non-literal bounds (attribute references, arithmetic expressions) collapse to -1. + # + try: + return int(b) + except (TypeError, ValueError): + return -1 bound1, bound2 = map(make_bound, (type.bounds.lower, type.bounds.upper)) decl_type = get_declared_type(type.type, emitted_names) return x.aggregation_type(aggr_type, bound1, bound2, decl_type) @@ -547,7 +553,14 @@ class SchemaClass(codegen.Base): inv_attrs = [] for attr in type.inverse: if attr.bounds: - make_bound = lambda b: -1 if b == "?" else int(b) + def make_bound(b): + # `?` and non-literal bounds (attribute references, arithmetic + # expressions) collapse to -1 (unbounded) — the C++ runtime has + # no third state for "dynamic cardinality". + try: + return int(b) + except (TypeError, ValueError): + return -1 bound1, bound2 = map(make_bound, (attr.bounds.lower, attr.bounds.upper)) else: bound1, bound2 = -1, -1 diff --git a/src/ifcopenshell-python/test/test_express_aggregate_bounds.py b/src/ifcopenshell-python/test/test_express_aggregate_bounds.py new file mode 100644 index 0000000000..030c301a78 --- /dev/null +++ b/src/ifcopenshell-python/test/test_express_aggregate_bounds.py @@ -0,0 +1,80 @@ +import os +import sys +import tempfile +import unittest + +import ifcopenshell.express + +sys.path.insert(0, os.path.dirname(ifcopenshell.express.__file__)) + + +def _parse(schema_text): + with tempfile.NamedTemporaryFile(mode="w", suffix=".exp", delete=False) as f: + f.write(schema_text) + path = f.name + try: + return ifcopenshell.express.parse(path) + finally: + os.unlink(path) + cache = path + ".cache.dat" + if os.path.exists(cache): + os.unlink(cache) + + +class TestAggregateBounds(unittest.TestCase): + def test_literal_bounds_preserved(self): + """After loading [1;3] -> (1, 3)?""" + s = _parse( + "SCHEMA t; ENTITY E; v : ARRAY [1:3] OF REAL; END_ENTITY; END_SCHEMA;" + ) + agg = ( + next(d for d in s.schema.declarations() if d.name() == "E") + .attributes()[0] + .type_of_attribute() + .as_aggregation_type() + ) + self.assertEqual((agg.bound1(), agg.bound2()), (1, 3)) + s.disown() + + def test_unbounded_marker(self): + """ [0:?] -> (0, -1)?""" + s = _parse( + "SCHEMA t; ENTITY E; v : LIST [0:?] OF REAL; END_ENTITY; END_SCHEMA;" + ) + agg = ( + next(d for d in s.schema.declarations() if d.name() == "E") + .attributes()[0] + .type_of_attribute() + .as_aggregation_type() + ) + # import pdb; pdb.set_trace() + self.assertEqual((agg.bound1(), agg.bound2()), (0, -1)) + s.disown() + + def test_voxel_grid_with_dynamic_bound_loads(self): + """ + Array that is an expression : [1:dim_x*dim_y*dim_z] + Parsing must not crash, Bbund must be (1, -1) + """ + s = _parse( + """ + SCHEMA t; + TYPE IfcBoolean = BOOLEAN; END_TYPE; + + ENTITY IfcVoxelHolder; + NumberOfVoxelsX : INTEGER; + NumberOfVoxelsY : INTEGER; + NumberOfVoxelsZ : INTEGER; + Voxels : ARRAY [1:NumberOfVoxelsX*NumberOfVoxelsY*NumberOfVoxelsZ] OF IfcBoolean; + END_ENTITY; + END_SCHEMA; + """ + ) + holder = next(d for d in s.schema.declarations() if d.name() == "IfcVoxelHolder") + voxels = holder.attributes()[-1].type_of_attribute().as_aggregation_type() + self.assertEqual((voxels.bound1(), voxels.bound2()), (1, -1)) + s.disown() + + +if __name__ == "__main__": + unittest.main() From c197a45247d1b7258049ca2c969d43e5932f021d Mon Sep 17 00:00:00 2001 From: Ghesselink Date: Wed, 6 May 2026 11:04:02 +0000 Subject: [PATCH 02/12] Apply black formatting --- .../ifcopenshell/express/schema_class.py | 6 +++++- .../test/test_express_aggregate_bounds.py | 18 ++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index dc399b53d0..3981dbc421 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -420,13 +420,15 @@ class SchemaClass(codegen.Base): if isinstance(type, nodes.AggregationType): aggr_type = type.aggregate_type + def make_bound(b): # `?` and non-literal bounds (attribute references, arithmetic expressions) collapse to -1. - # + # try: return int(b) except (TypeError, ValueError): return -1 + bound1, bound2 = map(make_bound, (type.bounds.lower, type.bounds.upper)) decl_type = get_declared_type(type.type, emitted_names) return x.aggregation_type(aggr_type, bound1, bound2, decl_type) @@ -553,6 +555,7 @@ class SchemaClass(codegen.Base): inv_attrs = [] for attr in type.inverse: if attr.bounds: + def make_bound(b): # `?` and non-literal bounds (attribute references, arithmetic # expressions) collapse to -1 (unbounded) — the C++ runtime has @@ -561,6 +564,7 @@ class SchemaClass(codegen.Base): return int(b) except (TypeError, ValueError): return -1 + bound1, bound2 = map(make_bound, (attr.bounds.lower, attr.bounds.upper)) else: bound1, bound2 = -1, -1 diff --git a/src/ifcopenshell-python/test/test_express_aggregate_bounds.py b/src/ifcopenshell-python/test/test_express_aggregate_bounds.py index 030c301a78..24b81fda5b 100644 --- a/src/ifcopenshell-python/test/test_express_aggregate_bounds.py +++ b/src/ifcopenshell-python/test/test_express_aggregate_bounds.py @@ -24,9 +24,7 @@ def _parse(schema_text): class TestAggregateBounds(unittest.TestCase): def test_literal_bounds_preserved(self): """After loading [1;3] -> (1, 3)?""" - s = _parse( - "SCHEMA t; ENTITY E; v : ARRAY [1:3] OF REAL; END_ENTITY; END_SCHEMA;" - ) + s = _parse("SCHEMA t; ENTITY E; v : ARRAY [1:3] OF REAL; END_ENTITY; END_SCHEMA;") agg = ( next(d for d in s.schema.declarations() if d.name() == "E") .attributes()[0] @@ -37,10 +35,8 @@ class TestAggregateBounds(unittest.TestCase): s.disown() def test_unbounded_marker(self): - """ [0:?] -> (0, -1)?""" - s = _parse( - "SCHEMA t; ENTITY E; v : LIST [0:?] OF REAL; END_ENTITY; END_SCHEMA;" - ) + """[0:?] -> (0, -1)?""" + s = _parse("SCHEMA t; ENTITY E; v : LIST [0:?] OF REAL; END_ENTITY; END_SCHEMA;") agg = ( next(d for d in s.schema.declarations() if d.name() == "E") .attributes()[0] @@ -52,12 +48,11 @@ class TestAggregateBounds(unittest.TestCase): s.disown() def test_voxel_grid_with_dynamic_bound_loads(self): - """ + """ Array that is an expression : [1:dim_x*dim_y*dim_z] Parsing must not crash, Bbund must be (1, -1) """ - s = _parse( - """ + s = _parse(""" SCHEMA t; TYPE IfcBoolean = BOOLEAN; END_TYPE; @@ -68,8 +63,7 @@ class TestAggregateBounds(unittest.TestCase): Voxels : ARRAY [1:NumberOfVoxelsX*NumberOfVoxelsY*NumberOfVoxelsZ] OF IfcBoolean; END_ENTITY; END_SCHEMA; - """ - ) + """) holder = next(d for d in s.schema.declarations() if d.name() == "IfcVoxelHolder") voxels = holder.attributes()[-1].type_of_attribute().as_aggregation_type() self.assertEqual((voxels.bound1(), voxels.bound2()), (1, -1)) From 7aa2bb366e4fadb0ef2a78a73435ba4b73bd2cd6 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 7 May 2026 20:35:54 +0200 Subject: [PATCH 03/12] arrange polies, fuse boxes only when obb also overlaps --- src/svgfill/src/arrange_polygons.cpp | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index a7b591fdb1..cfb51321f8 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1085,6 +1085,45 @@ bool aabb_overlap(const DBox& a, const DBox& b, double eps = 1.e-9) { a[1].y() + eps >= b[0].y(); } +std::pair projected_interval_on_axis(const std::array& points, const DDir& axis_u) { + auto u = unit(axis_u); + auto t0 = (points.front() - CGAL::ORIGIN) * u; + auto interval = std::make_pair(t0, t0); + for (auto& p : points) { + auto t = (p - CGAL::ORIGIN) * u; + interval.first = std::min(interval.first, t); + interval.second = std::max(interval.second, t); + } + return interval; +} + +bool intervals_overlap(const std::pair& a, const std::pair& b, double eps = 1.e-9) { + return a.first <= b.second + eps && b.first <= a.second + eps; +} + +bool obb_overlap(const std::array& a, const std::array& b, double eps = 1.e-9) { + auto has_separating_axis = [&](const std::array& points) { + for (size_t i = 0; i < points.size(); ++i) { + auto edge = points[(i + 1) % points.size()] - points[i]; + auto axis = unit(perpendicular(edge)); + if (axis.squared_length() < 1.e-18) { + continue; + } + if (!intervals_overlap(projected_interval_on_axis(a, axis), projected_interval_on_axis(b, axis), eps)) { + return true; + } + } + return false; + }; + + return !has_separating_axis(a) && !has_separating_axis(b); +} + +template +bool obb_overlap(const T& a, const U& b, double eps = 1.e-9) { + return obb_overlap(a.corners, b.corners, eps); +} + CenterLineGraphData make_center_line_graph_data( const std::map>& line_graph, const std::map& midpoint_to_edge_length) @@ -1378,6 +1417,9 @@ bool clusters_can_merge(const BoxCluster& a, const BoxCluster& b, double angle_t if (!aabb_overlap(a.box.bbox, b.box.bbox)) { return false; } + if (!obb_overlap(a.box, b.box)) { + return false; + } if (angle_between_dirs_deg(a.box.direction, b.box.direction) > angle_tol_deg) { return false; } From 47312e1fbb2c7cf8b4899a9638d94e35732142d5 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 8 May 2026 15:00:30 +0200 Subject: [PATCH 04/12] Reduce log noise on materials without styles #7947 --- src/ifcgeom/mapping/mapping.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index edfdd3a13b..a44ef1a57a 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -562,6 +562,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcMaterial* material) { } // Check if it's failed or just some unsupported case. if (failed_on_purpose_.find(styled_item) == failed_on_purpose_.end()) { + failed_on_purpose_.insert(material); return nullptr; } Logger::Warning("Skipping unsupported material style for material: ", material); @@ -569,6 +570,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcMaterial* material) { } // When material does not have a representation we don't create a style from it + failed_on_purpose_.insert(material); return nullptr; /* From 1b637c649990d6cbf874b84f37b74afc31f5920a Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 12 May 2026 20:52:30 +0200 Subject: [PATCH 05/12] Arrange polies: reorder segment to exterior insertion based on length --- src/svgfill/src/arrange_polygons.cpp | 148 ++++++++++++++++----------- 1 file changed, 86 insertions(+), 62 deletions(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index cfb51321f8..760b9cad2f 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1469,6 +1469,7 @@ std::vector merge_intersecting_parallel_boxes_iterative(const s std::vector members = clusters[i].members; members.insert(members.end(), clusters[j].members.begin(), clusters[j].members.end()); auto merged = BoxCluster{members, merge_cluster_to_box(members, records)}; + std::cout << "Result width: " << merged.box.avg_width << " fromt " << clusters[i].box.avg_width << " & " << clusters[j].box.avg_width << std::endl; std::vector next_clusters; next_clusters.reserve(clusters.size() - 1); @@ -2170,92 +2171,115 @@ extend_end_vertices_based_on_input_simple( const K::FT& max_projection_distance) { auto max_intersection_distance = max_projection_distance / 4; - std::list> constructed_segments; - for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { - if (it->second.size() == 1) { - auto& M = it->first; + const auto& process_point = [&](const Point_2& M, const Point_2& incoming) { + for (auto& bnd : outer_perimiter) { + // if point M is contained in bnd interior: + // if (!bnd.has_on_unbounded_side(M)) { + if (bnd.has_on_bounded_side(M)) { + // create ray incoming -> M + CGAL::Ray_2 ray(incoming, M - incoming); - for (auto& bnd : outer_perimiter) { - // if point M is contained in bnd interior: - // if (!bnd.has_on_unbounded_side(M)) { - if (bnd.has_on_bounded_side(M)) { - auto& incoming = *it->second.begin(); - // create ray incoming -> M - CGAL::Ray_2 ray(incoming, M - incoming); + // intersect ray with boundary + boost::optional> closest_segment; + boost::optional> closest_intersection_point; + K::FT sq_distance_along_ray = std::numeric_limits::infinity(); + for (auto jt = bnd.edges_begin(); jt != bnd.edges_end(); ++jt) { + const auto& seg = *jt; + auto x = CGAL::intersection(ray, seg); + if (x) { + if (auto* xp = variant_get>(&*x)) { + auto dist = ((*xp) - M).squared_length(); + if (dist < sq_distance_along_ray) { + if (dist < (max_intersection_distance * max_intersection_distance)) { + closest_segment = seg; + closest_intersection_point = *xp; + sq_distance_along_ray = dist; + } else { + } + } + } + } + } - // intersect ray with boundary - boost::optional> closest_segment; - boost::optional> closest_intersection_point; - K::FT sq_distance_along_ray = std::numeric_limits::infinity(); - for (auto jt = bnd.edges_begin(); jt != bnd.edges_end(); ++jt) { - const auto& seg = *jt; - auto x = CGAL::intersection(ray, seg); - if (x) { - if (auto* xp = variant_get>(&*x)) { - auto dist = ((*xp) - M).squared_length(); - if (dist < sq_distance_along_ray) { - if (dist < (max_intersection_distance * max_intersection_distance)) { - closest_segment = seg; - closest_intersection_point = *xp; - sq_distance_along_ray = dist; - } else { + if (closest_intersection_point) { + return closest_intersection_point; + // constructed_segments.push_front({M, *closest_intersection_point}); + } else { + + // Loop over boundary segments, and project point onto it, take the closest + K::FT closest_distance = std::numeric_limits::infinity(); + boost::optional> closest_point; + for (auto& poly : outer_perimiter) { + for (auto jt = poly.edges_begin(); jt != poly.edges_end(); ++jt) { + auto seg = *jt; + auto Pp = seg.supporting_line().projection(M); + if (seg.has_on(Pp)) { + auto d = CGAL::squared_distance(Pp, M); + if (d < (max_projection_distance * max_projection_distance)) { + if (d < closest_distance) { + closest_distance = d; + closest_point = Pp; } } } } } - if (closest_intersection_point) { - constructed_segments.push_front({M, *closest_intersection_point}); + if (closest_point) { + return closest_point; + // constructed_segments.push_front({M, *closest_point}); } else { - // Loop over boundary segments, and project point onto it, take the closest - K::FT closest_distance = std::numeric_limits::infinity(); - boost::optional> closest_point; for (auto& poly : outer_perimiter) { - for (auto jt = poly.edges_begin(); jt != poly.edges_end(); ++jt) { - auto seg = *jt; - auto Pp = seg.supporting_line().projection(M); - if (seg.has_on(Pp)) { - auto d = CGAL::squared_distance(Pp, M); - if (d < (max_projection_distance * max_projection_distance)) { - if (d < closest_distance) { - closest_distance = d; - closest_point = Pp; - } + for (auto it = poly.begin(); it != poly.end(); ++it) { + auto Pp = *it; + auto d = CGAL::squared_distance(Pp, M); + if (d < (max_projection_distance * max_projection_distance)) { + if (d < closest_distance) { + closest_distance = d; + closest_point = Pp; } } } } if (closest_point) { - constructed_segments.push_front({M, *closest_point}); + return closest_point; + // constructed_segments.push_front({M, *closest_point}); } else { - - for (auto& poly : outer_perimiter) { - for (auto it = poly.begin(); it != poly.end(); ++it) { - auto Pp = *it; - auto d = CGAL::squared_distance(Pp, M); - if (d < (max_projection_distance * max_projection_distance)) { - if (d < closest_distance) { - closest_distance = d; - closest_point = Pp; - } - } - } - } - - if (closest_point) { - constructed_segments.push_front({M, *closest_point}); - } else { - std::cout << "Unable to find projection or intersection point for interior boundary (" << M.x() << " " << M.y() << ")" << std::endl; - } } } } } } + return boost::optional{}; + }; + + using solution_length_point_incoming = std::tuple; + std::vector solutions; + + for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { + if (it->second.size() == 1) { + auto& M = it->first; + if (auto result = process_point(M, *it->second.begin())) { + auto d = (M - *result).squared_length(); + solutions.emplace_back(d, *result, *it->second.begin()); + } else { + std::cout << "Unable to find projection or intersection point for interior boundary (" << M.x() << " " << M.y() << ")" << std::endl; + } + } + } + + std::sort(solutions.begin(), solutions.end()); + std::list> constructed_segments; + + for (auto& [d, point, incoming] : solutions) { + if (auto result = process_point(point, incoming)) { + constructed_segments.push_front({point, *result}); + } else { + std::cout << "Unable to find projection or intersection point for interior boundary (" << M.x() << " " << M.y() << ")" << std::endl; + } } return constructed_segments; From 97218b1fdb8f26dc7586544f5d16059ae4daab7f Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 14 May 2026 14:17:10 +0200 Subject: [PATCH 06/12] Calculate box-width as orthogonal distance; aabb code for segment intersection (disabled) --- src/svgfill/src/arrange_polygons.cpp | 213 ++++++++++++++++++++++----- 1 file changed, 175 insertions(+), 38 deletions(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 760b9cad2f..8cfa70b211 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -267,11 +267,12 @@ void clean_polygon(Polygon_2& poly) { void smooth_polygon(double factor, Polygon_2& poly) { auto ps = create_and_convert_offset_polygon(-factor, poly); - if (ps.size() == 1) { - auto r2 = ps.front(); - ps = create_and_convert_offset_polygon(+factor, r2); - if (ps.size() == 1) { - poly = ps.front(); + auto it = std::max_element(ps.begin(), ps.end(), [&](const auto& p, const auto& q) { return p.area() < q.area(); }); + if (it != ps.end()) { + auto qs = create_and_convert_offset_polygon(+factor, *it); + auto jt = std::max_element(qs.begin(), qs.end(), [&](const auto& p, const auto& q) { return p.area() < q.area(); }); + if (jt != qs.end()) { + poly = *jt; } } } @@ -884,8 +885,7 @@ Polygon_with_holes_2 subdivide_polygon_on_same_input(SegmentLookup& segment_look std::tuple< std::map>, std::map>, - std::map, std::vector*>>, - std::map + std::map, std::vector*>> > build_line_graph(const std::vector& input_polygons, const std::map& point_lookup, const std::vector& triangular_polygons) { @@ -896,7 +896,9 @@ build_line_graph(const std::vector& input_polygons, const std::map, Point_2> segment_to_midpoint; std::map> midpoint_to_segment; std::map*, std::vector>> facet_to_segment; - std::map midpoint_to_edge_length; + + + // std::map midpoint_to_edge_length; for (auto& tri : triangular_polygons) { for (size_t i = 0; i < 3; ++i) { @@ -929,7 +931,7 @@ build_line_graph(const std::vector& input_polygons, const std::mapsecond != input_polygons.end() && p2index->second != input_polygons.end() && p1index->second != p2index->second) { segment_to_midpoint[p.first] = center; midpoint_to_segment[center] = p.first; - midpoint_to_edge_length[center] = std::sqrt(CGAL::to_double(CGAL::squared_distance(p.first.first, p.first.second))); + // midpoint_to_edge_length[center] = std::sqrt(CGAL::to_double(CGAL::squared_distance(p.first.first, p.first.second))); } } @@ -949,7 +951,7 @@ build_line_graph(const std::vector& input_polygons, const std::map::Point_2; @@ -958,8 +960,8 @@ using DBox = std::array; struct CenterLineGraphData { std::vector points; + std::vector>> orig_segments; std::vector points_double; - std::vector widths; std::vector> edges; std::vector> incident_edges; }; @@ -1126,7 +1128,7 @@ bool obb_overlap(const T& a, const U& b, double eps = 1.e-9) { CenterLineGraphData make_center_line_graph_data( const std::map>& line_graph, - const std::map& midpoint_to_edge_length) + const std::map>& midpoint_to_segment) { CenterLineGraphData graph; std::map point_to_index; @@ -1139,9 +1141,13 @@ CenterLineGraphData make_center_line_graph_data( auto i = graph.points.size(); point_to_index[p] = i; graph.points.push_back(p); + auto mit = midpoint_to_segment.find(p); + if (mit == midpoint_to_segment.end()) { + graph.orig_segments.emplace_back(); + } else { + graph.orig_segments.emplace_back(mit->second); + } graph.points_double.push_back(to_double_point(p)); - auto wt = midpoint_to_edge_length.find(p); - graph.widths.push_back(wt == midpoint_to_edge_length.end() ? 0. : wt->second); graph.incident_edges.emplace_back(); return i; }; @@ -1175,7 +1181,41 @@ CenterLineGraphData make_center_line_graph_data( } double segment_width(const CenterLineGraphData& graph, const std::pair& edge) { - return 0.5 * (graph.widths[edge.first] + graph.widths[edge.second]); + auto s1 = graph.orig_segments[edge.first]; + auto s2 = graph.orig_segments[edge.second]; + if (!s1 || !s2) { + throw std::runtime_error("!!!"); + } + + // A line segment between two points is expected to span a triangle, which means that one of the + // segment points ought to be shared. + Point_2 refpoint; + if (s1->first == s2->first) { + refpoint = s1->first; + } else if (s1->second == s2->first) { + refpoint = s1->second; + } else if (s1->first == s2->second) { + refpoint = s1->first; + } else if (s1->second == s2->second) { + refpoint = s1->second; + } else { + throw std::runtime_error("!!!!!"); + } + + auto p1 = graph.points_double[edge.first]; + auto p2 = graph.points_double[edge.second]; + auto v = p2 - p1; + + if (v.squared_length() < 1.e-9) { + throw std::runtime_error("!!!!!!!"); + } + + v /= std::sqrt(v.squared_length()); + auto n = perpendicular(v); + auto P = to_double_point(refpoint); + auto l = CGAL::abs((P - p1) * n); + + return 2 * l; } bool edge_supports_same_line( @@ -1266,6 +1306,7 @@ std::vector runs_from_graph(const CenterLineGraphData& graph, double an auto len = std::sqrt(d.squared_length()); total_length += len; weighted_width_sum += len * segment_width(graph, edge); + // std::cout << " l: " << len << " w: " << segment_width(graph, edge) << " p1: " << graph.points_double[edge.first] << " p2: " << graph.points_double[edge.second] << std::endl; } auto run_direction = direction_sum.squared_length() < 1.e-18 ? ref : unit(direction_sum); @@ -1288,6 +1329,8 @@ std::vector runs_from_graph(const CenterLineGraphData& graph, double an auto avg_width = total_length < 1.e-9 ? segment_width(graph, seed_edge) : weighted_width_sum / total_length; + // std::cout << "avg_width: " << avg_width << std::endl; + runs.push_back({ graph.points[start_index], graph.points[end_index], @@ -1469,7 +1512,7 @@ std::vector merge_intersecting_parallel_boxes_iterative(const s std::vector members = clusters[i].members; members.insert(members.end(), clusters[j].members.begin(), clusters[j].members.end()); auto merged = BoxCluster{members, merge_cluster_to_box(members, records)}; - std::cout << "Result width: " << merged.box.avg_width << " fromt " << clusters[i].box.avg_width << " & " << clusters[j].box.avg_width << std::endl; + // std::cout << "Result width: " << merged.box.avg_width << "; from " << clusters[i].box.avg_width << " & " << clusters[j].box.avg_width << std::endl; std::vector next_clusters; next_clusters.reserve(clusters.size() - 1); @@ -1549,6 +1592,7 @@ double point_to_oriented_box_distance(const DPoint& p, const MergedBoxRecord& bo } std::map> snap_points_to_box_axes( + DebugWriter& debug, const CenterLineGraphData& graph, const std::vector& boxes, const K::FT& max_projection_distance) { @@ -1592,15 +1636,18 @@ std::map> snap_points_to_box_axes( if (angle_between_dirs_deg(boxes[c1.box_index].direction, boxes[c2.box_index].direction) > 8.) { if (auto x = intersect_infinite_lines_exact(boxes[c1.box_index], boxes[c2.box_index])) { snapped_points[i] = *x; + debug.write_segment(graph.points[i], *x, "snap_candidate_1"); continue; } } snapped_points[i] = c1.projection; + debug.write_segment(graph.points[i], c1.projection, "snap_candidate_2"); continue; } if (containing.size() == 1) { snapped_points[i] = containing[0].projection; + debug.write_segment(graph.points[i], containing[0].projection, "snap_candidate_3"); continue; } @@ -1613,6 +1660,7 @@ std::map> snap_points_to_box_axes( if ((graph.points[i] - best.projection).squared_length() < (max_projection_distance * max_projection_distance)) { snapped_points[i] = best.projection; + debug.write_segment(graph.points[i], best.projection, "snap_candidate_4"); } else { snapped_points[i] = graph.points[i]; std::cout << "Warning: snapping distance exceeding distance: " << std::sqrt(CGAL::to_double((snapped_points[i] - best.projection).squared_length())) << " > " << max_projection_distance << std::endl; @@ -1640,9 +1688,9 @@ std::map> snap_points_to_box_axes( Graph2D join_segment_runs( DebugWriter& debug, const std::map>& line_graph, - const std::map& midpoint_to_edge_length, + const std::map>& midpoint_to_segment, const K::FT& max_projection_distance) { - auto graph = make_center_line_graph_data(line_graph, midpoint_to_edge_length); + auto graph = make_center_line_graph_data(line_graph, midpoint_to_segment); auto runs = runs_from_graph(graph); runs.erase(std::remove_if(runs.begin(), runs.end(), [](const LineRun& run) { return run.vertex_count <= 5; @@ -1672,7 +1720,7 @@ Graph2D join_segment_runs( } debug.write_polygons(run_polygons, "merged_boxes"); - auto snapped_graph = snap_points_to_box_axes(graph, boxes, max_projection_distance); + auto snapped_graph = snap_points_to_box_axes(debug, graph, boxes, max_projection_distance); return Graph2D(snapped_graph); } @@ -2166,17 +2214,69 @@ std::list> extend_end_vertices_based_on_input( std::list> extend_end_vertices_based_on_input_simple( + DebugWriter& debug_output, const Graph2D& G, const Polygon_list& outer_perimiter, - const K::FT& max_projection_distance) + const K::FT& max_projection_distance, int pass) { auto max_intersection_distance = max_projection_distance / 4; + using ValidationSegmentList = std::list>; + using ValidationSegmentIt = ValidationSegmentList::iterator; + using ValidationTreeTraits = CGAL::AABB_traits>; + using ValidationTree = CGAL::AABB_tree; + + const auto& to_3d = [](const Point_2& p) { + return CGAL::Point_3(p.x(), p.y(), 0); + }; + + const auto& to_2d = [](const CGAL::Point_3& p) { + return CGAL::Point_2(p.x(), p.y()); + }; + + ValidationSegmentList validation_segments; + for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { + if (it->first != it->second) { + validation_segments.emplace_back(to_3d(it->first), to_3d(it->second)); + } + } + + ValidationTree validation_tree(validation_segments.begin(), validation_segments.end()); + + const auto has_intersection = [&](const Segment_2& candidate) { + // @nb still disabled. + return false; + std::vector intersected_segments; + validation_tree.all_intersected_primitives(CGAL::Segment_3(to_3d(candidate.source()), to_3d(candidate.target())), std::back_inserter(intersected_segments)); + + for (auto it : intersected_segments) { + auto existing = CGAL::Segment_2(to_2d(it->source()), to_2d(it->target())); + auto intersection = CGAL::intersection(candidate, existing); + if (!intersection) { + continue; + } + + if (auto* point = variant_get(&*intersection)) { + const bool candidate_endpoint = *point == candidate.source() || *point == candidate.target(); + const bool existing_endpoint = *point == existing.source() || *point == existing.target(); + if (candidate_endpoint && existing_endpoint) { + continue; + } + } + + return true; + } + + return false; + }; + const auto& process_point = [&](const Point_2& M, const Point_2& incoming) { + bool within_any_perimeter = false; for (auto& bnd : outer_perimiter) { // if point M is contained in bnd interior: // if (!bnd.has_on_unbounded_side(M)) { if (bnd.has_on_bounded_side(M)) { + within_any_perimeter = true; // create ray incoming -> M CGAL::Ray_2 ray(incoming, M - incoming); @@ -2192,9 +2292,13 @@ extend_end_vertices_based_on_input_simple( auto dist = ((*xp) - M).squared_length(); if (dist < sq_distance_along_ray) { if (dist < (max_intersection_distance * max_intersection_distance)) { - closest_segment = seg; - closest_intersection_point = *xp; - sq_distance_along_ray = dist; + if (has_intersection(CGAL::Segment_2(M, *xp))) { + debug_output.write_segment(M, *xp, "exterior_extension_intersection"); + } else { + closest_segment = seg; + closest_intersection_point = *xp; + sq_distance_along_ray = dist; + } } else { } } @@ -2218,8 +2322,12 @@ extend_end_vertices_based_on_input_simple( auto d = CGAL::squared_distance(Pp, M); if (d < (max_projection_distance * max_projection_distance)) { if (d < closest_distance) { - closest_distance = d; - closest_point = Pp; + if (has_intersection(CGAL::Segment_2(M, Pp))) { + debug_output.write_segment(M, Pp, "exterior_projection_intersection"); + } else { + closest_distance = d; + closest_point = Pp; + } } } } @@ -2236,9 +2344,13 @@ extend_end_vertices_based_on_input_simple( auto Pp = *it; auto d = CGAL::squared_distance(Pp, M); if (d < (max_projection_distance * max_projection_distance)) { - if (d < closest_distance) { - closest_distance = d; - closest_point = Pp; + if (has_intersection(CGAL::Segment_2(M, Pp))) { + debug_output.write_segment(M, Pp, "exterior_nearby_intersection"); + } else { + if (d < closest_distance) { + closest_distance = d; + closest_point = Pp; + } } } } @@ -2246,13 +2358,19 @@ extend_end_vertices_based_on_input_simple( if (closest_point) { return closest_point; - // constructed_segments.push_front({M, *closest_point}); } else { } } } + } else if (bnd.has_on_boundary(M)) { + return boost::optional{M}; } } + if (within_any_perimeter) { + std::cout << "Within boundary but still no solution given" << std::endl; + } else { + std::cout << "Outside of all boundaries" << std::endl; + } return boost::optional{}; }; @@ -2263,10 +2381,14 @@ extend_end_vertices_based_on_input_simple( if (it->second.size() == 1) { auto& M = it->first; if (auto result = process_point(M, *it->second.begin())) { + if (*result == M) { + std::cout << "Point already on perimeter (" << M.x() << " " << M.y() << ")" << std::endl; + continue; + } auto d = (M - *result).squared_length(); solutions.emplace_back(d, *result, *it->second.begin()); } else { - std::cout << "Unable to find projection or intersection point for interior boundary (" << M.x() << " " << M.y() << ")" << std::endl; + std::cout << "Unable to find projection or intersection point for interior boundary pass " << pass << " [round 1] (" << M.x() << " " << M.y() << ")" << std::endl; } } } @@ -2277,8 +2399,15 @@ extend_end_vertices_based_on_input_simple( for (auto& [d, point, incoming] : solutions) { if (auto result = process_point(point, incoming)) { constructed_segments.push_front({point, *result}); + debug_output.write_segment(point, *result, "exterior_constructed_segment"); + + auto d = CGAL::squared_distance(point, *result); + std::cout << "Distance: " << std::sqrt(CGAL::to_double(d)) << std::endl; + validation_segments.emplace_back(to_3d(point), to_3d(*result)); + auto inserted_it = std::prev(validation_segments.end()); + validation_tree.insert(inserted_it, validation_segments.end()); } else { - std::cout << "Unable to find projection or intersection point for interior boundary (" << M.x() << " " << M.y() << ")" << std::endl; + std::cout << "Unable to find projection or intersection point for interior boundary pass " << pass << " [round 2] (" << point.x() << " " << point.y() << ")" << std::endl; } } @@ -3245,6 +3374,14 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std std::swap(input_polygons, split_polygons); } + // before overlap elimition we can (and should) still smooth + /* + * @todo + for (auto& r : input_polygons) { + smooth_polygon(polygon_offset_distance / 100., r); + } + */ + t0.stop(); t0 = timer.start("overlap elimination"); @@ -3398,7 +3535,7 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std debug_output.write_polygons(triangular_polygons, "triangulated_corridor"); - auto [line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length] = build_line_graph(input_polygons, point_lookup, triangular_polygons); + auto [line_graph, midpoint_to_segment, segment_to_input_facet] = build_line_graph(input_polygons, point_lookup, triangular_polygons); for (auto& p : line_graph) { for (auto& q : p.second) { debug_output.write_segment(p.first, q, "network_1"); @@ -3438,17 +3575,17 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std Graph2D G2(line_graph); G = G2.weld_vertices(); for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - debug_output.write_segment(it->first, it->second, "network_2"); + debug_output.write_segment(it->first, it->second, "network_b_2"); } eliminate_colinear_vertices(G); edge_slide(G); for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - debug_output.write_segment(it->first, it->second, "network_3"); + debug_output.write_segment(it->first, it->second, "network_b_3"); } }; if (settings.line_cleaning_algo == 0) { - G = join_segment_runs(debug_output, line_graph, midpoint_to_edge_length, subdivision_length * 4); + G = join_segment_runs(debug_output, line_graph, midpoint_to_segment, subdivision_length * 4); Arrangement_2 arr; G.to_arrangement(arr); Graph2D G2; @@ -3456,7 +3593,7 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std eliminate_colinear_vertices(G2); G = G2; for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - debug_output.write_segment(it->first, it->second, "network_2"); + debug_output.write_segment(it->first, it->second, "network_a_2"); } } else { apply_line_cleaning_algo_1(); @@ -3470,8 +3607,8 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std bool fallback_to_line_cleaning_algo_1 = false; if (settings.line_cleaning_algo == 0) { - segments1 = extend_end_vertices_based_on_input_simple(G, outer_perimiter, subdivision_length * 16); - segments2 = extend_end_vertices_based_on_input_simple(G_orig, outer_perimiter, subdivision_length * 16); + segments1 = extend_end_vertices_based_on_input_simple(debug_output, G, outer_perimiter, subdivision_length * 16, 0); + segments2 = extend_end_vertices_based_on_input_simple(debug_output, G_orig, outer_perimiter, subdivision_length * 16, 1); Arrangement_2 arr_clean; G.to_arrangement(arr_clean); From 0b5dded3b3d38ad28b57d93cc8a3d7b562f9f72a Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 14 May 2026 14:37:44 +0200 Subject: [PATCH 07/12] Fix temporary solution storage in arrange polygons --- src/svgfill/src/arrange_polygons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 8cfa70b211..cbd4206eef 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -2386,7 +2386,7 @@ extend_end_vertices_based_on_input_simple( continue; } auto d = (M - *result).squared_length(); - solutions.emplace_back(d, *result, *it->second.begin()); + solutions.emplace_back(d, M, *it->second.begin()); } else { std::cout << "Unable to find projection or intersection point for interior boundary pass " << pass << " [round 1] (" << M.x() << " " << M.y() << ")" << std::endl; } From 9345b9ce3ffd36b778c09fd6328eb9a9a606ca45 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 14 May 2026 21:45:59 +0200 Subject: [PATCH 08/12] arrange polies: don't allow snapped point paths to cross non-containing other rect axes --- src/svgfill/src/arrange_polygons.cpp | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index cbd4206eef..1f42a4792b 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1635,13 +1635,28 @@ std::map> snap_points_to_box_axes( auto& c2 = containing[1]; if (angle_between_dirs_deg(boxes[c1.box_index].direction, boxes[c2.box_index].direction) > 8.) { if (auto x = intersect_infinite_lines_exact(boxes[c1.box_index], boxes[c2.box_index])) { - snapped_points[i] = *x; - debug.write_segment(graph.points[i], *x, "snap_candidate_1"); - continue; + auto seg = CGAL::Segment_2(graph.points[i], *x); + bool intersects_with_other_box_axis = false; + for (size_t j = 0; j < boxes.size(); ++j) { + if (j == c1.box_index || j == c2.box_index) { + continue; + } + auto& box = boxes[j]; + auto box_seg = CGAL::Segment_2(box.exact_start, box.exact_end); + if (CGAL::do_intersect(seg, box_seg)) { + intersects_with_other_box_axis = true; + break; + } + } + if (!intersects_with_other_box_axis) { + snapped_points[i] = *x; + debug.write_segment(graph.points[i], *x, "snap_candidate_1"); + continue; + } } } - snapped_points[i] = c1.projection; - debug.write_segment(graph.points[i], c1.projection, "snap_candidate_2"); + snapped_points[i] = (c1.projection - graph.points[i]).squared_length() < (c2.projection - graph.points[i]).squared_length() ? c1.projection : c2.projection; + debug.write_segment(graph.points[i], snapped_points[i], "snap_candidate_2"); continue; } From e78ef865b893933b28d92fac746fe40d8a563d19 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Fri, 15 May 2026 07:28:29 -0500 Subject: [PATCH 09/12] Fix #8056 - Dimensions with `CustomUnit" = "Inches - Fractional"` should not show `0`. --- src/bonsai/bonsai/bim/module/drawing/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index d4895410cb..ef72207914 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -313,7 +313,7 @@ def format_distance( if not feet and not add_inches: tx_dist += str(feet) + "'" - if not feet and add_inches: + if not feet and add_inches and unit_length != "INCHES": if value < 0: tx_dist += "-0' - " else: From 227d85d81f5b002b9be5f95681349c5533d8799e Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 15 May 2026 21:12:43 +0200 Subject: [PATCH 10/12] arrange polygons: limit width ratio when merging boxes --- src/svgfill/src/arrange_polygons.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 1f42a4792b..260b22809e 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1457,6 +1457,13 @@ std::pair merge_score(const MergedBoxRecord& a, const MergedBoxR } bool clusters_can_merge(const BoxCluster& a, const BoxCluster& b, double angle_tol_deg = 5., double axis_overlap_ratio_limit = 0.5) { + auto min_width = a.box.avg_width < b.box.avg_width ? a.box.avg_width : b.box.avg_width; + auto max_width = a.box.avg_width > b.box.avg_width ? a.box.avg_width : b.box.avg_width; + if (min_width > 1.e-9) { + if (max_width / min_width > 5) { + return false; + } + } if (!aabb_overlap(a.box.bbox, b.box.bbox)) { return false; } From 4e406ab1ce906d9487b2a80dc0fd7595ba10b771 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 18 May 2026 13:29:39 +0200 Subject: [PATCH 11/12] Change default value of assume_asset_uniqueness_by_name #8045 --- src/ifcpatch/ifcpatch/recipes/ExtractElements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index 6f88f820be..10d8b23330 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -33,7 +33,7 @@ class Patcher(ifcpatch.BasePatcher): file: ifcopenshell.file, logger: Union[Logger, None] = None, query: str = "IfcWall", - assume_asset_uniqueness_by_name: bool = True, + assume_asset_uniqueness_by_name: bool = False, ): """Extract certain elements into a new model From 508b99cb731af604e5adab921dacb58793cd4c91 Mon Sep 17 00:00:00 2001 From: Geert Hesselink <54070862+Ghesselink@users.noreply.github.com> Date: Mon, 18 May 2026 22:17:45 +0200 Subject: [PATCH 12/12] Fix lint failures and add missing pyparsing dependency (#8048) * unblock voxel schema loading, add test for express * Apply black formatting * Fix lint failures and add missing pyparsing dependency * align ty -> 0.0.34 --- .github/workflows/ci-lint.yaml | 2 +- .github/workflows/ci.yml | 2 +- nix/build-all.py | 3 +-- src/bonsai/bonsai/bim/module/model/wall.py | 6 +++++- src/bonsai/test/tool/test_cost.py | 10 +++------- src/ifcopenshell-python/ifcopenshell/draw.py | 5 ++++- .../ifcopenshell/ifcopenshell_wrapper.pyi | 2 +- src/ifcopenshell-python/pyproject.toml | 1 + src/ifcopenshell-python/test/util/test_cost.py | 5 ++--- 9 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci-lint.yaml b/.github/workflows/ci-lint.yaml index 4ef84f8cbb..677ec10c25 100644 --- a/.github/workflows/ci-lint.yaml +++ b/.github/workflows/ci-lint.yaml @@ -30,7 +30,7 @@ jobs: uv tool install ruff uv tool install black uv tool install poethepoet - uv tool install ty + uv tool install ty==0.0.34 # black doesn't catch all syntax errors, so we check them explicitly. - name: Check syntax errors diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea7fb590e2..2801d19ecc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely + pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pyparsing pip install src/bcf --no-deps pip install pytest-xdist==3.8.0 diff --git a/nix/build-all.py b/nix/build-all.py index 4858907ce6..fd2abe2a11 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -126,9 +126,8 @@ ssl._create_default_https_context = ssl._create_unverified_context import time from collections.abc import Generator, Sequence from pathlib import Path -from urllib.request import urlretrieve - from typing import Literal, Union +from urllib.request import urlretrieve logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index b5d2f5fa77..dd900f4a23 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -468,7 +468,11 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle - profiles = extrusion.SweptArea.Profiles if extrusion.SweptArea.is_a("IfcCompositeProfileDef") else [extrusion.SweptArea] + profiles = ( + extrusion.SweptArea.Profiles + if extrusion.SweptArea.is_a("IfcCompositeProfileDef") + else [extrusion.SweptArea] + ) for profile in profiles: coord_list = builder.get_polyline_coords(profile.OuterCurve) coord_list = [ diff --git a/src/bonsai/test/tool/test_cost.py b/src/bonsai/test/tool/test_cost.py index 3cfbe03c91..564337a8d4 100644 --- a/src/bonsai/test/tool/test_cost.py +++ b/src/bonsai/test/tool/test_cost.py @@ -17,20 +17,20 @@ # along with Bonsai. If not, see . -import test.bim.bootstrap import ifcopenshell.api.cost import bonsai.core.tool import bonsai.tool as tool import test.bim.bootstrap +from bonsai.tool.cost import Cost as subject from test.bim.bootstrap import NewFile -from bonsai.tool.cost import Cost as subject class TestImplementsTool(NewFile): def test_run(self): assert isinstance(subject(), bonsai.core.tool.Cost) + class TestDisableEditingCostItemParent(NewFile): def test_avoid_recursion_error(newfile, monkeypatch): class DummyProps: @@ -39,11 +39,7 @@ class TestDisableEditingCostItemParent(NewFile): self.active_cost_item_id = 5 props = DummyProps() - monkeypatch.setattr( - "bonsai.tool.Cost.get_cost_props", - lambda: props - ) + monkeypatch.setattr("bonsai.tool.Cost.get_cost_props", lambda: props) subject.disable_editing_cost_item_parent() assert props.active_cost_item_id == 0 assert props.change_cost_item_parent is not False - diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index ba78f0d48d..f4ea932933 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -538,7 +538,10 @@ def main( *(tup for i, tup in enumerate(zip(path_objects, section_polies, polies)) if has_relevant_zone(i)) ) - arranged = W.arrange_polygons(*filter(None, (ARRANGE_POLYGON_SETTINGS,)), polies) + arranged = W.arrange_polygons( + *filter(None, (ARRANGE_POLYGON_SETTINGS,)), + polies, # ty: ignore[too-many-positional-arguments] + ) svg_data_3 = W.polygons_to_svg(arranged, False) dom3 = parseString(svg_data_3) svg3 = dom3.childNodes[0] diff --git a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi index 28caafc262..3345eee5a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi +++ b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi @@ -1695,7 +1695,7 @@ class type_declaration(declaration): class uninitialized_tag: ... -def arrange_polygons(polygons): ... +def arrange_polygons(settings, polygons): ... def clear_schemas(): ... def construct_iterator(geometry_library, settings, file, num_threads): ... def construct_iterator_with_include_exclude(geometry_library, settings, file, elems, include, num_threads): ... diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml index 9bcaeeebaa..288e3e3585 100644 --- a/src/ifcopenshell-python/pyproject.toml +++ b/src/ifcopenshell-python/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "isodate", "python-dateutil", "lark", + "pyparsing", "typing-extensions", ] diff --git a/src/ifcopenshell-python/test/util/test_cost.py b/src/ifcopenshell-python/test/util/test_cost.py index 516a69edd0..d0f9612673 100644 --- a/src/ifcopenshell-python/test/util/test_cost.py +++ b/src/ifcopenshell-python/test/util/test_cost.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . -import pytest import ifcopenshell.api.control import ifcopenshell.api.cost @@ -25,6 +24,7 @@ import ifcopenshell.api.root import ifcopenshell.util.cost as subject + class TestGetCostItemForProduct(test.bootstrap.IFC4): def test_run(self): model = self.file @@ -40,7 +40,7 @@ class TestGetCostItemForProduct(test.bootstrap.IFC4): cost_schedule = ifcopenshell.api.cost.add_cost_schedule(model) item1 = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=cost_schedule) ifcopenshell.api.control.assign_control(model, related_objects=[element], relating_control=item1) - ifcopenshell.api.cost.remove_cost_item(model, cost_item = item1) + ifcopenshell.api.cost.remove_cost_item(model, cost_item=item1) assert list(subject.get_cost_items_for_product(element)) == [] def test_no_assigned_cost_items(self): @@ -49,4 +49,3 @@ class TestGetCostItemForProduct(test.bootstrap.IFC4): cost_schedule = ifcopenshell.api.cost.add_cost_schedule(model) item1 = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=cost_schedule) assert list(subject.get_cost_items_for_product(element)) == [] -