From 3fbf01f44676662a6f44bf7ed5301bd5b6fa37fb Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 8 Apr 2026 13:48:23 +0200 Subject: [PATCH 01/25] partial revert of 24acfea --- .../mapping/IfcPointByDistanceExpression.cpp | 9 - src/ifcopenshell-python/ifcopenshell/draw.py | 3 +- src/ifcparse/IfcSchema.h | 1 - src/ifcwrap/IfcGeomWrapper.i | 5 +- src/svgfill/src/arrange_polygons.cpp | 1038 ++++------------- src/svgfill/src/svgfill.cpp | 12 - src/svgfill/src/svgfill.h | 21 +- 7 files changed, 233 insertions(+), 856 deletions(-) diff --git a/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp b/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp index f07234a8f1..180226fb82 100644 --- a/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp +++ b/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp @@ -52,15 +52,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPointByDistanceExpression* i if (inst->OffsetVertical().has_value()) { auto offset_vertical = inst->OffsetVertical().get() * length_unit_; o += offset_vertical * z; - - auto tmp1 = (z * offset_vertical).eval(); - auto tmp2 = (Eigen::Vector3d(0, 0, 1) * offset_vertical).eval(); - auto tmp3 = (tmp1 - tmp2).eval(); - - std::ostringstream oss; - oss << "local z: " << z.x() << "," << z.y() << "," << z.z() << "; delta: " << tmp3.x() << "," << tmp3.y() << "," << tmp3.z(); - auto osss = oss.str(); - std::wcout << osss.c_str() << std::endl; } if (inst->OffsetLongitudinal().has_value()) { diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index 962dbbb34f..5f6d761ceb 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -42,7 +42,6 @@ WHITE = numpy.array((1.0, 1.0, 1.0)) DO_NOTHING = lambda *args: None -ARRANGE_POLYGON_SETTINGS = W.arrange_polygon_settings() if hasattr(W, 'arrange_polygon_settings') else None @dataclass class draw_settings: @@ -537,7 +536,7 @@ 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(polies) svg_data_3 = W.polygons_to_svg(arranged, False) dom3 = parseString(svg_data_3) svg3 = dom3.childNodes[0] diff --git a/src/ifcparse/IfcSchema.h b/src/ifcparse/IfcSchema.h index 349a81532d..3dedd47a8e 100644 --- a/src/ifcparse/IfcSchema.h +++ b/src/ifcparse/IfcSchema.h @@ -358,7 +358,6 @@ class IFC_PARSE_API entity : public declaration { const std::vector& subtypes() const { return subtypes_; } const std::vector& attributes() const { return attributes_; } - const std::vector& inverse_attributes() const { return inverse_attributes_; } const std::vector& derived() const { return derived_; } const std::vector all_attributes() const { diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index e992e4beab..e155c2837e 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -1166,7 +1166,6 @@ ifcopenshell::geometry::taxonomy::item::ptr try_upcast(PyObject* obj0, swig_type %ignore svgfill::line_segments_to_polygons; %ignore svgfill::svg_to_polygons; %ignore svgfill::arrange_polygons; -%ignore svgfill::abstract_arrangement; %template(svg_line_segments) std::vector>; %template(svg_groups_of_line_segments) std::vector>>; @@ -1288,9 +1287,9 @@ ifcopenshell::geometry::taxonomy::item::ptr try_upcast(PyObject* obj0, swig_type } } - std::vector arrange_polygons(svgfill::arrange_polygon_settings settings, const std::vector& polygons) { + std::vector arrange_polygons(const std::vector& polygons) { std::vector r; - if (svgfill::arrange_polygons(settings, polygons, r)) { + if (svgfill::arrange_polygons(polygons, r)) { return r; } else { throw std::runtime_error("Failed to arrange polygons"); diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 0d5cfd96e3..4fa20c68dc 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -350,8 +350,6 @@ find_overlaps(const std::vector& polygons) { class DebugWriter { public: - DebugWriter() : enabled_(false) {} - DebugWriter(bool enabled, const std::string& filename_prefix) : enabled_(enabled) { if (enabled_) { @@ -370,43 +368,6 @@ class DebugWriter { } } - DebugWriter(const DebugWriter&) = delete; - - DebugWriter(DebugWriter&& other) noexcept - : obj(std::move(other.obj)), vi(other.vi), svg(std::move(other.svg)), enabled_(other.enabled_), last_segment_name_(std::move(other.last_segment_name_)) - { - other.enabled_ = false; - other.vi = 1; - other.last_segment_name_.clear(); - } - - DebugWriter& operator=(const DebugWriter&) = delete; - - DebugWriter& operator=(DebugWriter&& other) noexcept { - if (this == &other) { - return *this; - } - - if (enabled_) { - svg << "\n"; - obj << std::flush; - obj.close(); - svg.close(); - } - - obj = std::move(other.obj); - svg = std::move(other.svg); - vi = other.vi; - enabled_ = other.enabled_; - last_segment_name_ = std::move(other.last_segment_name_); - - other.enabled_ = false; - other.vi = 1; - other.last_segment_name_.clear(); - - return *this; - } - void write_polygon(const Polygon_2& polygon, const std::string& name) { if (enabled_) { write_polygon_to_obj_(obj, vi, true, polygon, name); @@ -426,7 +387,7 @@ class DebugWriter { obj << "l " << vi++; obj << " " << vi++ << "\n"; - svg << "\n"; + svg << ""; obj << std::flush; } @@ -507,7 +468,7 @@ class DebugWriter { } }; -void eliminate_overlaps(DebugWriter& debug_writer, double OVERLAP_RESOLUTION_DISTANCE, std::vector& polygons) { +void eliminate_overlaps(double OVERLAP_RESOLUTION_DISTANCE, std::vector& polygons) { // solve overlaps by means of subtraction // loop over overlaps and subtract the smaller polygon from the larger one @@ -615,40 +576,11 @@ void eliminate_overlaps(DebugWriter& debug_writer, double OVERLAP_RESOLUTION_DIS std::swap(poly1, poly2); } - std::cerr << "processing: " << edge.first << " " << edge.second << std::endl; - std::cerr << "area before: " << poly1->area() << " " << poly2->area() << std::endl; - - bool is_ = edge == std::make_pair(25, 27); - bool success = false; if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) { - if (is_) { - debug_writer.write_polygon(*mp1, "mp1"); - } - smooth_polygon(OVERLAP_RESOLUTION_DISTANCE / 100., *mp1); - if (is_) { - debug_writer.write_polygon(*mp1, "mp1b"); - } if ((mp2 = subtract_retain_largest(*poly1, *mp1))) { - if (is_) { - debug_writer.write_polygon(*mp2, "mp2"); - } - smooth_polygon(OVERLAP_RESOLUTION_DISTANCE / 100., *mp2); - if (is_) { - debug_writer.write_polygon(*mp2, "mp2b"); - } if ((mp3 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE * 2, *mp2)))) { - if (is_) { - debug_writer.write_polygon(*mp3, "mp3"); - } - smooth_polygon(OVERLAP_RESOLUTION_DISTANCE / 100., *mp3); - if (is_) { - debug_writer.write_polygon(*mp3, "mp3b"); - } if ((mp4 = subtract_retain_largest(*poly2, *mp3))) { - if (is_) { - debug_writer.write_polygon(*mp4, "mp4"); - } *poly1 = *mp2; *poly2 = *mp4; success = true; @@ -657,8 +589,6 @@ void eliminate_overlaps(DebugWriter& debug_writer, double OVERLAP_RESOLUTION_DIS } } - std::cerr << "area after: " << poly1->area() << " " << poly2->area() << std::endl; - if (!success) { eliminated_polies.insert(swap ? edge.first : edge.second); continue; @@ -847,19 +777,14 @@ Polygon_with_holes_2 subdivide_polygon(double max_distance, const Polygon_with_h std::tuple< std::map>, std::map>, - std::map, std::vector*>>, - std::map -> -build_line_graph(const std::vector& input_polygons, SegmentLookup& segment_lookup, const std::vector& triangular_polygons) -{ - + std::map, std::vector*>>> +build_line_graph(const std::vector& input_polygons, SegmentLookup& segment_lookup, const std::vector& triangular_polygons) { // Build maps of triangle -> edge and edge -> triangle in order to do traversal on the 'corridor mesh' std::map, std::vector*>> segment_to_facet; std::map, std::vector*>> segment_to_input_facet; 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; for (auto& tri : triangular_polygons) { for (size_t i = 0; i < 3; ++i) { @@ -888,7 +813,6 @@ build_line_graph(const std::vector& input_polygons, SegmentLookup& se if (p1index != input_polygons.end() && p2index != input_polygons.end() && p1index != p2index) { 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))); } } @@ -908,7 +832,7 @@ build_line_graph(const std::vector& input_polygons, SegmentLookup& se } } - return {line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length}; + return {line_graph, midpoint_to_segment, segment_to_input_facet}; } std::set> find_triangles(const std::map>& line_graph) { @@ -1194,91 +1118,66 @@ std::list> extend_end_vertices_based_on_input( const Graph2D& G, const std::map>& midpoint_to_segment, const std::map, std::vector*>>& segment_to_input_facet, - const Polygon_list& outer_perimiter, - const SegmentLookup& segment_lookup, - const K::FT& max_projection_distance + const Polygon_list& inner_offset, + const SegmentLookup& segment_lookup ){ std::list> constructed_segments; - std::set processed_vertices; + for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { + if (it->second.size() == 1) { + auto& M = it->first; - while (true) { - // The idea was to peal off 1-degree vertices when projecting them did not result into - // nearby intersections with the outer perimiter. This in case there would be turns near - // the perimeter, which would be eliminated by pealing off the vertices, which would then - // require out of the loop because of invalidated iterators. For now we decided to stick - // to a projection of the vertex onto the perimeter segment when the projection distance - // exceeds a threshold. - bool broke_out = false; + const std::pair* q = nullptr; - for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { - if (it->second.size() == 1) { - auto& M = it->first; - - if (processed_vertices.find(M) != processed_vertices.end()) { - continue; - } - - const std::pair* q = nullptr; - - if (midpoint_to_segment.find(M) == midpoint_to_segment.end()) { - typename K::FT min_sq_distance = std::numeric_limits::infinity(); - for (auto& pa : midpoint_to_segment) { - if (CGAL::squared_distance(pa.first, M) < min_sq_distance) { - q = &pa.second; - min_sq_distance = CGAL::squared_distance(pa.first, M); - } + if (midpoint_to_segment.find(M) == midpoint_to_segment.end()) { + typename K::FT min_sq_distance = std::numeric_limits::infinity(); + for (auto& pa : midpoint_to_segment) { + if (CGAL::squared_distance(pa.first, M) < min_sq_distance) { + q = &pa.second; + min_sq_distance = CGAL::squared_distance(pa.first, M); } - } else { - q = &midpoint_to_segment.find(M)->second; } + } else { + q = &midpoint_to_segment.find(M)->second; + } - if (q == nullptr) { - continue; - } + if (q == nullptr) { + continue; + } - bool handled_as_graph_path = false; + bool handled_as_graph_path = false; - // distance from unioned - shoot ray? - if (segment_to_input_facet.find(*q)->second.size() == 2) { - 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); - - std::cerr << "Extending end vertex " << M << " along ray " << ray << " to boundary of input polygon" << std::endl; - - // 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(); - std::cerr << " - found " << *xp << " on segment " << seg << " with distance " << std::sqrt(CGAL::to_double(dist)) << std::endl; - if (dist < sq_distance_along_ray) { - if (dist < (max_projection_distance * max_projection_distance)) { - closest_segment = seg; - closest_intersection_point = *xp; - sq_distance_along_ray = dist; - } else { - - } - } + // distance from unioned - shoot ray? + if (segment_to_input_facet.find(*q)->second.size() == 2) { + for (auto& bnd : inner_offset) { + // 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) { + closest_segment = seg; + closest_intersection_point = *xp; + sq_distance_along_ray = dist; } } } + } - if (closest_intersection_point) { - constructed_segments.push_front({M, *closest_intersection_point}); - processed_vertices.insert(M); - break; + if (closest_intersection_point) { + constructed_segments.push_front({M, *closest_intersection_point}); + break; #if 0 Graph2D GGG(bnd); GGG.refine(*GGG.query(*closest_intersection_point, 0.01), *closest_intersection_point); @@ -1318,33 +1217,12 @@ std::list> extend_end_vertices_based_on_input( break; } #endif - } 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 < closest_distance) { - closest_distance = d; - closest_point = Pp; - } - } - } - } - - if (closest_point) { - constructed_segments.push_front({M, *closest_point}); - processed_vertices.insert(M); - } - } + } else { + std::cerr << "Warning: no intersection found when extending end vertex, this will likely result in invalid topology" << std::endl; } } } + } #if 0 if (!handled_as_graph_path) { @@ -1389,11 +1267,6 @@ std::list> extend_end_vertices_based_on_input( constructed_segments.push_front({avg, R}); } #endif - } - } - - if (!broke_out) { - break; } } @@ -1482,6 +1355,8 @@ void fuse_corridor_halves_with_input(Arrangement_2& arr, Graph2D& G, SegmentL } } +#include + class Segment_2_less { public: bool operator()(const Segment_2& a, const Segment_2& b) const { @@ -1492,112 +1367,7 @@ class Segment_2_less { } }; -std::vector arrangement_cell_iou(Arrangement_2& left, Arrangement_2& right) { - - using Walk_pl = CGAL::Arr_walk_along_line_point_location; - Walk_pl walk_pl(right); - - std::set visited_faces_on_right; - - std::vector return_values; - - for (auto it = left.faces_begin(); it != left.faces_end(); ++it) { - if (!it->is_unbounded()) { - // convert arr facet to polygon with holes - auto polygon_exterior = circ_to_poly(it->outer_ccb()); - Polygon_with_holes_2 pwh(polygon_exterior); - for (auto hit = it->inner_ccbs_begin(); hit != it->inner_ccbs_end(); ++hit) { - pwh.add_hole(circ_to_poly(*hit)); - } - - CGAL::Polygon_triangulation_decomposition_2 decompositor; - std::vector temp; - decompositor(pwh, std::back_inserter(temp)); - - std::set visited_points; - - while (true) { - // select triangle edge that has largest squared edge length times distance from polygon exterior - K::FT max_score = -std::numeric_limits::infinity(); - Point_2 best_point; - for (auto& tri : temp) { - for (size_t i = 0; i < 3; ++i) { - size_t j = (i + 1) % 3; - auto& pi = tri.vertex(i); - auto& pj = tri.vertex(j); - - auto center_point = CGAL::ORIGIN + (((pi - CGAL::ORIGIN) + (pj - CGAL::ORIGIN)) / 2); - - K::FT min_dist = std::numeric_limits::infinity(); - for (auto eit = polygon_exterior.edges_begin(); eit != polygon_exterior.edges_end(); ++eit) { - auto ep = eit->source(); - auto eq = eit->target(); - Segment_2 seg(ep, eq); - auto dist = CGAL::squared_distance(center_point, seg); - if (dist < min_dist) { - min_dist = dist; - } - } - - auto sq_length = CGAL::squared_distance(pi, pj); - - auto score = sq_length * min_dist; - if (score > max_score && visited_points.count(center_point) == 0) { - max_score = score; - best_point = center_point; - } - } - } - - auto res = walk_pl.locate(best_point); - if (auto* v = variant_get(&res)) { - if (visited_faces_on_right.count(*v) > 0) { - return_values.push_back(0); - } else { - // convert arr facet to polygon with holes - auto polygon_exterior = circ_to_poly((*v)->outer_ccb()); - Polygon_with_holes_2 pwh_right(polygon_exterior); - for (auto hit = (*v)->inner_ccbs_begin(); hit != (*v)->inner_ccbs_end(); ++hit) { - pwh_right.add_hole(circ_to_poly(*hit)); - } - - // compute intersection over union of pwh and the original polygon - if (CGAL::do_intersect(pwh, pwh_right)) { - std::vector result; - CGAL::intersection(pwh, pwh_right, std::back_inserter(result)); - typename K::FT intersection_area = 0; - for (auto& r : result) { - auto poly_area = r.outer_boundary().area(); - for (auto& h : r.holes()) { - poly_area -= h.area(); - } - intersection_area += poly_area; - } - CGAL::Polygon_with_holes_2 poly12; - CGAL::join(pwh, pwh_right, poly12); - typename K::FT union_area = poly12.outer_boundary().area(); - for (auto& h : poly12.holes()) { - union_area -= h.area(); - } - return_values.push_back(intersection_area / union_area); - } else { - return_values.push_back(0); - } - } - visited_faces_on_right.insert(*v); - break; - } else { - // Not in facet on right, retry another point - continue; - } - } - } - } - - return return_values; -} - -void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLookup& segment_lookup, double& threshold) { +void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { using SK = CGAL::Simple_cartesian; CGAL::Cartesian_converter C{}; @@ -1648,17 +1418,15 @@ void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLoo auto [dv, dl] = get_dir(s); best = std::min(best, angle(dv)); } - return (best + 0.01) / own_length; + return (best + 0.1) / own_length; }; - std::cerr << "badnesses:"; std::map badnesses; for (auto& e : edges) { badnesses[e] = edge_badness(e); - std::cerr << " (" << e.source().x() << "," << e.source().y() << ") - (" << e.target().x() << "," << e.target().y() << "): " << badnesses[e] << ";"; } - std::cerr << std::endl; + double thr; { std::vector tmp; tmp.reserve(badnesses.size()); @@ -1667,14 +1435,12 @@ void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLoo } std::nth_element(tmp.begin(), tmp.begin() + tmp.size() / 2, tmp.end()); double med = tmp[tmp.size() / 2]; - threshold = 4.0 * med; + thr = 10.0 * med; } - std::cerr << "badness threshold: " << threshold << std::endl; - std::set bad_edges; for (auto& p : badnesses) { - if (p.second > threshold) { + if (p.second > thr) { bad_edges.insert(p.first); } } @@ -1802,57 +1568,10 @@ void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLoo return best_x; }; - auto process_modifications = [&]( - Arrangement_2& arr_, - const std::set>& to_remove_, - const std::vector>& to_insert_) { - for (auto& e : to_remove_) { - bool removed = false; - for (auto he = arr_.edges_begin(); he != arr_.edges_end(); ++he) { - auto a = he->source()->point(); - auto b = he->target()->point(); - if ((a == e.first && b == e.second) || (a == e.second && b == e.first)) { - CGAL::remove_edge(arr_, he); - removed = true; - break; - } - } - if (!removed) { - std::cerr << "Warning: unable to locate edge for removal, skipping" << std::endl; - } - } - - for (auto& pq : to_insert_) { - if (pq.first == pq.second) { - continue; - } - CGAL::insert(arr_, Segment_2(pq.first, pq.second)); - } - }; - - size_t path_index = 0; for (auto& path : bad_paths) { - decltype(to_remove) to_remove_this_path; - decltype(to_insert) to_insert_this_path; - - std::cerr << "Processing bad path:"; - for (size_t i = 0; i < path.size() - 1; ++i) { - auto& a = path[i]; - auto& b = path[i + 1]; - std::cerr << " (" << a.x() << "," << a.y() << ") - (" << b.x() << "," << b.y() << ");"; - } - std::cerr << std::endl; - - for (size_t i = 0; i < path.size() - 1; ++i) { - auto& a = path[i]; - auto& b = path[i + 1]; - - debug_output.write_segment(a, b, "arr_bad_path path_nr_" + std::to_string(path_index)); - } - auto x = collapse_path(path); if (!x) { - std::cerr << "Unable to collapse path, skipping" << std::endl; + // std::cerr << "Unable to collapse path, skipping" << std::endl; continue; } @@ -1866,7 +1585,7 @@ void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLoo double new_length = std::sqrt(CGAL::to_double((path.front() - *x).squared_length())) + std::sqrt(CGAL::to_double((path.back() - *x).squared_length())); if (new_length > orig_length * 2 || orig_length > new_length * 2) { - std::cerr << "Collapsing path would increase length too much, skipping" << std::endl; + // std::cerr << "Collapsing path would increase length too much, skipping" << std::endl; continue; } @@ -1885,326 +1604,75 @@ void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLoo auto& b = path[i + 1]; if (a < b) { to_remove.insert({a, b}); - to_remove_this_path.insert({a, b}); } else { to_remove.insert({b, a}); - to_remove_this_path.insert({b, a}); } } auto s = path.front(); auto t = path.back(); if (s != *x) { to_insert.push_back({s, *x}); - to_insert_this_path.push_back({s, *x}); - - debug_output.write_segment(s, *x, "corrected_path path_nr_" + std::to_string(path_index)); } if (t != *x) { to_insert.push_back({t, *x}); - to_insert_this_path.push_back({t, *x}); - - debug_output.write_segment(t, *x, "corrected_path path_nr_" + std::to_string(path_index)); } - - path_index += 1; - -#if 1 - process_modifications(arr, to_remove_this_path, to_insert_this_path); -#else - auto arr_copy = arr; - process_modifications(arr_copy, to_remove_this_path, to_insert_this_path); - auto ious = arrangement_cell_iou(arr, arr_copy); - for (auto& iou : ious) { - std::cerr << " - cell iou: " << CGAL::to_double(iou) << std::endl; - } - std::swap(arr_copy, arr); -#endif } - process_modifications(arr, to_remove, to_insert); -} + /* + using Walk_pl = CGAL::Arr_walk_along_line_point_location; + Walk_pl walk_pl(arr); -template -void next_circular(typename Vec::const_iterator& it, const Vec& vec) { - std::advance(it, 1); - if (it == vec.end()) { - it = vec.begin(); - } -} + for (auto& e : to_remove) { + // debug_output.write_segment(e->source()->point(), e->target()->point(), "arr_bad_remove"); + auto res = walk_pl.locate(e.first); + if (auto* v = boost::get(&res)) { + Arrangement_2::Halfedge_around_vertex_circulator first, curr; + first = curr = (*v)->incident_halfedges(); + size_t i = 0; + std::array pts; + std::array hes; + do { + Arrangement_2::Vertex_const_handle u = curr->source(); + hes[i] = curr; + pts[i++] = u->point(); + } while (++curr != first); -template -void previous_circular(typename Vec::const_iterator& it, const Vec& vec) { - if (it == vec.begin()) { - it = vec.end(); - } - std::advance(it, -1); -} -template -std::size_t circular_distance(typename Vec::const_iterator first, - typename Vec::const_iterator last, - const Vec& vec) { - if (first <= last) { - return static_cast(last - first); - } - return static_cast(vec.end() - first) + static_cast(last - vec.begin()); -} - -template -std::pair -longest_wrapping_true_run(const Vec& v, Pred pred) { - using It = typename Vec::const_iterator; - - const auto n = v.size(); - if (n == 0) { - return {v.end(), v.end()}; - } - - // Find best non-wrapping run - std::size_t best_len = 0; - std::size_t best_start = 0; - - std::size_t curr_len = 0; - std::size_t curr_start = 0; - - for (std::size_t i = 0; i < n; ++i) { - if (pred(v[i])) { - if (curr_len == 0) { - curr_start = i; - } - ++curr_len; - if (curr_len > best_len) { - best_len = curr_len; - best_start = curr_start; + if ((*v)->point() != e.first) { + std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; + continue; } } else { - curr_len = 0; + std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; + continue; + } + } + */ + + for (auto& e : to_remove) { + bool removed = false; + for (auto he = arr.edges_begin(); he != arr.edges_end(); ++he) { + auto a = he->source()->point(); + auto b = he->target()->point(); + if ((a == e.first && b == e.second) || (a == e.second && b == e.first)) { + CGAL::remove_edge(arr, he); + removed = true; + break; + } + } + if (!removed) { + std::cerr << "Warning: unable to locate edge for removal, skipping" << std::endl; } } - // Count leading true - std::size_t leading = 0; - while (leading < n && pred(v[leading])) { - ++leading; - } - - // All true - if (leading == n) { - return {v.begin(), v.end()}; - } - - // Count trailing true - std::size_t trailing = 0; - while (trailing < n && pred(v[n - 1 - trailing])) { - ++trailing; - } - - // Wrapped run = [n - trailing, n) + [0, leading) - const std::size_t wrapped_len = leading + trailing; - - if (wrapped_len > best_len) { - It first = v.begin() + static_cast(n - trailing); - It last = v.begin() + static_cast(leading); - return {first, last}; - } - - It first = v.begin() + static_cast(best_start); - It last = first + static_cast(best_len); - return {first, last}; -} - -void clean_noisy_bounds(DebugWriter& debug_output, Arrangement_2& arr, SegmentLookup& segment_lookup, double threshold) { - using SK = CGAL::Simple_cartesian; - CGAL::Cartesian_converter C{}; - - auto other = [](const Segment_2& e, const Point_2& v) { - return (e.source() == v) ? e.target() : e.source(); - }; - - auto edge_badness = [&](const Segment_2& e) -> double { - auto closest = segment_lookup.n_closest_input_segments(e, 2); - if (closest.size() != 2) { - throw std::runtime_error("Unable to locate two nearby edges"); - } - - auto get_dir = [&](const Segment_2& s) { - auto a = C(s.source()); - auto b = C(s.target()); - SK::Vector_2 v = b - a; - double l = std::sqrt(v.squared_length()); - if (l <= 1e-12) { - return std::make_pair(SK::Vector_2(0, 0), 0.); - } - return std::make_pair(v / l, l); - }; - - auto [own_dir, own_length] = get_dir(e); - - auto angle = [&](const SK::Vector_2& ov) { - double d = std::abs(own_dir * ov); - if (d > 1.0) { - d = 1.0; - } - return std::acos(d); - }; - - double best = std::numeric_limits::infinity(); - for (auto& s : closest) { - auto [dv, dl] = get_dir(s); - best = std::min(best, angle(dv)); - } - return (best + 0.01) / own_length; - }; - - size_t facet_index = 0; - for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it, ++facet_index) { - std::cout << "facet_index " << facet_index << std::endl; - if (!it->is_unbounded()) { - std::set> to_remove; - std::vector> to_insert; - - std::vector segs; - std::vector vertices; - std::vector halfedges; - - auto circ = it->outer_ccb(); - do { - auto a = circ->source()->point(); - auto b = circ->target()->point(); - segs.emplace_back(a, b); - vertices.push_back(circ->source()); - halfedges.push_back(circ); - ++circ; - } while (circ != it->outer_ccb()); - - std::cerr << "badnesses:"; - std::vector badnesses; - for (auto& e : segs) { - badnesses.push_back(edge_badness(e)); - std::cerr << " (" << e.source().x() << "," << e.source().y() << ") - (" << e.target().x() << "," << e.target().y() << "): " << badnesses.back() << ";"; - } - std::cerr << std::endl; - - auto bit = std::min_element(badnesses.begin(), badnesses.end()); - if (*bit > threshold) { - std::cerr << "All edges are good, skipping" << std::endl; - continue; - } - - auto it_pair = longest_wrapping_true_run(badnesses, [&](double d) { return d > threshold; }); - auto N = circular_distance(it_pair.first, it_pair.second, badnesses); - - if (N == 0) { - std::cerr << "Unable to find run of bad edges, skipping" << std::endl; - continue; - } - - std::vector> incoming_paths; - - std::cout << "range " << std::distance(badnesses.cbegin(), it_pair.first) << " to " << std::distance(badnesses.cbegin(), it_pair.second) << " length " << N << std::endl; - - auto jt = it_pair.first; - for (std::size_t k = 0; k < N; ++k, next_circular(jt, badnesses)) { - - std::cout << " at " << std::distance(badnesses.cbegin(), jt) << " badness: " << *jt << std::endl; - - auto he = halfedges[std::distance(badnesses.cbegin(), jt)]; - to_remove.insert({he->source()->point(), he->target()->point()}); - debug_output.write_segment(he->source()->point(), he->target()->point(), "arr_bad_bound facet_" + std::to_string(facet_index)); - - Arrangement_2::Vertex_handle v = he->source(); - - // circle around other edges onto v - Arrangement_2::Halfedge_around_vertex_circulator first, curr; - first = curr = v->incident_halfedges(); - do { - Arrangement_2::Vertex_handle u = curr->source(); - if (curr->face() != it && curr->twin()->face() != it) { - - // loop until we find a 3-degree vertex, or we come back to the start - std::vector path{v->point(), u->point()}; - auto he = curr; - - while (u->degree() == 2 && u != v && path.size() < 10) { - std::vector hes; - - { - Arrangement_2::Halfedge_around_vertex_circulator first, curr; - first = curr = u->incident_halfedges(); - do { - hes.push_back(curr); - curr++; - } while (curr != first); - } - - auto next_he = hes.front() != he && hes.front() != he->twin() ? hes.front() : hes.back(); - auto next_v = next_he->target() != u ? next_he->target() : next_he->source(); - - path.push_back(next_v->point()); - u = next_v; - } - incoming_paths.push_back(std::move(path)); - } - } while (++curr != first); - } - - const std::size_t start = - static_cast(std::distance(badnesses.cbegin(), it_pair.first)); - - auto n = badnesses.size(); - - auto wrap = [n](std::ptrdiff_t i) -> std::size_t { - i %= static_cast(n); - if (i < 0) { - i += static_cast(n); - } - return static_cast(i); - }; - - const std::size_t ib = start; - const std::size_t ia = wrap(static_cast(start) - 1); - const std::size_t ic = wrap(static_cast(start + N)); - const std::size_t id = wrap(static_cast(start + N + 1)); - - auto a = vertices.begin() + static_cast(ia); - auto b = vertices.begin() + static_cast(ib); - auto c = vertices.begin() + static_cast(ic); - auto d = vertices.begin() + static_cast(id); - - CGAL::Ray_2 r1((*a)->point(), (*b)->point()); - CGAL::Ray_2 r2((*d)->point(), (*c)->point()); - - std::cout << "a: (" << (*a)->point().x() << "," << (*a)->point().y() << ") b: (" << (*b)->point().x() << "," << (*b)->point().y() << ") c: (" << (*c)->point().x() << "," << (*c)->point().y() << ") d: (" << (*d)->point().x() << "," << (*d)->point().y() << ")" << std::endl; - - auto x = CGAL::intersection(r1, r2); - if (x) { - if (auto* xp = variant_get>(&*x)) { - std::cout << "ray xp: (" << xp->x() << "," << xp->y() << ")" << std::endl; - to_insert.emplace_back((*b)->point(), *xp); - to_insert.emplace_back((*c)->point(), *xp); - - debug_output.write_segment((*b)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); - debug_output.write_segment((*c)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); - } - } else { - CGAL::Line_2 r1((*a)->point(), (*b)->point()); - CGAL::Line_2 r2((*d)->point(), (*c)->point()); - - auto x = CGAL::intersection(r1, r2); - if (x) { - if (auto* xp = variant_get>(&*x)) { - std::cout << "line xp: (" << xp->x() << "," << xp->y() << ")" << std::endl; - to_insert.emplace_back((*b)->point(), *xp); - to_insert.emplace_back((*c)->point(), *xp); - - debug_output.write_segment((*b)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); - debug_output.write_segment((*c)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); - } - } - } + for (auto& pq : to_insert) { + if (pq.first == pq.second) { + continue; } + CGAL::insert(arr, Segment_2(pq.first, pq.second)); + // debug_output.write_segment(pq.first, pq.second, "arr_bad_insert"); } + } void remove_colinear_vertices(Arrangement_2& arr) { @@ -2257,31 +1725,20 @@ class timer { public: class entry { public: - entry() {} - entry(std::map::const_iterator start_it) : start_it(start_it) {} - void stop() { - if (start_it) { - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration(end - start_it.value()->second).count(); - std::cerr << "Timing for " << start_it.value()->first << ": " << duration << " ms" << std::endl; - } + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration(end - start_it->second).count(); + std::cerr << "Timing for " << start_it->first << ": " << duration << " ms" << std::endl; } private: - std::optional::const_iterator> start_it; + std::map::const_iterator start_it; }; - timer(bool enabled = true) : enabled_(enabled) {} - entry start(const std::string& name) { - if (enabled_) { - return entry(timings_.insert({name, std::chrono::high_resolution_clock::now()}).first); - } else { - return entry(); - } + return entry(timings_.insert({name, std::chrono::high_resolution_clock::now()}).first); } private: @@ -2289,30 +1746,27 @@ class timer { std::string, std::chrono::high_resolution_clock::time_point> timings_; - - bool enabled_; }; -void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std::vector& input_polygons_, std::vector& output_polygons, double polygon_offset_distance = -1.) { +void arrange_cgal_polygons(const std::vector& input_polygons_, std::vector& output_polygons, double polygon_offset_distance = -1.) { static const double OVERLAP_RESOLUTION_DISTANCE = 1.e-1; // even larger amount of inset so that outer perimeter is safely within all input polygons even when overlap resolution is applied // no, `1.e-2 + 1.e-5` creates issues with the outer perimeter, are there other tolerances in play? static const double OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT = 1.e-5; - DebugWriter debug_output; - if (settings.debug_output) { - auto t = std::time(nullptr); - auto tm = *std::localtime(&t); +#ifdef SVGFILL_DEBUG + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); - std::ostringstream oss; - oss << std::put_time(&tm, "arrangement_%Y%m%d%H%M%S"); - auto now = oss.str(); - debug_output = DebugWriter(true, now); - } else { - debug_output = DebugWriter(false, ""); - } + std::ostringstream oss; + oss << std::put_time(&tm, "arrangement_%Y%m%d%H%M%S"); + auto now = oss.str(); + DebugWriter debug_output(true, now); +#else + DebugWriter debug_output(false, ""); +#endif - timer timer(settings.debug_output); + timer timer; auto t0 = timer.start("input"); @@ -2340,7 +1794,7 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std t0.stop(); t0 = timer.start("overlap elimination"); - eliminate_overlaps(debug_output, OVERLAP_RESOLUTION_DISTANCE, input_polygons); + eliminate_overlaps(OVERLAP_RESOLUTION_DISTANCE, input_polygons); t0.stop(); @@ -2359,80 +1813,80 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std debug_output.write_polygons(input_polygons, "processed_input"); - std::vector outer_perimiter; - if (settings.outer_perimiter_algo == 0) { - t0 = timer.start("outer perimeter"); +#if 1 + t0 = timer.start("outer perimeter"); - // Find the outer perimeter using offset - union - negative offset - std::vector offset_polygons; - for (auto& r : input_polygons) { - auto R = r; - if (!R.is_counterclockwise_oriented()) { - R.reverse_orientation(); - } - - // Overlap removal can also result in close points causing problems when converted into non-exact nt - remove_close_points(R); - - auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); - for (auto& p : ps) { - if (!p.is_simple()) { - throw std::runtime_error("Complex polygon originated from offset"); - } - } - offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); + // Find the outer perimeter using offset - union - negative offset + std::vector offset_polygons; + for (auto& r : input_polygons) { + auto R = r; + if (!R.is_counterclockwise_oriented()) { + R.reverse_orientation(); } - debug_output.write_polygons(offset_polygons, "offset_input"); + // Overlap removal can also result in close points causing problems when converted into non-exact nt + remove_close_points(R); - // Perform Boolean union on the offset polygons - std::vector unioned_polygons; - CGAL::join(offset_polygons.begin(), offset_polygons.end(), std::back_inserter(unioned_polygons)); - - if (unioned_polygons.size() > 1) { - // @todo this is currently one of the major limitations in the code that still can be eliminated - // by grouping the input polygons by their perimiter polygon in unioned_polygons - std::sort(unioned_polygons.begin(), unioned_polygons.end(), [](auto& p, auto& q) { return p.outer_boundary().area() > q.outer_boundary().area(); }); - } - - debug_output.write_polygon(unioned_polygons.front().outer_boundary(), "offset_joined"); - - Polygon_2 fused_removed_close_points = unioned_polygons.front().outer_boundary(); - remove_close_points(fused_removed_close_points, 1.e-4); - - // Apply negative offset to get the outer perimeter polygon - outer_perimiter = create_and_convert_offset_polygon( - // Because polygon_offset is inexact, make sure our inset distance is slightly larger - // std::nexttoward(-polygon_offset_distance, -std::numeric_limits::infinity()), - - // 1.e-8 even was too little and still resulted in slivers of triangle around the perimeter - -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, - fused_removed_close_points); - - debug_output.write_polygons(outer_perimiter, "outer_perimiter"); - } else { - std::map> neighbour_map; - build_radial_neighbour_map(input_polygons, polygon_offset_distance, neighbour_map); - - auto start_vertex = neighbour_map.rbegin()->first; - auto next_vertex = neighbour_map.rbegin()->second.front(); - - std::vector cycle = {start_vertex, next_vertex}; - while (cycle.back() != cycle.front()) { - const auto& incoming_from = *(cycle.rbegin() + 1); - const auto& nb = neighbour_map[cycle.back()]; - auto it = std::find(nb.begin(), nb.end(), incoming_from); - // cycle it -1 around nb - if (it == nb.begin()) { - it = nb.end() - 1; - } else { - --it; + auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); + for (auto& p : ps) { + if (!p.is_simple()) { + throw std::runtime_error("Complex polygon originated from offset"); } - cycle.push_back(*it); } - outer_perimiter.emplace_back(cycle.begin(), cycle.end()); + offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); } + debug_output.write_polygons(offset_polygons, "offset_input"); + + // Perform Boolean union on the offset polygons + std::vector unioned_polygons; + CGAL::join(offset_polygons.begin(), offset_polygons.end(), std::back_inserter(unioned_polygons)); + + if (unioned_polygons.size() > 1) { + // @todo this is currently one of the major limitations in the code that still can be eliminated + // by grouping the input polygons by their perimiter polygon in unioned_polygons + std::sort(unioned_polygons.begin(), unioned_polygons.end(), [](auto& p, auto& q) { return p.outer_boundary().area() > q.outer_boundary().area(); }); + } + + debug_output.write_polygon(unioned_polygons.front().outer_boundary(), "offset_joined"); + + Polygon_2 fused_removed_close_points = unioned_polygons.front().outer_boundary(); + remove_close_points(fused_removed_close_points, 1.e-4); + + // Apply negative offset to get the outer perimeter polygon + auto outer_perimiter = create_and_convert_offset_polygon( + // Because polygon_offset is inexact, make sure our inset distance is slightly larger + // std::nexttoward(-polygon_offset_distance, -std::numeric_limits::infinity()), + + // 1.e-8 even was too little and still resulted in slivers of triangle around the perimeter + -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, + fused_removed_close_points); + + debug_output.write_polygons(outer_perimiter, "outer_perimiter"); +#else + std::map> neighbour_map; + build_radial_neighbour_map(input_polygons, polygon_offset_distance, neighbour_map); + + auto start_vertex = neighbour_map.rbegin()->first; + auto next_vertex = neighbour_map.rbegin()->second.front(); + + std::vector cycle = {start_vertex, next_vertex}; + while (cycle.back() != cycle.front()) { + const auto& incoming_from = *(cycle.rbegin() + 1); + const auto& nb = neighbour_map[cycle.back()]; + auto it = std::find(nb.begin(), nb.end(), incoming_from); + // cycle it -1 around nb + if (it == nb.begin()) { + it == nb.end() - 1; + } else { + --it; + } + cycle.push_back(*it); + } + std::vector outer_perimiter; + outer_perimiter.emplace_back(cycle.begin(), cycle.end()); +#endif + t0.stop(); t0 = timer.start("corridor creation"); @@ -2457,10 +1911,8 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std // subdivide difference_result to have better more detailed triangulation and therefore less-pronounced artefacts in midpoint network - auto subdivision_length = polygon_offset_distance / settings.subdivision_factor; - for (auto& pwh : difference_result) { - difference_result_subdivided.push_back(subdivide_polygon(subdivision_length, pwh)); + difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 8., pwh)); // difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 64., pwh)); } @@ -2488,43 +1940,13 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std SegmentLookup segment_lookup(input_polygons); - auto [line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length] = build_line_graph(input_polygons, segment_lookup, triangular_polygons); + auto [line_graph, midpoint_to_segment, segment_to_input_facet] = build_line_graph(input_polygons, segment_lookup, triangular_polygons); for (auto& p : line_graph) { for (auto& q : p.second) { debug_output.write_segment(p.first, q, "network_1"); } } - // Write a JSON structure with center line topology with the midpoint_to_edge_length as per-point data - { - std::ofstream ofs("center_line_topology.json"); - ofs << "{\n"; - ofs << " \"vertices\": [\n"; - bool first_vertex = true; - for (auto& p : line_graph) { - if (!first_vertex) { - ofs << ",\n"; - } - first_vertex = false; - ofs << " {\n"; - ofs << " \"point\": [" << p.first.x() << ", " << p.first.y() << "],\n"; - ofs << " \"width\":" << midpoint_to_edge_length.find(p.first)->second << ",\n"; - ofs << " \"connected_to\": [\n"; - bool first_connected = true; - for (auto& q : p.second) { - if (!first_connected) { - ofs << ",\n"; - } - first_connected = false; - ofs << " [" << q.x() << ", " << q.y() << "]"; - } - ofs << "\n ]\n"; - ofs << " }"; - } - ofs << "\n ]\n"; - ofs << "}\n"; - } - t0.stop(); t0 = timer.start("center line cleaning"); @@ -2559,7 +1981,7 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std t0 = timer.start("topology"); - auto segments = extend_end_vertices_based_on_input(G, midpoint_to_segment, segment_to_input_facet, outer_perimiter, segment_lookup, subdivision_length * 4); + auto segments = extend_end_vertices_based_on_input(G, midpoint_to_segment, segment_to_input_facet, outer_perimiter, segment_lookup); // Now plot the edges on an arrangement in order to find planar cycles // and merge the corridor-halves with their neighbouring input polygon @@ -2575,31 +1997,31 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std debug_output.write_segment(pq.first, pq.second, "extended_segments"); } - if (settings.topology_reconstruction_algo != 0) { - // Write input polygons to arrangement_2 - // We no longer do this because we add the outer perimiter now, subdivided by the corridor network which is extended and intersected with the outer perimiter - for (auto& poly : input_polygons) { - for (size_t i = 0; i != poly.size(); ++i) { - auto j = (i + 1) % poly.size(); - if (poly.vertex(i) == poly.vertex(j)) { - continue; - } - CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); - } - } - } else { - // Write outer perimeter to arrangement_2 - for (auto& p : outer_perimiter) { - for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { - auto source = it->source(); - auto target = it->target(); - if (source == target) { - continue; - } - CGAL::insert(arr, Segment_2(source, target)); +#if 0 + // Write input polygons to arrangement_2 + // We no longer do this because we add the outer perimiter now, subdivided by the corridor network which is extended and intersected with the outer perimiter + for (auto& poly : input_polygons) { + for (size_t i = 0; i != poly.size(); ++i) { + auto j = (i + 1) % poly.size(); + if (poly.vertex(i) == poly.vertex(j)) { + continue; } + CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); } } +#else + // Write outer perimeter to arrangement_2 + for (auto& p : outer_perimiter) { + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + auto source = it->source(); + auto target = it->target(); + if (source == target) { + continue; + } + CGAL::insert(arr, Segment_2(source, target)); + } + } +#endif // Just for the automatic numbering, create a full vector std::vector temp; @@ -2625,17 +2047,12 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std // corridor network we know it needs to be joined with an input polygon. In that // case the edges need to be eliminated that correspond to original geometry. - if (settings.topology_reconstruction_algo != 0) { - fuse_corridor_halves_with_input(arr, G, segment_lookup, input_polygons, debug_output); - } - - if (settings.perform_cleanup) { - remove_colinear_vertices(arr); - double threshold; - clean_noisy_paths(debug_output, arr, segment_lookup, threshold); - remove_colinear_vertices(arr); - clean_noisy_bounds(debug_output, arr, segment_lookup, threshold); - } +#if 0 + fuse_corridor_halves_with_input(arr, G, segment_lookup, input_polygons, debug_output); +#else + remove_colinear_vertices(arr); + clean_noisy_paths(arr, segment_lookup); +#endif t0.stop(); @@ -2651,7 +2068,8 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std #ifndef SVGFILL_MAIN -bool svgfill::arrange_polygons(arrange_polygon_settings settings, const std::vector& polygons, std::vector& arranged) { +bool svgfill::arrange_polygons(const std::vector& polygons, std::vector& arranged) +{ std::vector cgal_polygons, cgal_polygons_out; std::transform(polygons.begin(), polygons.end(), std::back_inserter(cgal_polygons), [](auto& poly) { Polygon_2 result; @@ -2660,7 +2078,7 @@ bool svgfill::arrange_polygons(arrange_polygon_settings settings, const std::vec }); return result; }); - arrange_cgal_polygons(settings, cgal_polygons, cgal_polygons_out); + arrange_cgal_polygons(cgal_polygons, cgal_polygons_out); std::transform(cgal_polygons_out.begin(), cgal_polygons_out.end(), std::back_inserter(arranged), [](auto& poly) { svgfill::polygon_2 result; std::transform(poly.begin(), poly.end(), std::back_inserter(result.boundary), [](auto& pt) { @@ -2710,7 +2128,7 @@ int main(int argc, char** argv) { input_polygons.back().push_back(CGAL::Point_2(x, y)); } } - arrange_cgal_polygons(arrange_polygon_settings{}, input_polygons, output); + arrange_cgal_polygons(input_polygons, output); break; } return 0; @@ -2723,7 +2141,7 @@ int main(int argc, char** argv) { input_polygons = { rect1, rect2, rect3, rect4, rect5 }; } - arrange_cgal_polygons(arrange_polygon_settings{}, input_polygons, output); + arrange_cgal_polygons(input_polygons, output); return 0; } diff --git a/src/svgfill/src/svgfill.cpp b/src/svgfill/src/svgfill.cpp index cf45881c93..8a2a1bb008 100644 --- a/src/svgfill/src/svgfill.cpp +++ b/src/svgfill/src/svgfill.cpp @@ -483,18 +483,6 @@ public: return ps; } - size_t delete_same_facet_edge_pairs() { - size_t n_deleted = 0; - for (auto it = arr.edges_begin(); it != arr.edges_end();) { - decltype(it) current = it++; - if (current->face() == current->twin()->face()) { - arr.remove_edge(current); - n_deleted++; - } - } - return n_deleted; - } - void merge(const std::vector& edge_indices) { if (edge_indices.empty()) { return; diff --git a/src/svgfill/src/svgfill.h b/src/svgfill/src/svgfill.h index fab4dd0142..396fc9c924 100644 --- a/src/svgfill/src/svgfill.h +++ b/src/svgfill/src/svgfill.h @@ -67,7 +67,6 @@ namespace svgfill { virtual std::vector get_face_pairs() = 0; virtual size_t num_edges() = 0; virtual size_t num_faces() = 0; - virtual size_t delete_same_facet_edge_pairs() = 0; }; class SVGFILL_API context { @@ -102,7 +101,6 @@ namespace svgfill { void write(std::vector>&); size_t num_edges() { return arr_->num_edges(); } size_t num_faces() { return arr_->num_faces(); } - size_t delete_same_facet_edge_pairs() { return arr_->delete_same_facet_edge_pairs(); } ~context() { delete arr_; @@ -115,22 +113,7 @@ namespace svgfill { SVGFILL_API std::string polygons_to_svg(const std::vector>& polygons, bool random_color=false); SVGFILL_API std::string polygons_to_svg(const std::vector& polygons, bool random_color = false); SVGFILL_API bool svg_to_polygons(const std::string& data, const boost::optional& class_name, std::vector& polygons); - - struct SVGFILL_API arrange_polygon_settings { - bool debug_output = false; - // -1: compute from average edge length - double polygon_offset_distance = -1.; - // 0: use offset - union - negative offset to find the outer perimeter - // 1: radial walk along vertices; exact, but can only reuse vertices, not create new positions by means of intersections - int outer_perimiter_algo = 0; - // 0: outer perimiter and corridor center lines - // 1: input polygons, corridor center lines and segments connecting corridor center lines to input polygons - int topology_reconstruction_algo = 0; - bool perform_cleanup = true; - double subdivision_factor = 16.; - }; - - SVGFILL_API bool arrange_polygons(arrange_polygon_settings settings, const std::vector& polygons, std::vector& arranged); - } + SVGFILL_API bool arrange_polygons(const std::vector& polygons, std::vector& arranged); +} #endif From 90bd7d26acc91670d2d5362f9d9ab50e67652cab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:45:51 +0000 Subject: [PATCH 02/25] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/publish-aichat-app.yaml | 2 +- .github/workflows/publish-pyodide-demo-app.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-aichat-app.yaml b/.github/workflows/publish-aichat-app.yaml index 776781428e..917b0b282f 100644 --- a/.github/workflows/publish-aichat-app.yaml +++ b/.github/workflows/publish-aichat-app.yaml @@ -32,7 +32,7 @@ jobs: submodules: recursive fetch-depth: 0 - name: Checkout intermediate Pages repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: IfcOpenShell/aichat_ifcopenshell_org_static_html ref: gh-pages diff --git a/.github/workflows/publish-pyodide-demo-app.yml b/.github/workflows/publish-pyodide-demo-app.yml index 9f4c3ddbf8..6b0141fc29 100644 --- a/.github/workflows/publish-pyodide-demo-app.yml +++ b/.github/workflows/publish-pyodide-demo-app.yml @@ -32,7 +32,7 @@ jobs: submodules: recursive fetch-depth: 0 - name: Checkout intermediate Pages repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: IfcOpenShell/wasm_ifcopenshell_org_static_html ref: gh-pages From 06cfd0931c43482646821bdc594dad82fc7ce874 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:45:08 +0000 Subject: [PATCH 03/25] Bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/publish-aichat-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-aichat-app.yaml b/.github/workflows/publish-aichat-app.yaml index 917b0b282f..20369577f5 100644 --- a/.github/workflows/publish-aichat-app.yaml +++ b/.github/workflows/publish-aichat-app.yaml @@ -42,7 +42,7 @@ jobs: run: | rsync -av --delete --exclude='.git/' src/ifcchat/ output/ - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - name: Download wheels From ebd5fe854f9e815f324fd1f4af11799858a7e907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:45:03 +0000 Subject: [PATCH 04/25] Bump ruff from 0.15.8 to 0.15.9 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.8 to 0.15.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.8...0.15.9) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 330f6150d0..289a6c6415 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "IfcOpenShell" version = "0.0.0" dependencies = [ "black==26.3.1", - "ruff==0.15.8", + "ruff==0.15.9", "poethepoet", "gersemi==0.26.1", ] From b4558f7f759dda23d097494c11a49bd6a3c28433 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Thu, 9 Apr 2026 00:43:04 +0100 Subject: [PATCH 05/25] Fix ruff import ordering complaints --- src/bonsai/bonsai/tool/ifcgit.py | 4 ++-- src/bonsai/bonsai/tool/raycast.py | 5 ++--- src/bonsai/test/core/test_ifcgit.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 9c7199e839..2db7a5a171 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -288,9 +288,9 @@ class IfcGit: bpy.data.orphans_purge(do_recursive=True) # ty:ignore[unknown-argument] - from bonsai.bim.module.root.data import IfcClassData - from bonsai.bim.module.model.data import AuthoringData import bonsai.bim.handler + from bonsai.bim.module.model.data import AuthoringData + from bonsai.bim.module.root.data import IfcClassData AuthoringData.type_thumbnails = {} diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 38882a5095..dc45b4e9bb 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -25,13 +25,12 @@ import bmesh import bpy import mathutils import numpy as np -from mathutils import Vector - from bpy_extras import view3d_utils +from mathutils import Vector import bonsai.core.tool import bonsai.tool as tool -from bpy_extras import view3d_utils + class Raycast(bonsai.core.tool.Raycast): offset = 10 diff --git a/src/bonsai/test/core/test_ifcgit.py b/src/bonsai/test/core/test_ifcgit.py index 539e4a082c..4884497c8b 100644 --- a/src/bonsai/test/core/test_ifcgit.py +++ b/src/bonsai/test/core/test_ifcgit.py @@ -20,7 +20,7 @@ import pytest import bonsai.core.ifcgit as subject -from test.core.bootstrap import ifcgit, ifc +from test.core.bootstrap import ifc, ifcgit class MockOperator: From 217bfed847b1c63458dff197bc4da270ecd93b6d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 10 Apr 2026 19:05:54 +1000 Subject: [PATCH 06/25] Add py313 to stable build --- .github/workflows/ci-bonsai.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-bonsai.yml b/.github/workflows/ci-bonsai.yml index 4c8364a958..6fcc61658f 100644 --- a/.github/workflows/ci-bonsai.yml +++ b/.github/workflows/ci-bonsai.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - pyver: [py311, py312] + pyver: [py311, py312, py313] config: - { name: "Windows Build", @@ -42,6 +42,11 @@ jobs: name: "MacOS ARM Build", short_name: macosm1, } + exclude: + # Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0. + - pyver: py313 + config: + short_name: macos steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 # https://github.com/actions/setup-python From c509f1d3eed76520e6300598293d1c98e9af8ecd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:29:45 +0000 Subject: [PATCH 07/25] Bump vite from 6.4.1 to 6.4.2 in /src/ifctester/webapp Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.4.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- src/ifctester/webapp/package-lock.json | 8 ++++---- src/ifctester/webapp/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index 1244317fd2..90a6743443 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -33,7 +33,7 @@ "tailwindcss": "^4.0.0", "tw-animate-css": "^1.3.2", "typescript": "^5.8.3", - "vite": "^6.4.1" + "vite": "^6.4.2" } }, "node_modules/@ampproject/remapping": { @@ -3264,9 +3264,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/ifctester/webapp/package.json b/src/ifctester/webapp/package.json index 14b54ad8ed..f36baf96ec 100644 --- a/src/ifctester/webapp/package.json +++ b/src/ifctester/webapp/package.json @@ -30,7 +30,7 @@ "tailwindcss": "^4.0.0", "typescript": "^5.8.3", "tw-animate-css": "^1.3.2", - "vite": "^6.4.1" + "vite": "^6.4.2" }, "dependencies": { "eventemitter3": "^5.0.1", From 16899602576c6460dc4ad54e32fd92aee508f2cc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 16:19:36 +0500 Subject: [PATCH 08/25] Maintenence - document ci-bonsai.yml update --- src/bonsai/docs/guides/development/maintenance.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/docs/guides/development/maintenance.rst b/src/bonsai/docs/guides/development/maintenance.rst index e1728fd7a5..e35306de1f 100644 --- a/src/bonsai/docs/guides/development/maintenance.rst +++ b/src/bonsai/docs/guides/development/maintenance.rst @@ -44,6 +44,8 @@ When a new Blender version is released and supported: * - File - What to update + * - ``.github/workflows/ci-bonsai.yml`` + - ``pyver`` matrix * - ``.github/workflows/ci-bonsai-daily.yml`` - Blender download URL From 6242251d3cd8bcce47a283dfed3f3e94e3312e07 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 1 Apr 2026 11:11:47 +0500 Subject: [PATCH 09/25] Fix typo --- nix/build-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/build-all.py b/nix/build-all.py index 46b7b5c30c..f5436262d2 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -1515,7 +1515,7 @@ if "IfcOpenShell-Python" in targets: ) # Copy setup.py where pyodide build system expects it. shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH) - # Empty pyproject so it's contents won't affect the resulting wheelthe the + # Empty pyproject so it's contents won't affect the resulting wheel # otherwise the wheel will use version and dependencies from toml, not setup.py. (REPO_PATH / "pyproject.toml").write_text("") From c8f46cfb697454b38abaa206c7484cf7346a7b49 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 3 Apr 2026 12:37:53 +0500 Subject: [PATCH 10/25] build_pyodide.sh - use emsdk from pyodide --- pyodide/build_pyodide.sh | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/pyodide/build_pyodide.sh b/pyodide/build_pyodide.sh index 20ad946162..db5c5f08b0 100755 --- a/pyodide/build_pyodide.sh +++ b/pyodide/build_pyodide.sh @@ -14,18 +14,11 @@ source .venv/bin/activate uv pip install pyodide-build # `uv run` is required, so xbuildenv would skip using `pip`. uv run pyodide xbuildenv install +uv run pyodide xbuildenv install-emscripten -# Emscripten doesn't come with xbuildenv. -if [ ! -d emsdk ]; then - git clone https://github.com/emscripten-core/emsdk -fi -pushd emsdk -PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version) -./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION} -./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION} -source emsdk_env.sh +EMSDK_ROOT=$(pyodide config get emscripten_dir) +source ${EMSDK_ROOT}/emsdk_env.sh which emcc -popd mkdir -p packages/ifcopenshell VERSION=`cat IfcOpenShell/VERSION` From b9d4ea38b015b336686609ce05a38644c223a8fb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 12:12:27 +0500 Subject: [PATCH 11/25] Script for packing pyodide wheel --- pyodide/pack_wheel.py | 232 ++++++++++++++++++++++++++++++++++++++++++ pyodide/setup.py | 40 +++++++- 2 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 pyodide/pack_wheel.py diff --git a/pyodide/pack_wheel.py b/pyodide/pack_wheel.py new file mode 100644 index 0000000000..7b6c2a63d5 --- /dev/null +++ b/pyodide/pack_wheel.py @@ -0,0 +1,232 @@ +# +# /// script +# # Latest Pyodide build env versions are listed here: +# # https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json +# # https://github.com/pyodide/pyodide-build/blob/main/pyodide_build/xbuildenv_releases.py +# requires-python = "==3.13.2" +# dependencies = [ +# "requests", +# "setuptools", +# ] +# /// +""" +Pack an IfcOpenShell WASM wheel using Pyodide build system. + +Usage: + uv run make_wheel.py # Show this help + uv run make_wheel.py --build # Build wheel + uv run make_wheel.py --clean # Clean build artifacts and exit +""" + +import argparse +import os +import re +import shutil +import subprocess +import time +import zipfile +from pathlib import Path +from urllib.parse import quote + +import requests + +# Get repo root (parent of this script's parent directory) +REPO_ROOT = Path(__file__).parent.parent +PYODIDE_DIR = REPO_ROOT / "pyodide" +BUILD_DIR = PYODIDE_DIR / "build" + +# Hardcoded path (Windows packing workaround with --dev flag) +PYODIDE_BUILD = Path(r"L:\Projects\Github\pyodide-build") + +# Wheel platform tag (from PYODIDE_EMSCRIPTEN_VERSION in pyodide-build/Makefile.envs) +WHEEL_PLATFORM_TAG = "emscripten_4_0_9_wasm32" + +# Location where ifcopenshell will be extracted +IFCOPENSHELL_DIR = PYODIDE_DIR / "ifcopenshell" + + +class WheelBuilder: + @staticmethod + def extract_ifcopenshell_from_git(dst: Path) -> None: + """Extract ifcopenshell directory from git repo into destination.""" + Tools.rmrf(dst) + + print(f"Extracting ifcopenshell from git to {dst}...") + # Use git ls-files piped to git checkout-index to avoid copying + # untracked or ignored files from the actual repo. + ls_proc = subprocess.Popen( + ["git", "ls-files", "-z", "src/ifcopenshell-python/ifcopenshell"], + cwd=REPO_ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + checkout_proc = subprocess.Popen( + ["git", "checkout-index", "-z", "--prefix", "pyodide/", "--stdin"], + cwd=REPO_ROOT, + stdin=ls_proc.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert ls_proc.stdout is not None + ls_proc.stdout.close() + checkout_proc.communicate() + + if checkout_proc.returncode != 0: + assert checkout_proc.stderr is not None + raise RuntimeError(f"Failed to extract: {checkout_proc.stderr.decode()}") + + # Move src/ifcopenshell-python/ifcopenshell to ifcopenshell. + temp_src = PYODIDE_DIR / "src" / "ifcopenshell-python" / "ifcopenshell" + shutil.move(temp_src, dst) + + # Clean up temporary src directory. + Tools.rmrf(PYODIDE_DIR / "src") + + print("✓ Extracted ifcopenshell from git") + + @staticmethod + def get_wheel_url(makefile_path: Path) -> str: + """Get S3 wheel URL based on BINARY_VERSION and BUILD_COMMIT from Makefile.""" + + def parse_makefile_vars() -> dict[str, str]: + content = makefile_path.read_text() + vars: dict[str, str] = {} + for match in re.finditer(r"^(BINARY_VERSION|BUILD_COMMIT):=(.+)$", content, re.MULTILINE): + vars[match.group(1)] = match.group(2).strip() + return vars + + vars: dict[str, str] = parse_makefile_vars() + binary_version = vars["BINARY_VERSION"] + build_commit = vars["BUILD_COMMIT"] + filename = f"ifcopenshell-{binary_version}+{build_commit}-cp313-cp313-pyodide_2025_0_wasm32.whl" + encoded_filename = quote(filename, safe="") + return f"https://s3.amazonaws.com/ifcopenshell-builds/{encoded_filename}" + + @staticmethod + def download_and_extract_so(url: str, build_dir: Path) -> tuple[Path, Path]: + """Download wheel from URL and extract .so and .py files.""" + py_wrapper_filename = "ifcopenshell_wrapper.py" + build_dir.mkdir(parents=True, exist_ok=True) + + wheel_path = build_dir / url.rsplit("/", 1)[-1] + + if wheel_path.exists(): + print(f"Using cached wheel: {wheel_path}") + else: + print(f"Downloading {url}...") + response = requests.get(url) + response.raise_for_status() + wheel_path.write_bytes(response.content) + + print("Extracting _ifcopenshell_wrapper files...") + with zipfile.ZipFile(wheel_path) as zf: + so_files = [f for f in zf.namelist() if f.endswith(".so")] + py_files = [f for f in zf.namelist() if f.endswith(py_wrapper_filename)] + + assert so_files, "No .so file found in wheel" + assert py_files, f"No {py_wrapper_filename} file found in wheel" + + so_file = so_files[0] + so_dst = build_dir / Path(so_file).name + so_dst.write_bytes(zf.read(so_file)) + + py_file = py_files[0] + py_dst = build_dir / Path(py_file).name + py_dst.write_bytes(zf.read(py_file)) + + return so_dst, py_dst + + +class Tools: + @staticmethod + def run( + cmd: list[str], + cwd: Path | None = None, + ) -> None: + print(f"$ {' '.join(cmd)}") + subprocess.check_call(cmd, cwd=cwd) + + @staticmethod + def create_symlink(dst: Path, src: Path) -> None: + Tools.rmrf(dst) + dst.symlink_to(src) + + @staticmethod + def rmrf(path: Path) -> None: + if path.exists() or path.is_symlink(): + if path.is_dir() and not path.is_symlink(): + shutil.rmtree(path) + else: + path.unlink() + + +def clean() -> None: + """Remove build artifacts.""" + paths_to_remove = ( + BUILD_DIR, + PYODIDE_DIR / ".pyodide_build", + PYODIDE_DIR / "dist", + PYODIDE_DIR / "ifcopenshell.egg-info", + PYODIDE_DIR / "src", + IFCOPENSHELL_DIR, + ) + for path in paths_to_remove: + if path.exists() or path.is_symlink(): + print(f"Removing {path}...") + Tools.rmrf(path) + print("✓ Clean complete") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, add_help=False) + parser.add_argument("--build", action="store_true", help="Build the wheel") + parser.add_argument("--clean", action="store_true", help="Clean build folder") + parser.add_argument( + "--dev", + action="store_true", + help="Use editable pyodide-build from hardcoded path (Windows packing workaround)", + ) + args = parser.parse_args() + + if not args.build and not args.clean: + print(__doc__) + return + + if args.clean: + clean() + return + + start_time = time.time() + + WheelBuilder.extract_ifcopenshell_from_git(IFCOPENSHELL_DIR) + + print("Downloading and extracting _ifcopenshell_wrapper files...") + makefile = REPO_ROOT / "src" / "ifcopenshell-python" / "Makefile" + wheel_url = WheelBuilder.get_wheel_url(makefile) + so_file, py_file = WheelBuilder.download_and_extract_so(wheel_url, BUILD_DIR) + + Tools.create_symlink(IFCOPENSHELL_DIR / Path(so_file).name, so_file) + Tools.create_symlink(IFCOPENSHELL_DIR / Path(py_file).name, py_file) + + print("Installing pyodide-build...") + if args.dev: + Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)]) + else: + Tools.run(["uv", "pip", "install", "pyodide-build"]) + + print("Building with pyodide...") + # Use --no-isolation due to pyodide-build Windows support issues: + # symlink_unisolated_packages fails with missing `_sysconfigdata_$(CPYTHON_ABI_FLAGS)_emscripten_wasm32-emscripten.py`. + # Hardcode platform name since pyodide doesn't yet support overriding wheel tags on Windows. + # + # Use `LEGACY_PLATFORM` since pyodide 0.34.1 introduced new tag for wheels `pyemscripten`, + # which doesn't work with pyodide itself yet - https://github.com/pyodide/pyodide/issues/6177. + os.environ["USE_LEGACY_PLATFORM"] = "1" + Tools.run(["pyodide", "build", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"]) + + elapsed = time.time() - start_time + print(f"\n✓ Done! ({elapsed:.1f}s)") + + +if __name__ == "__main__": + main() diff --git a/pyodide/setup.py b/pyodide/setup.py index 9678de0ac3..474a0b45a4 100644 --- a/pyodide/setup.py +++ b/pyodide/setup.py @@ -2,12 +2,16 @@ # because `tool.setuptools.ext-modules` is still experimental in pyproject.toml # and we need it to get the wheel suffix right. import os +import sys from pathlib import Path import tomllib from setuptools import Extension, find_packages, setup +from setuptools.command.build_ext import build_ext -REPO_FOLDER = Path(__file__).parent +# Detect repo folder: if setup.py is in pyodide folder, go to parent +SETUP_DIR = Path(__file__).parent +REPO_FOLDER = SETUP_DIR.parent if SETUP_DIR.name == "pyodide" else SETUP_DIR def get_version() -> str: @@ -25,6 +29,39 @@ def get_dependencies() -> list[str]: return dependencies +class UnixBuildExt(build_ext): + """Customize ``build_ext`` to support packing on Windows.""" + + def finalize_options(self): + from distutils import sysconfig + + super().finalize_options() + if sys.platform == "win32": + self.compiler = "unix" + + # Configure sysconfig for Windows builds + # CCSHARED is the only variable that's not customizable with env vars. + # Basically avoiding this: + # File ".venv\Lib\site-packages\setuptools\_distutils\sysconfig.py", line 366, in customize_compiler + # compiler_so=cc_cmd + ' ' + ccshared, + # ~~~~~~~~~~~~~^~~~~~~~~~ + # TypeError: can only concatenate str (not "NoneType") to str + sysconfig.get_config_vars() # Initialize config cache + if sysconfig._config_vars.get("CCSHARED") is None: + sysconfig._config_vars["CCSHARED"] = "-fPIC" + # Override compiler type before it's instantiated + + # Set Emscripten compiler environment variables + os.environ["CC"] = "emcc" + os.environ["CXX"] = "em++" + os.environ["CFLAGS"] = "" + os.environ["CXXFLAGS"] = "" + os.environ["LDSHARED"] = "emcc -shared" + os.environ["AR"] = "emar" + os.environ["ARFLAGS"] = "rcs" + os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".cpython-313-wasm32-emscripten.so" + + setup( name="ifcopenshell", version=get_version(), @@ -44,4 +81,5 @@ setup( }, # Has to provide extension to get the correct wheel suffix. ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])], + cmdclass={"build_ext": UnixBuildExt}, ) From 7169dcd0537ac6e9336c2788ccd8f4d6ebf24967 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 17:29:17 +0500 Subject: [PATCH 12/25] Create ci-pyodide-wasm-release.yml --- .github/workflows/ci-pyodide-wasm-release.yml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/ci-pyodide-wasm-release.yml diff --git a/.github/workflows/ci-pyodide-wasm-release.yml b/.github/workflows/ci-pyodide-wasm-release.yml new file mode 100644 index 0000000000..eff7bc30d9 --- /dev/null +++ b/.github/workflows/ci-pyodide-wasm-release.yml @@ -0,0 +1,43 @@ +name: Release Pyodide WASM Wheel + +on: + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout IfcOpenShell + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Build wheel + working-directory: pyodide + run: uv run pack_wheel.py --build + + - name: Find wheel + id: wheel + run: | + WHEEL=$(ls pyodide/dist/ifcopenshell-*.whl) + echo "path=$WHEEL" >> $GITHUB_OUTPUT + echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT + + - name: Checkout wasm-wheels + uses: actions/checkout@v6 + with: + repository: IfcOpenShell/wasm-wheels + path: wasm-wheels + token: ${{ secrets.WASM_WHEELS_TOKEN }} + + - name: Commit and push wheel to wasm-wheels + run: | + WHEEL_NAME="${{ steps.wheel.outputs.name }}" + cp "${{ steps.wheel.outputs.path }}" "wasm-wheels/$WHEEL_NAME" + cd wasm-wheels + git config user.name "IfcOpenBot" + git config user.email "ifcopenbot@ifcopenshell.org" + git add "$WHEEL_NAME" + git commit -m "Add $WHEEL_NAME" + git push origin main From 51a338e4c87a46e47c31f02468f85b4f8a59ea75 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Sat, 21 Mar 2026 10:54:42 +0100 Subject: [PATCH 13/25] Suppress reportRedeclaration in Pyright config --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 289a6c6415..13169b7c0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ extend-exclude = ''' reportInvalidTypeForm = false disableBytesTypePromotions = true reportUnnecessaryTypeIgnoreComment = true +reportRedeclaration = false # Pylance doesn't respect gitignore, so we have to exclude files manually here # to avoid VS Code slowing down. # https://github.com/microsoft/pylance-release/issues/5169 From 8eb0060d4a1d7f970b69087e5ab8f5f24702c60e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 17:55:06 +0500 Subject: [PATCH 14/25] Get rid of pyright ignore reportRedeclaration noise Welp, it was helping to point out untyped props, but it is getting too noisy now. --- .../bonsai/bim/module/attribute/operator.py | 8 +- .../bonsai/bim/module/attribute/prop.py | 14 ++-- .../bonsai/bim/module/clash/operator.py | 8 +- src/bonsai/bonsai/bim/module/clash/prop.py | 8 +- src/bonsai/bonsai/bim/module/cost/operator.py | 2 +- .../bonsai/bim/module/debug/operator.py | 10 +-- .../bonsai/bim/module/drawing/operator.py | 18 ++-- src/bonsai/bonsai/bim/module/drawing/prop.py | 4 +- .../bonsai/bim/module/group/operator.py | 6 +- .../bonsai/bim/module/ifcgit/operator.py | 6 +- .../bonsai/bim/module/light/operator.py | 16 ++-- src/bonsai/bonsai/bim/module/misc/operator.py | 10 +-- src/bonsai/bonsai/bim/module/misc/prop.py | 42 +++++----- src/bonsai/bonsai/bim/module/model/product.py | 2 +- src/bonsai/bonsai/bim/module/model/profile.py | 2 +- src/bonsai/bonsai/bim/module/model/prop.py | 8 +- .../bonsai/bim/module/owner/operator.py | 54 ++++++------ .../bonsai/bim/module/project/operator.py | 82 +++++++++---------- src/bonsai/bonsai/bim/module/project/prop.py | 2 +- src/bonsai/bonsai/bim/module/pset/operator.py | 14 ++-- src/bonsai/bonsai/bim/module/pset/prop.py | 6 +- .../bonsai/bim/module/search/operator.py | 8 +- .../bonsai/bim/module/sequence/operator.py | 12 +-- src/bonsai/bonsai/bim/module/sequence/prop.py | 6 +- .../bonsai/bim/module/spatial/operator.py | 2 +- .../bonsai/bim/module/structural/operator.py | 6 +- .../bonsai/bim/module/system/operator.py | 2 +- src/bonsai/bonsai/bim/module/web/prop.py | 6 +- src/bonsai/bonsai/bim/operator.py | 12 +-- src/bonsai/bonsai/bim/prop.py | 4 +- src/bonsai/bonsai/bim/ui.py | 2 +- .../nodes/ifc/shape_builder/extrude.py | 2 +- 32 files changed, 192 insertions(+), 192 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/attribute/operator.py b/src/bonsai/bonsai/bim/module/attribute/operator.py index 13901c0f81..8069aa29ce 100644 --- a/src/bonsai/bonsai/bim/module/attribute/operator.py +++ b/src/bonsai/bonsai/bim/module/attribute/operator.py @@ -295,13 +295,13 @@ class ExplorerShowUIPopup(bpy.types.Operator): bl_description = "Show Explorer UI to select element as attribute value or edit it." bl_options = {"REGISTER", "UNDO"} - ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + ifc_class: bpy.props.StringProperty() """Element IFC class.""" - attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + attribute_name: bpy.props.StringProperty() """IFC class attribute name.""" - data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + data_path: bpy.props.StringProperty() """Full data path""" - preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) """IFC id to preselect in the popup.""" if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py index acff2424b7..425c875e37 100644 --- a/src/bonsai/bonsai/bim/module/attribute/prop.py +++ b/src/bonsai/bonsai/bim/module/attribute/prop.py @@ -41,7 +41,7 @@ class BIMAttributeProperties(PropertyGroup): class ExplorerEntity(PropertyGroup): - ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + ifc_definition_id: bpy.props.IntProperty() if TYPE_CHECKING: ifc_definition_id: int @@ -60,7 +60,7 @@ class BIMExplorerProperties(PropertyGroup): self.property_unset("editing_entity_id") self.entity_attributes.clear() - is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration] + is_loaded: BoolProperty( name="Toggle Explorer UI", update=update_is_loaded, ) @@ -76,15 +76,15 @@ class BIMExplorerProperties(PropertyGroup): def update_ifc_class(self, context: object) -> None: tool.Attribute.refresh_uilist_entities() - ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration] + ifc_class: EnumProperty( name="IFC Class To Search", items=get_ifc_class, update=update_ifc_class, ) - entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration] - active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration] - editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration] - entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration] + entities: CollectionProperty(type=ExplorerEntity) + active_entity_index: IntProperty() + editing_entity_id: IntProperty() + entity_attributes: CollectionProperty(type=Attribute) if TYPE_CHECKING: is_loaded: bool diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 3788130a12..fb1af71774 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -201,16 +201,16 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper): "ALT+click to run a quick clash without selecting a file to save." ) - filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration] + filter_glob: bpy.props.StringProperty( default="*.bcf;*.json", options={"HIDDEN"} ) - format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + format: bpy.props.EnumProperty( name="Format", items=[(i, i, "") for i in ("bcf", "json")] ) - filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration] + filepath: bpy.props.StringProperty( subtype="FILE_PATH", options={"SKIP_SAVE"} ) - quick_clash: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + quick_clash: bpy.props.BoolProperty( options={"SKIP_SAVE"}, ) diff --git a/src/bonsai/bonsai/bim/module/clash/prop.py b/src/bonsai/bonsai/bim/module/clash/prop.py index 8bcd71632b..1ef3403e64 100644 --- a/src/bonsai/bonsai/bim/module/clash/prop.py +++ b/src/bonsai/bonsai/bim/module/clash/prop.py @@ -37,12 +37,12 @@ from bonsai.bim.prop import BIMFilterGroup, StrProperty class ClashSource(PropertyGroup): - name: StringProperty( # pyright: ignore[reportRedeclaration] + name: StringProperty( name="File", description="Absolute filepath to existing .ifc file to use as a clash source.", ) - filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration] - mode: EnumProperty( # pyright: ignore[reportRedeclaration] + filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") + mode: EnumProperty( items=[ ("a", "All Elements", "All elements will be used for clashing"), ("i", "Include", "Only the selected elements are included for clashing"), @@ -62,7 +62,7 @@ class Clash(PropertyGroup): b_global_id: StringProperty(name="B") a_name: StringProperty(name="A Name") b_name: StringProperty(name="B Name") - clash_type: EnumProperty( # pyright: ignore[reportRedeclaration] + clash_type: EnumProperty( name="Clash Type", items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS), ) diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index b612100a1d..ac9d2d4bf1 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -87,7 +87,7 @@ class CopyCostSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Copy Cost Schedule" bl_description = "Create a duplicate of the provided cost schedule." bl_options = {"REGISTER", "UNDO"} - cost_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + cost_schedule: bpy.props.IntProperty() if TYPE_CHECKING: cost_schedule: int diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 9315386008..c88ea4a00e 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -260,14 +260,14 @@ class CreateAllShapes(bpy.types.Operator): ) bl_options = {"REGISTER"} - geometry_library: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + geometry_library: bpy.props.EnumProperty( name="Geometry Library", description="Geometry library to use for testing shape creation.", items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)], # By default use the same library as used for importing ifc project. default="hybrid-cgal-simple-opencascade", ) - custom_geometry_library: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration] + custom_geometry_library: bpy.props.StringProperty( name="Custom Geometry Library", description="Provide a custom geometry library name, will override the 'geometry library' property.", ) @@ -781,7 +781,7 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Purge Unused Objects" bl_options = {"REGISTER", "UNDO"} - object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + object_type: bpy.props.EnumProperty( name="Object Type", items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)), ) @@ -827,7 +827,7 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): ) bl_options = {"REGISTER", "UNDO"} - object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + object_type: bpy.props.EnumProperty( name="Object Type", items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)), ) @@ -1073,7 +1073,7 @@ class ChangeLogLevel(bpy.types.Operator): bl_options = {"REGISTER"} bl_description = "Change general log level across all Python code in Blender" - log_level: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + log_level: bpy.props.EnumProperty( name="Log Level", items=[(i, i, "") for i in get_args(LogLevelType)], default="WARNING", diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 45f67b0769..d33047378a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -246,17 +246,17 @@ class CreateDrawing(bpy.types.Operator): + "Add the CTRL modifier to optionally open drawings to view them as\n" + "they are created" ) - print_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + print_all: bpy.props.BoolProperty( name="Print All", default=False, options={"SKIP_SAVE"}, ) - open_viewer: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + open_viewer: bpy.props.BoolProperty( name="Open in Viewer", default=False, options={"SKIP_SAVE"}, ) - sync: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + sync: bpy.props.BoolProperty( name="Sync Before Creating Drawing", description="Could save some time if you're sure IFC and current Blender session are already in sync", default=True, @@ -2322,14 +2322,14 @@ class ActivateDrawingBase(tool.Ifc.Operator): + "SHIFT+CLICK to load a quick preview of the drawing view" ) - drawing: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - should_view_from_camera: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + drawing: bpy.props.IntProperty() + should_view_from_camera: bpy.props.BoolProperty( name="Should View From Camera", description="Move view to the activated drawing's camera position.", default=True, options={"SKIP_SAVE"}, ) - use_quick_preview: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + use_quick_preview: bpy.props.BoolProperty( name="Use Quick Preview", description="Just move the camera to the drawing view, without loading anything else.", default=False, @@ -3635,12 +3635,12 @@ class ToggleTargetView(bpy.types.Operator): bl_label = "Toggle Target View" bl_options = {"REGISTER", "UNDO"} - target_view: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] - toggle_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + target_view: bpy.props.StringProperty() + toggle_all: bpy.props.BoolProperty( default=False, options={"SKIP_SAVE"}, ) - option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + option: bpy.props.EnumProperty( items=[(i, i, "") for i in get_args(ToggleOption)] ) diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 57c182c02e..1647d44773 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -860,13 +860,13 @@ class BIMTextProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) literals: CollectionProperty(name="Literals", type=LiteralProps) newline_at: IntProperty(name="Newline At") - symbol: EnumProperty( # pyright: ignore[reportRedeclaration] + symbol: EnumProperty( name="Symbol", description="Symbol from symbols.svg to use for this text.", items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS], default="NO SYMBOL", ) - custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration] + custom_symbol: StringProperty( name="Custom Symbol", description="Non-default symbol to use for this text.", ) diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index adea9ee48c..f58ba72763 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -43,11 +43,11 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Toggle Group" bl_options = {"REGISTER", "UNDO"} - ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - group_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + ifc_definition_id: bpy.props.IntProperty() + group_type: bpy.props.EnumProperty( items=[(i, i, "") for i in get_args(tool.Group.GroupType)], ) - option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + option: bpy.props.EnumProperty( items=[(i, i, "") for i in get_args(tool.Group.ToggleOption)], ) diff --git a/src/bonsai/bonsai/bim/module/ifcgit/operator.py b/src/bonsai/bonsai/bim/module/ifcgit/operator.py index cc0f75577a..65bec6b252 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/operator.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/operator.py @@ -314,7 +314,7 @@ class SelectConflictEntity(bpy.types.Operator): bl_idname = "ifcgit.select_conflict_entity" bl_options = {"REGISTER"} - step_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + step_id: bpy.props.IntProperty() if TYPE_CHECKING: step_id: int @@ -515,7 +515,7 @@ class RunGitDiff(bpy.types.Operator): ) bl_options = set() - save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"}) if TYPE_CHECKING: save_to_temp: bool @@ -547,7 +547,7 @@ class RenameBranch(bpy.types.Operator): bl_idname = "ifcgit.rename_branch" bl_options = {"REGISTER"} - new_name: bpy.props.StringProperty(name="New name") # pyright: ignore[reportRedeclaration] + new_name: bpy.props.StringProperty(name="New name") if TYPE_CHECKING: new_name: str diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index 6ee37f2285..c3f377e9b9 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -272,21 +272,21 @@ class RadianceRender(bpy.types.Operator): + '''" map_u map_v 0 1 0.5 - + # This is a multiplier to colour balance the env map # In this case, it provides a rough ground luminance from 3k-5k env_map colorfunc env_colour 4 100 100 100 . 0 0 - + # .37 .57 1.5 is measured from a HDRI image # It is multiplied by a factor such that grey(r,g,b) = 1 skyfunc colorfunc sky_colour 4 .64 .99 2.6 . 0 0 - + void mixpict composite 7 env_colour sky_colour grey "''' + hdr_mask_path @@ -295,22 +295,22 @@ void mixpict composite + """" map_u map_v 0 2 0.5 1 - + composite glow env_map_glow 0 0 4 1 1 1 0 - + env_map_glow source sky 0 0 4 0 0 1 180 - + env_colour glow ground_glow 0 0 4 1 1 1 0 - + ground_glow source ground 0 0 @@ -566,7 +566,7 @@ class LightPickCoordinates(bpy.types.Operator): ) bl_options = {"REGISTER", "UNDO"} - use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) if TYPE_CHECKING: use_current_location: bool diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 33da9a05dc..31317f5b95 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -136,7 +136,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator): "Will unassign element from a type if type has a representation." ) bl_options = {"REGISTER", "UNDO"} - mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + mode: bpy.props.EnumProperty( default="BOOLEAN", items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)), ) @@ -359,7 +359,7 @@ class ConfirmQuickFavoriteOperator(bpy.types.Operator): bl_idname = "bim.confirm_quick_favorite_operator" bl_label = "Confirm Operator" bl_options = {"REGISTER", "UNDO"} - index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + index: bpy.props.IntProperty() if TYPE_CHECKING: index: int @@ -452,8 +452,8 @@ class MoveQuickFavoritesItem(bpy.types.Operator): bl_idname = "bim.move_quick_favorites_item" bl_label = "Move Quick Favorites Item" bl_options = {"REGISTER", "UNDO"} - index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - direction: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + index: bpy.props.IntProperty() + direction: bpy.props.EnumProperty( items=[("UP", "Up", ""), ("DOWN", "Down", "")] ) @@ -474,7 +474,7 @@ class RemoveQuickFavoritesItem(bpy.types.Operator): bl_idname = "bim.remove_quick_favorites_item" bl_label = "Remove Quick Favorites Item" bl_options = {"REGISTER", "UNDO"} - index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + index: bpy.props.IntProperty() if TYPE_CHECKING: index: int diff --git a/src/bonsai/bonsai/bim/module/misc/prop.py b/src/bonsai/bonsai/bim/module/misc/prop.py index ddeda73b88..74a82f06cd 100644 --- a/src/bonsai/bonsai/bim/module/misc/prop.py +++ b/src/bonsai/bonsai/bim/module/misc/prop.py @@ -36,9 +36,9 @@ QuickFavoriteValueType = Literal["float_value", "bool_value", "int_value", "stri class QuickFavoriteEnumItem(PropertyGroup): - name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration] - display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration] - description: StringProperty(name="Description", default="") # pyright: ignore[reportRedeclaration] + name: StringProperty(name="Name", default="") + display_name: StringProperty(name="Display Name", default="") + description: StringProperty(name="Description", default="") if TYPE_CHECKING: name: str @@ -51,19 +51,19 @@ def get_enum_items(self: "QuickFavoriteProperty", context: bpy.types.Context | N class QuickFavoriteProperty(PropertyGroup): - name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration] - display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration] - value_prop: EnumProperty( # pyright: ignore[reportRedeclaration] + name: StringProperty(name="Name", default="") + display_name: StringProperty(name="Display Name", default="") + value_prop: EnumProperty( name="Value Prop", items=tuple((v, v, "") for v in get_args(QuickFavoriteValueType)), ) - string_value: StringProperty(name="String Value", default="") # pyright: ignore[reportRedeclaration] - float_value: FloatProperty(name="Float Value", default=0.0) # pyright: ignore[reportRedeclaration] - int_value: IntProperty(name="Int Value", default=0) # pyright: ignore[reportRedeclaration] - bool_value: BoolProperty(name="Bool Value", default=False) # pyright: ignore[reportRedeclaration] - enum_value: EnumProperty(name="Enum Value", items=get_enum_items) # pyright: ignore[reportRedeclaration] - enum_items: CollectionProperty(type=QuickFavoriteEnumItem) # pyright: ignore[reportRedeclaration] - is_active: BoolProperty( # pyright: ignore[reportRedeclaration] + string_value: StringProperty(name="String Value", default="") + float_value: FloatProperty(name="Float Value", default=0.0) + int_value: IntProperty(name="Int Value", default=0) + bool_value: BoolProperty(name="Bool Value", default=False) + enum_value: EnumProperty(name="Enum Value", items=get_enum_items) + enum_items: CollectionProperty(type=QuickFavoriteEnumItem) + is_active: BoolProperty( name="Is Active", description="Only active properties will be added to the operator when invoked from Quick Favorites", default=False, @@ -100,20 +100,20 @@ def get_operator_suggestions(self: "QuickFavoritesItem", context: bpy.types.Cont class QuickFavoritesItem(PropertyGroup): - is_expanded: BoolProperty(name="Is Expanded", default=False) # pyright: ignore[reportRedeclaration] - search: StringProperty( # pyright: ignore[reportRedeclaration] + is_expanded: BoolProperty(name="Is Expanded", default=False) + search: StringProperty( name="Search", default="", search=get_operator_suggestions, # Resetting `search_options`, allowing users only to use suggestions. search_options=set(), ) - properties: CollectionProperty(type=QuickFavoriteProperty) # pyright: ignore[reportRedeclaration] - operator_id: StringProperty( # pyright: ignore[reportRedeclaration] + properties: CollectionProperty(type=QuickFavoriteProperty) + operator_id: StringProperty( name="Operator ID", default="", ) - label: StringProperty( # pyright: ignore[reportRedeclaration] + label: StringProperty( name="Label", description="Label that will be used in Quick Favorites for this operator", default="", @@ -139,15 +139,15 @@ class QuickFavoritesItem(PropertyGroup): class BIMMiscProperties(PropertyGroup): - total_storeys: IntProperty( # pyright: ignore[reportRedeclaration] + total_storeys: IntProperty( name="Total Storeys", description="Number of storeys above object's storey to take into account for resizing", default=1, ) - override_colour: FloatVectorProperty( # pyright: ignore[reportRedeclaration] + override_colour: FloatVectorProperty( name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4 ) - quick_favorites: CollectionProperty(type=QuickFavoritesItem) # pyright: ignore[reportRedeclaration] + quick_favorites: CollectionProperty(type=QuickFavoritesItem) if TYPE_CHECKING: total_storeys: int diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 4cf4e00172..75f5441827 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -545,7 +545,7 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.change_type_page" bl_label = "Change Type Page" bl_options = {"REGISTER"} - page: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + page: bpy.props.IntProperty() if TYPE_CHECKING: page: int diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 1369ad3cb6..e5c0991e8e 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -271,7 +271,7 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.extend_profile" bl_label = "Extend Profile" bl_options = {"REGISTER", "UNDO"} - join_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + join_type: bpy.props.EnumProperty( items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")], default="-", ) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index c4956056aa..ff6ea96130 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -1729,20 +1729,20 @@ def poll_sverchok_nodes(self: "BIMExternalParametricGeometryProperties", node_tr class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup): - is_editing: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + is_editing: bpy.props.BoolProperty( name="Is Editing Paramteric Geometry", description="Toggle editing parametric geometry.", default=False, update=update_is_editing, ) - geometry_source: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + geometry_source: bpy.props.EnumProperty( name="Geometry Source", items=[ ("GEONODES", "Geometry Nodes", ""), ("IFCSVERCHOK", "IFC Sverchok", ""), ], ) - geo_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration] + geo_nodes: bpy.props.PointerProperty( name="Geometry Nodes", description="Geometry nodes tree to use as a source for representation.", type=bpy.types.GeometryNodeTree, @@ -1750,7 +1750,7 @@ class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup): poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"), ) - sverchok_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration] + sverchok_nodes: bpy.props.PointerProperty( name="Sverchok Nodes", description="Sverchok node tree to use as a source for representation.", type=bpy.types.NodeTree, diff --git a/src/bonsai/bonsai/bim/module/owner/operator.py b/src/bonsai/bonsai/bim/module/owner/operator.py index 2470fd8227..b934337b64 100644 --- a/src/bonsai/bonsai/bim/module/owner/operator.py +++ b/src/bonsai/bonsai/bim/module/owner/operator.py @@ -33,7 +33,7 @@ class EnableEditingPerson(bpy.types.Operator): bl_idname = "bim.enable_editing_person" bl_label = "Enable Editing Person" bl_options = {"REGISTER", "UNDO"} - person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + person: bpy.props.IntProperty() if TYPE_CHECKING: person: int @@ -75,7 +75,7 @@ class RemovePerson(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_person" bl_label = "Remove Person" bl_options = {"REGISTER", "UNDO"} - person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + person: bpy.props.IntProperty() if TYPE_CHECKING: person: int @@ -88,7 +88,7 @@ class AddPersonAttribute(bpy.types.Operator): bl_idname = "bim.add_person_attribute" bl_label = "Add Person Attribute" bl_options = {"REGISTER", "UNDO"} - name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name: bpy.props.EnumProperty( items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)), ) @@ -104,10 +104,10 @@ class RemovePersonAttribute(bpy.types.Operator): bl_idname = "bim.remove_person_attribute" bl_label = "Remove Person Attribute" bl_options = {"REGISTER", "UNDO"} - name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name: bpy.props.EnumProperty( items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)), ) - id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + id: bpy.props.IntProperty() if TYPE_CHECKING: name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride] @@ -122,7 +122,7 @@ class EnableEditingRole(bpy.types.Operator): bl_idname = "bim.enable_editing_role" bl_label = "Enable Editing Role" bl_options = {"REGISTER", "UNDO"} - role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + role: bpy.props.IntProperty() if TYPE_CHECKING: role: int @@ -146,7 +146,7 @@ class AddRole(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_role" bl_label = "Add Role" bl_options = {"REGISTER", "UNDO"} - parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + parent: bpy.props.IntProperty() if TYPE_CHECKING: parent: int @@ -168,7 +168,7 @@ class RemoveRole(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_role" bl_label = "Remove Role" bl_options = {"REGISTER", "UNDO"} - role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + role: bpy.props.IntProperty() if TYPE_CHECKING: role: int @@ -181,8 +181,8 @@ class AddAddress(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_address" bl_label = "Add Address" bl_options = {"REGISTER", "UNDO"} - parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - ifc_class: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + parent: bpy.props.IntProperty() + ifc_class: bpy.props.EnumProperty( items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)), ) @@ -198,7 +198,7 @@ class AddAddressAttribute(bpy.types.Operator): bl_idname = "bim.add_address_attribute" bl_label = "Add Address Attribute" bl_options = {"REGISTER", "UNDO"} - name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name: bpy.props.EnumProperty( items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)), ) @@ -214,10 +214,10 @@ class RemoveAddressAttribute(bpy.types.Operator): bl_idname = "bim.remove_address_attribute" bl_label = "Remove Address Attribute" bl_options = {"REGISTER", "UNDO"} - name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name: bpy.props.EnumProperty( items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)), ) - id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + id: bpy.props.IntProperty() if TYPE_CHECKING: name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride] @@ -232,7 +232,7 @@ class EnableEditingAddress(bpy.types.Operator): bl_idname = "bim.enable_editing_address" bl_label = "Enable Editing Address" bl_options = {"REGISTER", "UNDO"} - address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + address: bpy.props.IntProperty() if TYPE_CHECKING: address: int @@ -265,7 +265,7 @@ class RemoveAddress(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_address" bl_label = "Remove Address" bl_options = {"REGISTER", "UNDO"} - address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + address: bpy.props.IntProperty() if TYPE_CHECKING: address: int @@ -278,7 +278,7 @@ class EnableEditingOrganisation(bpy.types.Operator): bl_idname = "bim.enable_editing_organisation" bl_label = "Enable Editing Organisation" bl_options = {"REGISTER", "UNDO"} - organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + organisation: bpy.props.IntProperty() if TYPE_CHECKING: organisation: int @@ -320,7 +320,7 @@ class RemoveOrganisation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_organisation" bl_label = "Remove Organisation" bl_options = {"REGISTER", "UNDO"} - organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + organisation: bpy.props.IntProperty() if TYPE_CHECKING: organisation: int @@ -333,8 +333,8 @@ class AddPersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_person_and_organisation" bl_label = "Add Person And Organisation" bl_options = {"REGISTER", "UNDO"} - person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + person: bpy.props.IntProperty() + organisation: bpy.props.IntProperty() if TYPE_CHECKING: person: int @@ -350,7 +350,7 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_person_and_organisation" bl_label = "Remove Person And Organisation" bl_options = {"REGISTER", "UNDO"} - person_and_organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + person_and_organisation: bpy.props.IntProperty() if TYPE_CHECKING: person_and_organisation: int @@ -365,7 +365,7 @@ class SetUser(bpy.types.Operator): bl_idname = "bim.set_user" bl_label = "Set User" bl_options = {"REGISTER", "UNDO"} - user: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + user: bpy.props.IntProperty() if TYPE_CHECKING: user: int @@ -401,7 +401,7 @@ class EnableEditingActor(bpy.types.Operator): bl_idname = "bim.enable_editing_actor" bl_label = "Enable Editing Actor" bl_options = {"REGISTER", "UNDO"} - actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + actor: bpy.props.IntProperty() if TYPE_CHECKING: actor: int @@ -434,7 +434,7 @@ class RemoveActor(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_actor" bl_label = "Remove Actor" bl_options = {"REGISTER", "UNDO"} - actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + actor: bpy.props.IntProperty() if TYPE_CHECKING: actor: int @@ -447,7 +447,7 @@ class AssignActor(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_actor" bl_label = "Assign Actor" bl_options = {"REGISTER", "UNDO"} - actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + actor: bpy.props.IntProperty() if TYPE_CHECKING: actor: int @@ -462,7 +462,7 @@ class UnassignActor(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unassign_actor" bl_label = "Unassign Actor" bl_options = {"REGISTER", "UNDO"} - actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + actor: bpy.props.IntProperty() if TYPE_CHECKING: actor: int @@ -481,7 +481,7 @@ class RemoveApplication(bpy.types.Operator, tool.Ifc.Operator): "Remove provided IfcApplication." "\n\nFor safety will only work on applications without inverses (they are typically marked as '(unused)'." ) - application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + application_id: bpy.props.IntProperty() if TYPE_CHECKING: application_id: int @@ -525,7 +525,7 @@ class EnableEditingApplication(bpy.types.Operator): bl_idname = "bim.enable_editing_application" bl_label = "Enable Editing Application" bl_options = {"REGISTER", "UNDO"} - application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + application_id: bpy.props.IntProperty() if TYPE_CHECKING: application_id: int diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index e24f6f41d8..9ad9c082c8 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -86,7 +86,7 @@ class NewProject(bpy.types.Operator): bl_label = "New Project" bl_options = {"REGISTER", "UNDO"} bl_description = "Start a new IFC project in a fresh session" - preset: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + preset: bpy.props.EnumProperty( items=[(i, i, "") for i in get_args(PresetType)] ) @@ -180,11 +180,11 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper): ) filter_glob: bpy.props.StringProperty( default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"} - ) # pyright: ignore[reportRedeclaration] - append_all: bpy.props.BoolProperty(default=False) # pyright: ignore[reportRedeclaration] + ) + append_all: bpy.props.BoolProperty(default=False) use_relative_path: bpy.props.BoolProperty( name="Use Relative Path", default=False - ) # pyright: ignore[reportRedeclaration] + ) if TYPE_CHECKING: filter_glob: str @@ -568,7 +568,7 @@ class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.append_library_element_by_query" bl_label = "Append Library Element By Query" - query: bpy.props.StringProperty(name="Query") # pyright: ignore[reportRedeclaration] + query: bpy.props.StringProperty(name="Query") if TYPE_CHECKING: query: str @@ -600,11 +600,11 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): "Append element to the current project.\n\n" "ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)" ) - definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - prop_index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + definition: bpy.props.IntProperty() + prop_index: bpy.props.IntProperty() assume_unique_by_name: bpy.props.BoolProperty( name="Assume Unique By Name", default=True, options={"SKIP_SAVE"} - ) # pyright: ignore[reportRedeclaration] + ) if TYPE_CHECKING: definition: int @@ -961,26 +961,26 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_description = "Load an existing IFC project" filepath: bpy.props.StringProperty( subtype="FILE_PATH", options={"SKIP_SAVE"} - ) # pyright: ignore[reportRedeclaration] + ) filter_glob: bpy.props.StringProperty( default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"} - ) # pyright: ignore[reportRedeclaration] - is_advanced: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + ) + is_advanced: bpy.props.BoolProperty( name="Enable Advanced Mode", description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings", default=False, ) - use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + use_relative_path: bpy.props.BoolProperty( name="Use Relative Path", description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved", default=False, ) - should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + should_start_fresh_session: bpy.props.BoolProperty( name="Should Start Fresh Session", description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option", default=True, ) - import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + import_without_ifc_data: bpy.props.BoolProperty( name="Import Without IFC Data", description=( "Import IFC objects as Blender objects without any IFC metadata and authoring capabilities." @@ -990,7 +990,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): ) use_detailed_tooltip: bpy.props.BoolProperty( default=False, options={"HIDDEN"} - ) # pyright: ignore[reportRedeclaration] + ) filename_ext = ".ifc" if TYPE_CHECKING: @@ -1300,7 +1300,7 @@ class ToggleFilterCategories(bpy.types.Operator): bl_idname = "bim.toggle_filter_categories" bl_label = "Toggle Filter Categories" bl_options = {"REGISTER", "UNDO"} - should_select: bpy.props.BoolProperty(name="Should Select", default=True) # pyright: ignore[reportRedeclaration] + should_select: bpy.props.BoolProperty(name="Should Select", default=True) if TYPE_CHECKING: should_select: bool @@ -1327,7 +1327,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator): default=False, ) use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) - query: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration] + query: bpy.props.StringProperty( name="Query", description=( "Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n" @@ -1404,7 +1404,7 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Remove the selected file from the link list" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") if TYPE_CHECKING: link_index: int @@ -1428,7 +1428,7 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Unload the selected linked file" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") if TYPE_CHECKING: link_index: int @@ -1454,9 +1454,9 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Load the selected file" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] - use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration] - query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") + use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) + query: bpy.props.StringProperty() if TYPE_CHECKING: link_index: int @@ -1631,7 +1631,7 @@ class ReloadLink(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Reload the selected file" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") if TYPE_CHECKING: link_index: int @@ -1647,7 +1647,7 @@ class ToggleLinkSelectability(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle selectability" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") if TYPE_CHECKING: link_index: int @@ -1679,8 +1679,8 @@ class ToggleLinkVisibility(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle visibility between SOLID and WIREFRAME" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] - mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") + mode: bpy.props.EnumProperty( name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")), ) @@ -1821,7 +1821,7 @@ class SelectLinkHandle(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Select link empty object handle" - link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + link_index: bpy.props.IntProperty(name="Link Index") if TYPE_CHECKING: link_index: int @@ -1843,7 +1843,7 @@ class SelectLinkedModelElement(bpy.types.Operator): bl_options = {"REGISTER"} bl_description = "Select an element in the currently selected linked model by providing GlobalId." - guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration] + guid: bpy.props.StringProperty(name="GlobalId") if TYPE_CHECKING: guid: str @@ -1884,19 +1884,19 @@ class ExportIFC(bpy.types.Operator, ExportHelper): supported_filexts = (".ifc", ".ifczip", ".ifcjson") filter_glob: bpy.props.StringProperty( default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"} - ) # pyright: ignore[reportRedeclaration] + ) json_version: bpy.props.EnumProperty( items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version" - ) # pyright: ignore[reportRedeclaration] + ) json_compact: bpy.props.BoolProperty( name="Export Compact IFCJSON", default=False - ) # pyright: ignore[reportRedeclaration] + ) should_save_as: bpy.props.BoolProperty( name="Should Save As", default=False, options={"HIDDEN"} - ) # pyright: ignore[reportRedeclaration] + ) use_relative_path: bpy.props.BoolProperty( name="Use Relative Path", default=False - ) # pyright: ignore[reportRedeclaration] + ) if TYPE_CHECKING: filter_glob: str @@ -2053,7 +2053,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper): bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file." bl_options = {"REGISTER", "UNDO"} - query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + query: bpy.props.StringProperty() """See ``bim.link_ifc``.""" if TYPE_CHECKING: @@ -2443,8 +2443,8 @@ class HideQueriedLinkedElement(bpy.types.Operator): ) bl_options = {"REGISTER", "UNDO"} - unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] - hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) + hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) if TYPE_CHECKING: unhide_all: bool @@ -2920,10 +2920,10 @@ class IFCFileHandlerOperator(bpy.types.Operator): directory: bpy.props.StringProperty( subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"} - ) # pyright: ignore[reportRedeclaration] + ) files: bpy.props.CollectionProperty( type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"} - ) # pyright: ignore[reportRedeclaration] + ) if TYPE_CHECKING: directory: str @@ -2978,7 +2978,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): bl_label = "Measure Tool" bl_options = {"REGISTER", "UNDO"} - measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + measure_type: bpy.props.StringProperty() if TYPE_CHECKING: measure_type: str @@ -3077,7 +3077,7 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): bl_label = "Measure Face Area Tool" bl_options = {"REGISTER", "UNDO"} - measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + measure_type: bpy.props.StringProperty() if TYPE_CHECKING: measure_type: str @@ -3379,7 +3379,7 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator): bl_idname = "bim.load_blend_metadata_and_ifc" bl_label = "Load Blend Metadata and IFC" bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(name="IFC File Path", default="") # pyright: ignore[reportRedeclaration] + filepath: bpy.props.StringProperty(name="IFC File Path", default="") if TYPE_CHECKING: filepath: str diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 5ba0cf6f28..1408d2f786 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -345,7 +345,7 @@ class BIMProjectProperties(PropertyGroup): ), default=False, ) - should_cache: BoolProperty( # pyright: ignore[reportRedeclaration] + should_cache: BoolProperty( name="Cache", description=( "Cache loaded geometry to .h5 file in your cache directory (see in preferences) " diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index d7755cf80a..ea1d39fb96 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -240,7 +240,7 @@ class CopyPropertyToSelection(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Copy Property To Selection" bl_options = {"REGISTER", "UNDO"} - name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + name: bpy.props.StringProperty() if TYPE_CHECKING: name: str @@ -280,10 +280,10 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator): bl_label = "Add Property to Edit" bl_idname = "bim.add_property_to_edit" bl_options = {"REGISTER", "UNDO"} - option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + option: bpy.props.EnumProperty( items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES], ) - index: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration] + index: bpy.props.IntProperty(default=-1) if TYPE_CHECKING: option: tool.Pset.BulkOperationType @@ -307,9 +307,9 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator): bl_label = "Remove Property from Editing" bl_idname = "bim.remove_property_to_edit" bl_options = {"REGISTER", "UNDO"} - index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] - index2: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration] - option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + index: bpy.props.IntProperty() + index2: bpy.props.IntProperty(default=-1) + option: bpy.props.EnumProperty( items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES], ) @@ -336,7 +336,7 @@ class BIM_OT_bulk_edit_clear_list(bpy.types.Operator): bl_label = "Clear List of Properties" bl_idname = "bim.pset_bulk_edit_clear_list" bl_options = {"REGISTER", "UNDO"} - option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + option: bpy.props.EnumProperty( items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES], ) diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 1777fa0f94..786e6a62a2 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -368,9 +368,9 @@ class GlobalPsetProperties(PropertyGroup): qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"}) # Bulk operations. - psets_to_delete: CollectionProperty(type=DeletePsetEntry) # pyright: ignore[reportRedeclaration] - psets_to_rename: CollectionProperty(type=RenamePropertyEntry) # pyright: ignore[reportRedeclaration] - psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) # pyright: ignore[reportRedeclaration] + psets_to_delete: CollectionProperty(type=DeletePsetEntry) + psets_to_rename: CollectionProperty(type=RenamePropertyEntry) + psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) if TYPE_CHECKING: pset_filter: str diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index d5a6b9b1e6..f55dcf31b7 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -799,7 +799,7 @@ class SelectQueryElements(Operator): bl_description = "Select elements matching an provided selector query" bl_options = {"REGISTER", "UNDO"} - query: StringProperty(name="Query") # pyright: ignore[reportRedeclaration] + query: StringProperty(name="Query") if TYPE_CHECKING: query: str @@ -829,12 +829,12 @@ class SaveSearch(Operator, tool.Ifc.Operator): # Extra item so it will be easy to select current text. return [text] + SaveSearch.name_search_items - name: StringProperty( # pyright: ignore[reportRedeclaration] + name: StringProperty( name="Name", search=get_name_search_items, search_options={"SORT"}, ) - module: StringProperty() # pyright: ignore[reportRedeclaration] + module: StringProperty() def update_use_all_ifcgroups(self, context: object = None) -> None: ifc_file = tool.Ifc.get() @@ -845,7 +845,7 @@ class SaveSearch(Operator, tool.Ifc.Operator): } self.name_search_items[:] = natsorted(groups) - use_all_ifcgroups: BoolProperty( # pyright: ignore[reportRedeclaration] + use_all_ifcgroups: BoolProperty( name="Use Any IfcGroup", description=( "By default we're targeting only IfcGroups with SEARCH ObjectType " diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 2b4c317cd8..fb97d7152a 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -106,7 +106,7 @@ class ActivateStatusFilters(bpy.types.Operator): bl_description = "Filter and display objects based on currently selected IFC statuses" bl_options = {"REGISTER", "UNDO"} - only_if_enabled: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + only_if_enabled: bpy.props.BoolProperty( name="Only If Filters are Enabled", description="Activate status filters only in case if they were enabled from the UI before.", default=False, @@ -137,7 +137,7 @@ class SelectStatusFilter(bpy.types.Operator): bl_description = "Select elements with currently selected status" bl_options = {"REGISTER", "UNDO"} - status: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + status: bpy.props.StringProperty() if TYPE_CHECKING: status: tool.Sequence.ElementStatusUI @@ -156,7 +156,7 @@ class AssignStatus(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Assign status to the selected elements.\n\nAlt+CLICK to unassign the status." bl_options = {"REGISTER", "UNDO"} - should_override_previous_status: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + should_override_previous_status: bpy.props.BoolProperty( name="Override Previous Status", description=( "Whether assigning new status should override previous one.\n\n" @@ -165,8 +165,8 @@ class AssignStatus(bpy.types.Operator, tool.Ifc.Operator): ), default=True, ) - status: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] - should_unassign_status: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + status: bpy.props.StringProperty() + should_unassign_status: bpy.props.BoolProperty( options={"SKIP_SAVE"}, ) @@ -415,7 +415,7 @@ class CopyWorkSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Copy Work Schedule" bl_description = "Create a duplicate of the provided work schedule." bl_options = {"REGISTER", "UNDO"} - work_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + work_schedule: bpy.props.IntProperty() if TYPE_CHECKING: work_schedule: int diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index abb1aa9f0c..97f274c421 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -412,7 +412,7 @@ WorkPlanEditingType = Literal["-", "ATTRIBUTES", "SCHEDULES", "WORK_SCHEDULE", " class BIMWorkPlanProperties(PropertyGroup): work_plan_attributes: CollectionProperty(name="Work Plan Attributes", type=Attribute) - editing_type: EnumProperty( # pyright: ignore[reportRedeclaration] + editing_type: EnumProperty( items=[(i, i, "") for i in get_args(WorkPlanEditingType)], ) work_plans: CollectionProperty(name="Work Plans", type=WorkPlan) @@ -430,8 +430,8 @@ class BIMWorkPlanProperties(PropertyGroup): class IFCStatus(PropertyGroup): - name: StringProperty() # pyright: ignore[reportRedeclaration] - is_visible: BoolProperty( # pyright: ignore[reportRedeclaration] + name: StringProperty() + is_visible: BoolProperty( name="Is Visible", default=True, update=lambda x, y: (None, bpy.ops.bim.activate_status_filters())[0] ) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index f1eb4ec4ee..cd6bc6cab2 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -220,7 +220,7 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Copy to Container" bl_options = {"REGISTER", "UNDO"} - container: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + container: bpy.props.IntProperty() if TYPE_CHECKING: container: int diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index 20b32c7a0c..825f52e954 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -167,7 +167,7 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): bl_idname = "bim.enable_editing_structural_boundary_condition" bl_label = "Enable Editing Structural Boundary Condition" bl_options = {"REGISTER", "UNDO"} - boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + boundary_condition: bpy.props.IntProperty() if TYPE_CHECKING: boundary_condition: int @@ -186,7 +186,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_structural_boundary_condition" bl_label = "Edit Structural Boundary Condition" bl_options = {"REGISTER", "UNDO"} - connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + connection: bpy.props.IntProperty() if TYPE_CHECKING: connection: int @@ -917,7 +917,7 @@ class EnableEditingBoundaryCondition(bpy.types.Operator): bl_idname = "bim.enable_editing_boundary_condition" bl_label = "Enable Editing Boundary Condition" bl_options = {"REGISTER", "UNDO"} - boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + boundary_condition: bpy.props.IntProperty() if TYPE_CHECKING: boundary_condition: int diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index e1f6f5e9e4..5fe47564c0 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -54,7 +54,7 @@ class AddSystem(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Add System" bl_options = {"REGISTER", "UNDO"} - parent_system_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + parent_system_id: bpy.props.IntProperty() if TYPE_CHECKING: parent_system_id: int diff --git a/src/bonsai/bonsai/bim/module/web/prop.py b/src/bonsai/bonsai/bim/module/web/prop.py index 42bd272578..e9e5a33185 100644 --- a/src/bonsai/bonsai/bim/module/web/prop.py +++ b/src/bonsai/bonsai/bim/module/web/prop.py @@ -26,16 +26,16 @@ from bpy.types import PropertyGroup class WebProperties(PropertyGroup): - webserver_port: IntProperty( # pyright: ignore[reportRedeclaration] + webserver_port: IntProperty( name="Webserver Port", min=0, max=65535, ) - is_running: BoolProperty( # pyright: ignore[reportRedeclaration] + is_running: BoolProperty( name="Webserver Running Status", default=False, ) - is_connected: BoolProperty( # pyright: ignore[reportRedeclaration] + is_connected: BoolProperty( name="Connection Status", default=False, ) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 3216478132..f1ab24c7b0 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -159,9 +159,9 @@ class SelectURIAttribute(bpy.types.Operator, ImportHelper): bl_label = "Select URI Attribute" bl_options = {"REGISTER", "UNDO"} bl_description = "Select a local file" - attribute_data_path: bpy.props.StringProperty(name="Data Path") # pyright: ignore[reportRedeclaration] + attribute_data_path: bpy.props.StringProperty(name="Data Path") """Full data path to `Attribute`/string property.""" - use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] + use_relative_path: bpy.props.BoolProperty( name="Use Relative Path", default=False, ) @@ -601,7 +601,7 @@ class CreateMacBonsaiApp(bpy.types.Operator): "ALT+click to uninstall Bonsai app if it was installed previously." ) - uninstall: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + uninstall: bpy.props.BoolProperty(options={"SKIP_SAVE"}) if TYPE_CHECKING: uninstall: bool @@ -1667,7 +1667,7 @@ class BIM_OT_attribute_add_subitem(bpy.types.Operator): bl_description = "Add subitem to the current attribute" bl_options = {"REGISTER", "UNDO"} - data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + data_path: bpy.props.StringProperty() """Full data path.""" if TYPE_CHECKING: @@ -1691,9 +1691,9 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator): bl_description = "Add subitem to the current attribute" bl_options = {"REGISTER", "UNDO"} - data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration] + data_path: bpy.props.StringProperty() """Full data path.""" - index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + index: bpy.props.IntProperty() if TYPE_CHECKING: data_path: str diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index e5d495c62a..e10243d266 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -333,7 +333,7 @@ class Attribute(PropertyGroup): filter_glob: StringProperty() is_null: BoolProperty(name="Is Null", update=update_is_null) is_selected: BoolProperty(name="Is Selected", default=False) - subitems_values: CollectionProperty(type=StrProperty) # pyright: ignore[reportRedeclaration] + subitems_values: CollectionProperty(type=StrProperty) # Attribute parameters. is_optional: BoolProperty(name="Is Optional") @@ -342,7 +342,7 @@ class Attribute(PropertyGroup): value_max: FloatProperty(description="This is used to validate int_value and float_value") value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound") special_type: StringProperty(name="Special Value Type", default="") - use_explorer_ui: BoolProperty() # pyright: ignore[reportRedeclaration] + use_explorer_ui: BoolProperty() metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute") update: StringProperty(name="Update", description="Custom update function to be executed") diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 09eee51401..97980d92dd 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -665,7 +665,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False ) should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False) - should_always_cache: BoolProperty( # pyright: ignore[reportRedeclaration] + should_always_cache: BoolProperty( name="Always Cache Geometry", description="Whether to always cache geometry regardless of 'Cache' setting during Advanced Project Load.", ) diff --git a/src/ifcsverchok/nodes/ifc/shape_builder/extrude.py b/src/ifcsverchok/nodes/ifc/shape_builder/extrude.py index 1725f37aa2..350f4ac71f 100644 --- a/src/ifcsverchok/nodes/ifc/shape_builder/extrude.py +++ b/src/ifcsverchok/nodes/ifc/shape_builder/extrude.py @@ -32,7 +32,7 @@ class SvIfcSbExtrude(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.Sv bl_idname = "SvIfcSbExtrude" bl_label = "IFC Extrude" - extrude_axis: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + extrude_axis: bpy.props.EnumProperty( default="Z", items=[ ("X", "X", "Interpret curve as in XY plane and extrude along X+."), From 80a9df8f52da44ecb3bf42c49d2e902aa4d67f10 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 18:01:16 +0500 Subject: [PATCH 15/25] Ignore pyright warnings for bpy stubs See https://github.com/nutti/fake-bpy-module/discussions/440 --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 13169b7c0b..235d3f6e6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,8 @@ reportInvalidTypeForm = false disableBytesTypePromotions = true reportUnnecessaryTypeIgnoreComment = true reportRedeclaration = false +# Ignore warnings from bpy stubs missing actual source files. +reportMissingModuleSource = false # Pylance doesn't respect gitignore, so we have to exclude files manually here # to avoid VS Code slowing down. # https://github.com/microsoft/pylance-release/issues/5169 From 5a3160eb626f82761d58ac8d5f756f6ce4787bce Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 18:02:44 +0500 Subject: [PATCH 16/25] black . --- .../bonsai/bim/module/clash/operator.py | 12 +--- .../bonsai/bim/module/drawing/operator.py | 4 +- src/bonsai/bonsai/bim/module/misc/operator.py | 4 +- .../bonsai/bim/module/project/operator.py | 56 +++++-------------- 4 files changed, 19 insertions(+), 57 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index fb1af71774..ae5f622bbd 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -201,15 +201,9 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper): "ALT+click to run a quick clash without selecting a file to save." ) - filter_glob: bpy.props.StringProperty( - default="*.bcf;*.json", options={"HIDDEN"} - ) - format: bpy.props.EnumProperty( - name="Format", items=[(i, i, "") for i in ("bcf", "json")] - ) - filepath: bpy.props.StringProperty( - subtype="FILE_PATH", options={"SKIP_SAVE"} - ) + filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"}) + format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")]) + filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) quick_clash: bpy.props.BoolProperty( options={"SKIP_SAVE"}, ) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index d33047378a..d6594364bf 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3640,9 +3640,7 @@ class ToggleTargetView(bpy.types.Operator): default=False, options={"SKIP_SAVE"}, ) - option: bpy.props.EnumProperty( - items=[(i, i, "") for i in get_args(ToggleOption)] - ) + option: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(ToggleOption)]) if TYPE_CHECKING: target_view: str diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 31317f5b95..203bc9dd6c 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -453,9 +453,7 @@ class MoveQuickFavoritesItem(bpy.types.Operator): bl_label = "Move Quick Favorites Item" bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() - direction: bpy.props.EnumProperty( - items=[("UP", "Up", ""), ("DOWN", "Down", "")] - ) + direction: bpy.props.EnumProperty(items=[("UP", "Up", ""), ("DOWN", "Down", "")]) if TYPE_CHECKING: index: int diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9ad9c082c8..d38c4f3b68 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -86,9 +86,7 @@ class NewProject(bpy.types.Operator): bl_label = "New Project" bl_options = {"REGISTER", "UNDO"} bl_description = "Start a new IFC project in a fresh session" - preset: bpy.props.EnumProperty( - items=[(i, i, "") for i in get_args(PresetType)] - ) + preset: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(PresetType)]) if TYPE_CHECKING: preset: PresetType @@ -178,13 +176,9 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_description = ( "Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file." ) - filter_glob: bpy.props.StringProperty( - default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"} - ) + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) append_all: bpy.props.BoolProperty(default=False) - use_relative_path: bpy.props.BoolProperty( - name="Use Relative Path", default=False - ) + use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) if TYPE_CHECKING: filter_glob: str @@ -602,9 +596,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ) definition: bpy.props.IntProperty() prop_index: bpy.props.IntProperty() - assume_unique_by_name: bpy.props.BoolProperty( - name="Assume Unique By Name", default=True, options={"SKIP_SAVE"} - ) + assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}) if TYPE_CHECKING: definition: int @@ -959,12 +951,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_label = "Load Project" bl_options = {"REGISTER", "UNDO"} bl_description = "Load an existing IFC project" - filepath: bpy.props.StringProperty( - subtype="FILE_PATH", options={"SKIP_SAVE"} - ) - filter_glob: bpy.props.StringProperty( - default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"} - ) + filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}) is_advanced: bpy.props.BoolProperty( name="Enable Advanced Mode", description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings", @@ -988,9 +976,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): ), default=False, ) - use_detailed_tooltip: bpy.props.BoolProperty( - default=False, options={"HIDDEN"} - ) + use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) filename_ext = ".ifc" if TYPE_CHECKING: @@ -1882,21 +1868,11 @@ class ExportIFC(bpy.types.Operator, ExportHelper): bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" supported_filexts = (".ifc", ".ifczip", ".ifcjson") - filter_glob: bpy.props.StringProperty( - default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"} - ) - json_version: bpy.props.EnumProperty( - items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version" - ) - json_compact: bpy.props.BoolProperty( - name="Export Compact IFCJSON", default=False - ) - should_save_as: bpy.props.BoolProperty( - name="Should Save As", default=False, options={"HIDDEN"} - ) - use_relative_path: bpy.props.BoolProperty( - name="Use Relative Path", default=False - ) + filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}) + json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version") + json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) + should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"}) + use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) if TYPE_CHECKING: filter_glob: str @@ -2918,12 +2894,8 @@ class IFCFileHandlerOperator(bpy.types.Operator): bl_label = "Import .ifc file" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - directory: bpy.props.StringProperty( - subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"} - ) - files: bpy.props.CollectionProperty( - type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"} - ) + directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}) + files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}) if TYPE_CHECKING: directory: str From 98338e08316967a3c0ef45af368ead6db5d3a6e5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 18:06:21 +0500 Subject: [PATCH 17/25] Rename ci-black-formatting workflow to ci-lint --- .github/workflows/{ci-black-formatting.yaml => ci-lint.yaml} | 2 +- src/bonsai/docs/guides/development/code_style.rst | 2 +- src/bonsai/docs/guides/development/maintenance.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{ci-black-formatting.yaml => ci-lint.yaml} (99%) diff --git a/.github/workflows/ci-black-formatting.yaml b/.github/workflows/ci-lint.yaml similarity index 99% rename from .github/workflows/ci-black-formatting.yaml rename to .github/workflows/ci-lint.yaml index f081f02945..ddd1f70189 100644 --- a/.github/workflows/ci-black-formatting.yaml +++ b/.github/workflows/ci-lint.yaml @@ -1,4 +1,4 @@ -name: ci-black-formatting +name: ci-lint on: push: diff --git a/src/bonsai/docs/guides/development/code_style.rst b/src/bonsai/docs/guides/development/code_style.rst index cdc506d5ab..96f3d2b9cc 100644 --- a/src/bonsai/docs/guides/development/code_style.rst +++ b/src/bonsai/docs/guides/development/code_style.rst @@ -7,7 +7,7 @@ Python code formatters For Python code formatting, we use `Black code formatter `__, black settings are stored in the repository's pyproject.toml. -We have GitHub workflow `ci-black-formatting` to maintain black formatting across the repository. +We have GitHub workflow `ci-lint` to maintain black formatting across the repository. ``black`` can be installed using ``pip install black`` and files can be formatted with the following example command: diff --git a/src/bonsai/docs/guides/development/maintenance.rst b/src/bonsai/docs/guides/development/maintenance.rst index e35306de1f..d0de81c757 100644 --- a/src/bonsai/docs/guides/development/maintenance.rst +++ b/src/bonsai/docs/guides/development/maintenance.rst @@ -13,7 +13,7 @@ When adding or removing a supported Python version, update the following: * - File - What to update - * - ``.github/workflows/ci-black-formatting.yaml`` + * - ``.github/workflows/ci-lint.yaml`` - ``MIN_IOS_PY_VERSION`` * - ``.github/workflows/ci-ifcopenshell-python-pypi.yml`` - ``pyver`` matrix @@ -59,7 +59,7 @@ When Blender ships with a new Python version: * - File - What to update - * - ``.github/workflows/ci-black-formatting.yaml`` + * - ``.github/workflows/ci-lint.yaml`` - ``MIN_BLENDER_PY_VERSION`` * - ``src/bonsai/Makefile`` - ``SUPPORTED_PYVERSIONS`` From 0cf831133e159ccb8e0309f0e48cf20d2335de24 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 18:20:47 +0500 Subject: [PATCH 18/25] ci-lint - add `ty` type check --- .github/workflows/ci-lint.yaml | 11 +++++++++++ pyproject.toml | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-lint.yaml b/.github/workflows/ci-lint.yaml index ddd1f70189..6dc18453ad 100644 --- a/.github/workflows/ci-lint.yaml +++ b/.github/workflows/ci-lint.yaml @@ -30,6 +30,7 @@ jobs: uv tool install ruff uv tool install black uv tool install poethepoet + uv tool install ty # black doesn't catch all syntax errors, so we check them explicitly. - name: Check syntax errors @@ -57,6 +58,13 @@ jobs: black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py continue-on-error: true + - name: ty check + id: ty + run: | + poe ty-venv + poe ty + continue-on-error: true + - name: Ruff check id: ruff run: | @@ -105,4 +113,7 @@ jobs: if [ "${{ steps.ruff.outcome }}" != "success" ]; then echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1 fi + if [ "${{ steps.ty.outcome }}" != "success" ]; then + echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1 + fi exit $ERROR diff --git a/pyproject.toml b/pyproject.toml index 235d3f6e6a..2b9552cdd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ dependencies = [ "black==26.3.1", "ruff==0.15.9", "poethepoet", + "ty==0.0.29", "gersemi==0.26.1", ] @@ -227,7 +228,7 @@ ty.sequence = ["ty-bonsai", "ty-ios"] ty.help = "Run ty type checker. Requires ty-venv to be set up first." ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv" -ty-venv.sequence = ["ty-venv-bonsai", "ty-venv-ios"] +ty-venv.sequence = ["bonsai-deps", "ty-venv-bonsai", "ty-venv-ios"] ty-venv-bonsai.sequence = [ {cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"}, From fa8770c14d9d48057a8534272e60bcedab6a6773 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 18:21:03 +0500 Subject: [PATCH 19/25] ty - drop rules removed from recent version of ty --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b9552cdd5..eb6b4620d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,6 @@ all = "ignore" # Structural rules (no deep type inference needed, easier to adapt). abstract-method-in-final-class = "error" ambiguous-protocol-member = "error" -byte-string-type-annotation = "error" conflicting-declarations = "error" conflicting-metaclass = "error" cyclic-class-definition = "error" @@ -100,7 +99,6 @@ empty-body = "error" escape-character-in-forward-annotation = "error" final-on-non-method = "error" final-without-value = "error" -fstring-type-annotation = "error" ignore-comment-unknown-rule = "error" implicit-concatenated-string-type-annotation = "error" inconsistent-mro = "error" From 588f3653662a66986c1b2fb63c25b931366ae4c6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 18:55:49 +0500 Subject: [PATCH 20/25] Remove unused ty ignores - issue is resolved upsteam in stubs --- src/bonsai/bonsai/bim/module/aggregate/decorator.py | 2 +- src/bonsai/bonsai/bim/module/nest/decorator.py | 2 +- src/bonsai/bonsai/bim/module/structural/shader.py | 6 +++--- src/bonsai/bonsai/tool/ifcgit.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/aggregate/decorator.py b/src/bonsai/bonsai/bim/module/aggregate/decorator.py index 2cd1c1bca0..eb389a58bd 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/decorator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/decorator.py @@ -101,7 +101,7 @@ class AggregateDecorator: cls.is_installed = False def dotted_line_shader(self): - vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments] + vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out.smooth("FLOAT", "v_ArcLength") shader_info = gpu.types.GPUShaderCreateInfo() diff --git a/src/bonsai/bonsai/bim/module/nest/decorator.py b/src/bonsai/bonsai/bim/module/nest/decorator.py index 4a3637caa6..28c3835ba7 100644 --- a/src/bonsai/bonsai/bim/module/nest/decorator.py +++ b/src/bonsai/bonsai/bim/module/nest/decorator.py @@ -101,7 +101,7 @@ class NestDecorator: cls.is_installed = False def dotted_line_shader(self): - vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments] + vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out.smooth("FLOAT", "v_ArcLength") shader_info = gpu.types.GPUShaderCreateInfo() diff --git a/src/bonsai/bonsai/bim/module/structural/shader.py b/src/bonsai/bonsai/bim/module/structural/shader.py index 9688ce9f0e..b9b5a5c7bc 100644 --- a/src/bonsai/bonsai/bim/module/structural/shader.py +++ b/src/bonsai/bonsai/bim/module/structural/shader.py @@ -83,7 +83,7 @@ class DecorationShader: PARALLEL DISTRIBUTED FORCE, DISTRIBUTED MOMENT, """ - vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments] + vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out.smooth("VEC3", "forces") vert_out.smooth("VEC3", "co") @@ -203,7 +203,7 @@ class DecorationShader: """param: pattern: type of pattern SINGLE FORCE, SINGLE MOMENT""" - vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments] + vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out.smooth("VEC3", "co") shader_info = gpu.types.GPUShaderCreateInfo() @@ -253,7 +253,7 @@ class DecorationShader: def get_planar_shader(self) -> gpu.types.GPUShader: """shader for planar loads""" - vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments] + vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out.smooth("VEC3", "co") shader_info = gpu.types.GPUShaderCreateInfo() diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 2db7a5a171..49e6440bae 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -286,7 +286,7 @@ class IfcGit: if re.match("^Ifc", obj.name): bpy.data.objects.remove(obj, do_unlink=True) - bpy.data.orphans_purge(do_recursive=True) # ty:ignore[unknown-argument] + bpy.data.orphans_purge(do_recursive=True) import bonsai.bim.handler from bonsai.bim.module.model.data import AuthoringData From 4896946e784ff2b25035de767b5e9fe2efd79540 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 19:06:14 +0500 Subject: [PATCH 21/25] ty ignore some upstream bpy stubs issues --- .../bonsai/bim/module/geometry/operator.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 7382b093f4..8d075a9605 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -85,7 +85,7 @@ class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator): class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.override_mesh_separate" bl_label = "IFC Mesh Separate" - blender_op = bpy.ops.mesh.separate.get_rna_type() + blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument] bl_description = blender_op.description + ".\nAlso makes sure changes are in sync with IFC." bl_options = {"REGISTER", "UNDO"} blender_type_prop = blender_op.properties["type"] @@ -246,7 +246,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.override_origin_set" - blender_op = bpy.ops.object.origin_set.get_rna_type() + blender_op = bpy.ops.object.origin_set.get_rna_type() # ty: ignore[missing-argument] bl_label = "IFC Origin Set" bl_description = ( blender_op.description + ".\nAlso makes sure changes are in sync with IFC (operator works only on IFC objects)" @@ -801,7 +801,7 @@ def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context class OverrideDelete(bpy.types.Operator): bl_idname = "bim.override_object_delete" bl_label = "IFC Delete" - blender_op = bpy.ops.object.delete.get_rna_type() + blender_op = bpy.ops.object.delete.get_rna_type() # ty: ignore[missing-argument] bl_description = ( blender_op.description + ".\nAlso makes sure changes in sync with IFC." @@ -821,7 +821,7 @@ class OverrideDelete(bpy.types.Operator): def poll(cls, context): # Match `object.delete` poll for consistency. # `object.delete` poll just checks for OBJECT mode. - poll = bpy.ops.object.delete.poll() + poll = bpy.ops.object.delete.poll() # ty: ignore[missing-argument] if poll: return True cls.poll_message_set("Only available in OBJECT mode") @@ -1045,7 +1045,7 @@ class SelectedIdsData(NamedTuple): class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.override_outliner_delete" bl_label = "IFC Delete" - blender_op = bpy.ops.outliner.delete.get_rna_type() + blender_op = bpy.ops.outliner.delete.get_rna_type() # ty: ignore[missing-argument] bl_description = ( blender_op.description + ".\nAlso makes sure changes in sync with IFC." @@ -1060,7 +1060,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator): def poll(cls, context) -> bool: # Match `outliner.delete` poll for consistency. # `outliner.delete` just checks `area.type` == `OUTLINER`. - poll = bpy.ops.outliner.delete.poll() + poll = bpy.ops.outliner.delete.poll() # ty: ignore[missing-argument] if poll: return True cls.poll_message_set("Only available from Outliner.") @@ -1164,7 +1164,7 @@ class OverrideDuplicateMove(bpy.types.Operator): def poll(cls, context) -> bool: # Match `object.duplicate_move` poll for consistency. # `object.duplicate_move` poll checks for OBJECT mode. - poll = bpy.ops.object.duplicate_move.poll() + poll = bpy.ops.object.duplicate_move.poll() # ty: ignore[missing-argument] if poll: return True cls.poll_message_set("Only available in OBJECT mode") @@ -1908,7 +1908,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.override_object_join" bl_label = "IFC Join" - blender_op = bpy.ops.mesh.separate.get_rna_type() + blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument] bl_description = ( blender_op.description + ".\nAlso makes sure changes are in sync with IFC." @@ -1926,7 +1926,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - if not bpy.ops.object.join.poll(): + if not bpy.ops.object.join.poll(): # ty: ignore[missing-argument] cls.poll_message_set("Active object is not EDITable.") return False if not context.selected_editable_objects: From a3efa7e9ee6391946bfae8931e8fbadf0fd22092 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 10 Apr 2026 19:10:15 +0500 Subject: [PATCH 22/25] util.element - fix IfcComplexProperty KeyError when verbose=True (#7921) Introduced by me in b77df1892 --- .../ifcopenshell/util/element.py | 2 +- .../test/util/test_element.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 1c52ebc49d..8a8a817336 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -469,7 +469,7 @@ def get_properties( del data["HasProperties"] results[prop_name] = data if verbose: - results[prop_name] = {"id": data["id"], "class": data["class"], "value": results[prop_name]} + results[prop_name] = {"id": data["id"], "class": data["type"], "value": results[prop_name]} return results diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 5eb6034402..ffd5789f01 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -307,6 +307,35 @@ class TestGetPropertiesIFC4(test.bootstrap.IFC4): } } + def test_getting_complex_properties_verbose(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="pset") + complex_property = self.file.create_entity("IfcComplexProperty", Name="prop", UsageName="usage_name") + ifcopenshell.api.pset.edit_pset(self.file, pset=complex_property, properties={"a": "b"}) + pset.HasProperties = [complex_property] + properties = subject.get_properties(pset.HasProperties, verbose=True) + prop_value = properties["prop"]["value"] + nested_prop = prop_value["properties"]["a"] + assert properties == { + "prop": { + "id": complex_property.id(), + "class": "IfcComplexProperty", + "value": { + "UsageName": "usage_name", + "id": complex_property.id(), + "type": "IfcComplexProperty", + "properties": { + "a": { + "id": nested_prop["id"], + "class": "IfcPropertySingleValue", + "value": "b", + "value_type": "IfcLabel", + } + }, + }, + } + } + class TestGetElementsUsingPset(test.bootstrap.IFC4): def test_run(self): From 158756e9218358a719393bdc739f644bb15d67d9 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 10 Apr 2026 21:45:14 +0200 Subject: [PATCH 23/25] arrange_polygons: settings, simplify based on growing boxes; more... --- src/ifcopenshell-python/ifcopenshell/draw.py | 3 +- src/ifcwrap/IfcGeomWrapper.i | 5 +- src/svgfill/src/arrange_polygons.cpp | 1710 +++++++++++++++--- src/svgfill/src/graph_2d.h | 7 + src/svgfill/src/svgfill.cpp | 12 + src/svgfill/src/svgfill.h | 24 +- 6 files changed, 1503 insertions(+), 258 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/draw.py b/src/ifcopenshell-python/ifcopenshell/draw.py index 5f6d761ceb..962dbbb34f 100644 --- a/src/ifcopenshell-python/ifcopenshell/draw.py +++ b/src/ifcopenshell-python/ifcopenshell/draw.py @@ -42,6 +42,7 @@ WHITE = numpy.array((1.0, 1.0, 1.0)) DO_NOTHING = lambda *args: None +ARRANGE_POLYGON_SETTINGS = W.arrange_polygon_settings() if hasattr(W, 'arrange_polygon_settings') else None @dataclass class draw_settings: @@ -536,7 +537,7 @@ def main( *(tup for i, tup in enumerate(zip(path_objects, section_polies, polies)) if has_relevant_zone(i)) ) - arranged = W.arrange_polygons(polies) + arranged = W.arrange_polygons(*filter(None, (ARRANGE_POLYGON_SETTINGS,)), polies) svg_data_3 = W.polygons_to_svg(arranged, False) dom3 = parseString(svg_data_3) svg3 = dom3.childNodes[0] diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index e155c2837e..e992e4beab 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -1166,6 +1166,7 @@ ifcopenshell::geometry::taxonomy::item::ptr try_upcast(PyObject* obj0, swig_type %ignore svgfill::line_segments_to_polygons; %ignore svgfill::svg_to_polygons; %ignore svgfill::arrange_polygons; +%ignore svgfill::abstract_arrangement; %template(svg_line_segments) std::vector>; %template(svg_groups_of_line_segments) std::vector>>; @@ -1287,9 +1288,9 @@ ifcopenshell::geometry::taxonomy::item::ptr try_upcast(PyObject* obj0, swig_type } } - std::vector arrange_polygons(const std::vector& polygons) { + std::vector arrange_polygons(svgfill::arrange_polygon_settings settings, const std::vector& polygons) { std::vector r; - if (svgfill::arrange_polygons(polygons, r)) { + if (svgfill::arrange_polygons(settings, polygons, r)) { return r; } else { throw std::runtime_error("Failed to arrange polygons"); diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 4fa20c68dc..ba8785a267 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -350,6 +350,8 @@ find_overlaps(const std::vector& polygons) { class DebugWriter { public: + DebugWriter() : enabled_(false) {} + DebugWriter(bool enabled, const std::string& filename_prefix) : enabled_(enabled) { if (enabled_) { @@ -368,6 +370,43 @@ class DebugWriter { } } + DebugWriter(const DebugWriter&) = delete; + + DebugWriter(DebugWriter&& other) noexcept + : obj(std::move(other.obj)), vi(other.vi), svg(std::move(other.svg)), enabled_(other.enabled_), last_segment_name_(std::move(other.last_segment_name_)) + { + other.enabled_ = false; + other.vi = 1; + other.last_segment_name_.clear(); + } + + DebugWriter& operator=(const DebugWriter&) = delete; + + DebugWriter& operator=(DebugWriter&& other) noexcept { + if (this == &other) { + return *this; + } + + if (enabled_) { + svg << "\n"; + obj << std::flush; + obj.close(); + svg.close(); + } + + obj = std::move(other.obj); + svg = std::move(other.svg); + vi = other.vi; + enabled_ = other.enabled_; + last_segment_name_ = std::move(other.last_segment_name_); + + other.enabled_ = false; + other.vi = 1; + other.last_segment_name_.clear(); + + return *this; + } + void write_polygon(const Polygon_2& polygon, const std::string& name) { if (enabled_) { write_polygon_to_obj_(obj, vi, true, polygon, name); @@ -387,7 +426,7 @@ class DebugWriter { obj << "l " << vi++; obj << " " << vi++ << "\n"; - svg << ""; + svg << "\n"; obj << std::flush; } @@ -468,7 +507,7 @@ class DebugWriter { } }; -void eliminate_overlaps(double OVERLAP_RESOLUTION_DISTANCE, std::vector& polygons) { +void eliminate_overlaps(DebugWriter& debug_writer, double OVERLAP_RESOLUTION_DISTANCE, std::vector& polygons) { // solve overlaps by means of subtraction // loop over overlaps and subtract the smaller polygon from the larger one @@ -576,11 +615,37 @@ void eliminate_overlaps(double OVERLAP_RESOLUTION_DISTANCE, std::vector(25, 27); + bool success = false; if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) { + if (is_) { + debug_writer.write_polygon(*mp1, "mp1"); + } + smooth_polygon(OVERLAP_RESOLUTION_DISTANCE / 100., *mp1); + if (is_) { + debug_writer.write_polygon(*mp1, "mp1b"); + } if ((mp2 = subtract_retain_largest(*poly1, *mp1))) { + if (is_) { + debug_writer.write_polygon(*mp2, "mp2"); + } + smooth_polygon(OVERLAP_RESOLUTION_DISTANCE / 100., *mp2); + if (is_) { + debug_writer.write_polygon(*mp2, "mp2b"); + } if ((mp3 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE * 2, *mp2)))) { + if (is_) { + debug_writer.write_polygon(*mp3, "mp3"); + } + smooth_polygon(OVERLAP_RESOLUTION_DISTANCE / 100., *mp3); + if (is_) { + debug_writer.write_polygon(*mp3, "mp3b"); + } if ((mp4 = subtract_retain_largest(*poly2, *mp3))) { + if (is_) { + debug_writer.write_polygon(*mp4, "mp4"); + } *poly1 = *mp2; *poly2 = *mp4; success = true; @@ -777,14 +842,19 @@ Polygon_with_holes_2 subdivide_polygon(double max_distance, const Polygon_with_h std::tuple< std::map>, std::map>, - std::map, std::vector*>>> -build_line_graph(const std::vector& input_polygons, SegmentLookup& segment_lookup, const std::vector& triangular_polygons) { + std::map, std::vector*>>, + std::map +> +build_line_graph(const std::vector& input_polygons, SegmentLookup& segment_lookup, const std::vector& triangular_polygons) +{ + // Build maps of triangle -> edge and edge -> triangle in order to do traversal on the 'corridor mesh' std::map, std::vector*>> segment_to_facet; std::map, std::vector*>> segment_to_input_facet; 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; for (auto& tri : triangular_polygons) { for (size_t i = 0; i < 3; ++i) { @@ -813,6 +883,7 @@ build_line_graph(const std::vector& input_polygons, SegmentLookup& se if (p1index != input_polygons.end() && p2index != input_polygons.end() && p1index != p2index) { 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))); } } @@ -832,7 +903,682 @@ build_line_graph(const std::vector& input_polygons, SegmentLookup& se } } - return {line_graph, midpoint_to_segment, segment_to_input_facet}; + return {line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length}; +} + +using DPoint = CGAL::Simple_cartesian::Point_2; +using DDir = CGAL::Simple_cartesian::Vector_2; +using DBox = std::array; + +struct CenterLineGraphData { + std::vector points; + std::vector points_double; + std::vector widths; + std::vector> edges; + std::vector> incident_edges; +}; + +struct LineRun { + Point_2 start_exact; + Point_2 end_exact; + DPoint start; + DPoint end; + DDir direction; + double avg_width; + double length; + size_t vertex_count; +}; + +struct RunBoxRecord { + size_t run_index; + DPoint start; + DPoint end; + DDir direction; + double width; + double length; + std::array corners; + DBox bbox; +}; + +struct MergedBoxRecord { + DPoint start; + DPoint end; + DDir direction; + DDir normal; + double avg_width; + double length; + size_t member_count; + std::vector members; + std::array corners; + DBox bbox; + Point_2 exact_start; + Point_2 exact_end; +}; + +struct BoxCluster { + std::vector members; + MergedBoxRecord box; +}; + +struct SnapCandidate { + size_t box_index; + double box_distance; + double line_distance; + Point_2 projection; +}; + +DDir unit(const DDir& a) { + auto n = std::sqrt(a.squared_length()); + if (n < 1.e-9) { + return {0., 0.}; + } + return a / n; +} + +DDir perpendicular(const DDir& a) { + return DDir(-a.y(), a.x()); +} + +DDir canonicalize_like(const DDir& a, const DDir& ref) { + return (a * ref) < 0. ? -a : a; +} + +DPoint to_double_point(const Point_2& p) { + return {CGAL::to_double(p.x()), CGAL::to_double(p.y())}; +} + +Point_2 to_exact_point(const DPoint& p) { + return Point_2(p.x(), p.y()); +} + +double point_line_distance(const DPoint& p, const DPoint& line_point, const DDir& line_dir) { + auto u = unit(line_dir); + auto delta = (p - line_point); + if (u.squared_length() < 1.e-18) { + return std::sqrt(delta.squared_length()); + } + return std::abs(CGAL::determinant(u.x(), u.y(), delta.x(), delta.y())); +} + +double angle_between_dirs_deg(const DDir& a, const DDir& b) { + auto u = unit(a); + auto v = unit(b); + auto c = std::abs(u * v); + if (c > 1.) { + c = 1.; + } + return std::acos(c) * 180. / 3.14159265358979323846; +} + +std::array rectangle_corners(const DPoint& start, const DPoint& end, double width) { + auto u = unit(end - start); + if (u.squared_length() < 1.e-18) { + u = {1., 0.}; + } + auto n = perpendicular(u); + auto ext = width; + auto p0 = start - u * ext; + auto p1 = end + u * ext; + auto w = n * (width / 2.); + return {p0 + w, p1 + w, p1 - w, p0 - w}; +} + +DBox aabb_from_points(const std::array& corners) { + DBox bbox{corners[0], corners[0]}; + for (auto& p : corners) { + bbox[0] = {std::min(bbox[0].x(), p.x()), std::min(bbox[0].y(), p.y())}; + bbox[1] = {std::max(bbox[1].x(), p.x()), std::max(bbox[1].y(), p.y())}; + } + return bbox; +} + +bool aabb_overlap(const DBox& a, const DBox& b, double eps = 1.e-9) { + return a[0].x() <= b[1].x() + eps && + a[1].x() + eps >= b[0].x() && + a[0].y() <= b[1].y() + eps && + a[1].y() + eps >= b[0].y(); +} + +CenterLineGraphData make_center_line_graph_data( + const std::map>& line_graph, + const std::map& midpoint_to_edge_length) +{ + CenterLineGraphData graph; + std::map point_to_index; + + auto ensure_point = [&](const Point_2& p) { + auto it = point_to_index.find(p); + if (it != point_to_index.end()) { + return it->second; + } + auto i = graph.points.size(); + point_to_index[p] = i; + graph.points.push_back(p); + 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; + }; + + for (auto& p : line_graph) { + ensure_point(p.first); + for (auto& q : p.second) { + ensure_point(q); + } + } + + std::set> seen_edges; + for (auto& p : line_graph) { + auto i = ensure_point(p.first); + for (auto& q : p.second) { + auto j = ensure_point(q); + if (i == j) { + continue; + } + auto e = i < j ? std::make_pair(i, j) : std::make_pair(j, i); + if (seen_edges.insert(e).second) { + auto k = graph.edges.size(); + graph.edges.push_back(e); + graph.incident_edges[e.first].push_back(k); + graph.incident_edges[e.second].push_back(k); + } + } + } + + return graph; +} + +double segment_width(const CenterLineGraphData& graph, const std::pair& edge) { + return 0.5 * (graph.widths[edge.first] + graph.widths[edge.second]); +} + +bool edge_supports_same_line( + const DPoint& seed_a, + const DPoint& seed_b, + const DPoint& test_a, + const DPoint& test_b, + double angle_tol_deg = 3., + double line_dist_tol = 0.15) +{ + auto d_seed = seed_b - seed_a; + auto d_test = test_b - test_a; + if (d_seed.squared_length() < 1.e-18 || d_test.squared_length() < 1.e-18) { + return false; + } + if (angle_between_dirs_deg(d_seed, d_test) > angle_tol_deg) { + return false; + } + return + point_line_distance(test_a, seed_a, d_seed) <= line_dist_tol && + point_line_distance(test_b, seed_a, d_seed) <= line_dist_tol; +} + +std::vector runs_from_graph(const CenterLineGraphData& graph, double angle_tol_deg = 3., double line_dist_tol = 0.15) { + std::vector visited(graph.edges.size(), false); + std::vector runs; + + for (size_t seed_ei = 0; seed_ei < graph.edges.size(); ++seed_ei) { + if (visited[seed_ei]) { + continue; + } + + const auto& seed_edge = graph.edges[seed_ei]; + auto seed_a = graph.points_double[seed_edge.first]; + auto seed_b = graph.points_double[seed_edge.second]; + auto seed_dir = seed_b - seed_a; + if (seed_dir.squared_length() < 1.e-18) { + visited[seed_ei] = true; + continue; + } + + std::vector queue = {seed_ei}; + std::set component_edges; + + while (!queue.empty()) { + auto ei = queue.back(); + queue.pop_back(); + if (!component_edges.insert(ei).second) { + continue; + } + + const auto& edge = graph.edges[ei]; + std::array vertices = {edge.first, edge.second}; + for (auto v : vertices) { + for (auto ej : graph.incident_edges[v]) { + if (ej == ei || visited[ej] || component_edges.count(ej)) { + continue; + } + const auto& candidate = graph.edges[ej]; + auto test_a = graph.points_double[candidate.first]; + auto test_b = graph.points_double[candidate.second]; + if (edge_supports_same_line(seed_a, seed_b, test_a, test_b, angle_tol_deg, line_dist_tol)) { + queue.push_back(ej); + } + } + } + } + + for (auto ei : component_edges) { + visited[ei] = true; + } + + std::set component_vertices; + auto ref = unit(seed_dir); + DDir direction_sum{0., 0.}; + double total_length = 0.; + double weighted_width_sum = 0.; + + for (auto ei : component_edges) { + const auto& edge = graph.edges[ei]; + component_vertices.insert(edge.first); + component_vertices.insert(edge.second); + + auto d = graph.points_double[edge.second] - graph.points_double[edge.first]; + auto u = canonicalize_like(unit(d), ref); + direction_sum = direction_sum + u; + + auto len = std::sqrt(d.squared_length()); + total_length += len; + weighted_width_sum += len * segment_width(graph, edge); + } + + auto run_direction = direction_sum.squared_length() < 1.e-18 ? ref : unit(direction_sum); + + double min_t = std::numeric_limits::infinity(); + double max_t = -std::numeric_limits::infinity(); + size_t start_index = *component_vertices.begin(); + size_t end_index = start_index; + for (auto vi : component_vertices) { + auto t = (graph.points_double[vi] - CGAL::ORIGIN) * run_direction; + if (t < min_t) { + min_t = t; + start_index = vi; + } + if (t > max_t) { + max_t = t; + end_index = vi; + } + } + + auto avg_width = total_length < 1.e-9 ? segment_width(graph, seed_edge) : weighted_width_sum / total_length; + + runs.push_back({ + graph.points[start_index], + graph.points[end_index], + graph.points_double[start_index], + graph.points_double[end_index], + run_direction, + avg_width, + std::sqrt((graph.points_double[end_index] - graph.points_double[start_index]).squared_length()), + component_vertices.size() + }); + } + + return runs; +} + +std::vector build_run_box_records(const std::vector& runs) { + std::vector records; + records.reserve(runs.size()); + for (size_t i = 0; i < runs.size(); ++i) { + auto corners = rectangle_corners(runs[i].start, runs[i].end, runs[i].avg_width); + records.push_back({ + i, + runs[i].start, + runs[i].end, + unit(runs[i].end - runs[i].start), + runs[i].avg_width, + runs[i].length, + corners, + aabb_from_points(corners) + }); + } + return records; +} + +template +std::pair projected_interval_on_axis(const T& box, const DDir& axis_u) { + auto u = unit(axis_u); + auto ta = (box.start - CGAL::ORIGIN) * u; + auto tb = (box.end - CGAL::ORIGIN) * u; + return {std::min(ta, tb), std::max(ta, tb)}; +} + +double interval_overlap_length(const std::pair& a, const std::pair& b) { + return std::max(0., std::min(a.second, b.second) - std::max(a.first, b.first)); +} + +template +double boxes_overlap_along_merge_axis(const T& a, const T& b) { + auto d1 = unit(a.end - a.start); + auto d2 = unit(b.end - b.start); + if (d1 * d2 < 0.) { + d2 = {-d2.x(), -d2.y()}; + } + auto merge_axis = unit(d1 + d2); + if (merge_axis.squared_length() < 1.e-18) { + merge_axis = d1; + } + + auto i1 = projected_interval_on_axis(a, merge_axis); + auto i2 = projected_interval_on_axis(b, merge_axis); + auto overlap = interval_overlap_length(i1, i2); + auto small_length = std::min(i1.second - i1.first, i2.second - i2.first); + if (small_length < 1.e-9) { + return false; + } + return overlap / small_length; +} + +MergedBoxRecord merge_cluster_to_box(const std::vector& member_indices, const std::vector& records) { + auto ref = records[member_indices.front()].direction; + DDir direction_sum{0., 0.}; + for (auto i : member_indices) { + auto u = canonicalize_like(records[i].direction, ref); + direction_sum = direction_sum + u * std::max(records[i].length, 1.e-9); + } + + auto u = direction_sum.squared_length() < 1.e-18 ? ref : unit(direction_sum); + auto n = perpendicular(u); + + double tmin = std::numeric_limits::infinity(); + double tmax = -std::numeric_limits::infinity(); + double smin = std::numeric_limits::infinity(); + double smax = -std::numeric_limits::infinity(); + + for (auto i : member_indices) { + for (auto& corner : records[i].corners) { + auto t = (corner - CGAL::ORIGIN) * u; + auto s = (corner - CGAL::ORIGIN) * n; + tmin = std::min(tmin, t); + tmax = std::max(tmax, t); + smin = std::min(smin, s); + smax = std::max(smax, s); + } + } + + auto width = smax - smin; + auto sc = (smin + smax) / 2.; + auto start = u * tmin + n * sc; + auto end = u * tmax + n * sc; + auto corners = rectangle_corners(CGAL::ORIGIN + start, CGAL::ORIGIN + end, width); + + MergedBoxRecord box{ + CGAL::ORIGIN + start, + CGAL::ORIGIN + end, + u, + n, + width, + std::sqrt((end - start).squared_length()), + member_indices.size(), + member_indices, + corners, + aabb_from_points(corners), + to_exact_point(CGAL::ORIGIN + start), + to_exact_point(CGAL::ORIGIN + end) + }; + return box; +} + +std::pair merge_score(const MergedBoxRecord& a, const MergedBoxRecord& b) { + auto ang = angle_between_dirs_deg(a.direction, b.direction); + auto center_a = ((a.start - CGAL::ORIGIN) + (a.end - CGAL::ORIGIN)) / 2.; + auto center_b = ((b.start - CGAL::ORIGIN) + (b.end - CGAL::ORIGIN)) / 2.; + return {ang, std::sqrt((center_b - center_a).squared_length())}; +} + +bool clusters_can_merge(const BoxCluster& a, const BoxCluster& b, double angle_tol_deg = 5., double axis_overlap_ratio_limit = 0.5) { + if (!aabb_overlap(a.box.bbox, b.box.bbox)) { + return false; + } + if (angle_between_dirs_deg(a.box.direction, b.box.direction) > angle_tol_deg) { + return false; + } + if (boxes_overlap_along_merge_axis(a.box, b.box) > axis_overlap_ratio_limit) { + auto a_center = CGAL::ORIGIN + ((a.box.start - CGAL::ORIGIN) + (a.box.end - CGAL::ORIGIN)) / 2.; + auto b_center = CGAL::ORIGIN + ((b.box.start - CGAL::ORIGIN) + (b.box.end - CGAL::ORIGIN)) / 2.; + auto a_dir = a.box.direction; + auto b_dir = b.box.direction; + auto dist = a.box.length < b.box.length ? point_line_distance(a_center, b_center, b_dir) : point_line_distance(b_center, a_center, a_dir); + auto ref = a.box.length < b.box.length ? a.box.avg_width : b.box.avg_width; + return dist < (ref / 4.); + } + return true; +} + +std::vector merge_intersecting_parallel_boxes_iterative(const std::vector& runs) { + auto records = build_run_box_records(runs); + std::vector clusters; + clusters.reserve(records.size()); + for (size_t i = 0; i < records.size(); ++i) { + clusters.push_back({{i}, merge_cluster_to_box({i}, records)}); + } + + while (true) { + std::optional> best_pair; + std::pair best_score; + + for (size_t i = 0; i < clusters.size(); ++i) { + for (size_t j = i + 1; j < clusters.size(); ++j) { + if (!clusters_can_merge(clusters[i], clusters[j])) { + continue; + } + auto score = merge_score(clusters[i].box, clusters[j].box); + if (!best_pair || score < best_score) { + best_pair = std::make_pair(i, j); + best_score = score; + } + } + } + + if (!best_pair) { + break; + } + + auto i = best_pair->first; + auto j = best_pair->second; + 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::vector next_clusters; + next_clusters.reserve(clusters.size() - 1); + for (size_t k = 0; k < clusters.size(); ++k) { + if (k != i && k != j) { + next_clusters.push_back(std::move(clusters[k])); + } + } + next_clusters.push_back(std::move(merged)); + clusters = std::move(next_clusters); + } + + std::vector merged_boxes; + merged_boxes.reserve(clusters.size()); + for (auto& cluster : clusters) { + merged_boxes.push_back(cluster.box); + } + return merged_boxes; +} + +Point_2 project_point_to_line_exact(const Point_2& p, const MergedBoxRecord& box) { + auto d = box.exact_end - box.exact_start; + if (d.squared_length() == 0) { + return box.exact_start; + } + auto t = ((p - box.exact_start) * d) / d.squared_length(); + return box.exact_start + d * t; +} + +boost::optional intersect_infinite_lines_exact(const MergedBoxRecord& a, const MergedBoxRecord& b) { + if (a.exact_start == a.exact_end || b.exact_start == b.exact_end) { + return boost::none; + } + auto x = CGAL::intersection(CGAL::Line_2(a.exact_start, a.exact_end), CGAL::Line_2(b.exact_start, b.exact_end)); + if (!x) { + return boost::none; + } + if (auto* xp = variant_get(&*x)) { + return *xp; + } + return boost::none; +} + +double point_to_oriented_box_distance(const DPoint& p, const MergedBoxRecord& box) { + auto d = box.end - box.start; + auto L = std::sqrt(d.squared_length()); + if (L < 1.e-9) { + return std::sqrt((p - box.start).squared_length()); + } + + auto u = d / L; + auto n = perpendicular(u); + auto rel = p - box.start; + auto t = rel * u; + auto s = rel * n; + + auto tmin = -box.avg_width / 2.; + auto tmax = L + box.avg_width / 2.; + auto smin = -box.avg_width / 2.; + auto smax = box.avg_width / 2.; + + double dt = 0.; + if (t < tmin) { + dt = tmin - t; + } else if (t > tmax) { + dt = t - tmax; + } + + double ds = 0.; + if (s < smin) { + ds = smin - s; + } else if (s > smax) { + ds = s - smax; + } + + return std::hypot(dt, ds); +} + +std::map> snap_points_to_box_axes( + const CenterLineGraphData& graph, + const std::vector& boxes) +{ + std::vector snapped_points(graph.points.size()); + + for (size_t i = 0; i < graph.points.size(); ++i) { + if (boxes.empty()) { + snapped_points[i] = graph.points[i]; + continue; + } + + std::vector candidates; + candidates.reserve(boxes.size()); + for (size_t j = 0; j < boxes.size(); ++j) { + candidates.push_back({ + j, + point_to_oriented_box_distance(graph.points_double[i], boxes[j]), + point_line_distance(graph.points_double[i], boxes[j].start, boxes[j].direction), + project_point_to_line_exact(graph.points[i], boxes[j]) + }); + } + + std::vector containing; + for (auto& candidate : candidates) { + if (candidate.box_distance <= 1.e-9) { + containing.push_back(candidate); + } + } + + auto less = [](const SnapCandidate& a, const SnapCandidate& b) { + if (a.line_distance != b.line_distance) { + return a.line_distance < b.line_distance; + } + return a.box_distance < b.box_distance; + }; + + if (containing.size() >= 2) { + std::sort(containing.begin(), containing.end(), less); + auto& c1 = containing[0]; + 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; + continue; + } + } + snapped_points[i] = c1.projection; + continue; + } + + if (containing.size() == 1) { + snapped_points[i] = containing[0].projection; + continue; + } + + auto best = *std::min_element(candidates.begin(), candidates.end(), [](const SnapCandidate& a, const SnapCandidate& b) { + if (a.box_distance != b.box_distance) { + return a.box_distance < b.box_distance; + } + return a.line_distance < b.line_distance; + }); + snapped_points[i] = best.projection; + } + + std::map> adjacency; + for (auto& edge : graph.edges) { + auto a = snapped_points[edge.first]; + auto b = snapped_points[edge.second]; + if (a == b) { + continue; + } + adjacency[a].insert(b); + adjacency[b].insert(a); + } + + std::map> snapped_graph; + for (auto& p : adjacency) { + snapped_graph[p.first] = {p.second.begin(), p.second.end()}; + } + return snapped_graph; +} + +Graph2D join_segment_runs( + DebugWriter& debug, + const std::map>& line_graph, + const std::map& midpoint_to_edge_length) +{ + auto graph = make_center_line_graph_data(line_graph, midpoint_to_edge_length); + auto runs = runs_from_graph(graph); + runs.erase(std::remove_if(runs.begin(), runs.end(), [](const LineRun& run) { + return run.vertex_count <= 5; + }), runs.end()); + + std::vector run_polygons; + for (auto& r : runs) { + auto ps = rectangle_corners(r.start, r.end, r.avg_width); + std::array exact_corners; + std::transform(ps.begin(), ps.end(), exact_corners.begin(), [](const DPoint& p) { + return to_exact_point(p); + }); + run_polygons.emplace_back(exact_corners.begin(), exact_corners.end()); + } + debug.write_polygons(run_polygons, "initial_runs"); + run_polygons.clear(); + + auto boxes = merge_intersecting_parallel_boxes_iterative(runs); + + for (auto& r : boxes) { + auto ps = rectangle_corners(r.start, r.end, r.avg_width); + std::array exact_corners; + std::transform(ps.begin(), ps.end(), exact_corners.begin(), [](const DPoint& p) { + return to_exact_point(p); + }); + run_polygons.emplace_back(exact_corners.begin(), exact_corners.end()); + } + debug.write_polygons(run_polygons, "merged_boxes"); + + auto snapped_graph = snap_points_to_box_axes(graph, boxes); + return Graph2D(snapped_graph); } std::set> find_triangles(const std::map>& line_graph) { @@ -1118,66 +1864,88 @@ std::list> extend_end_vertices_based_on_input( const Graph2D& G, const std::map>& midpoint_to_segment, const std::map, std::vector*>>& segment_to_input_facet, - const Polygon_list& inner_offset, - const SegmentLookup& segment_lookup + const Polygon_list& outer_perimiter, + const SegmentLookup& segment_lookup, + const K::FT& max_projection_distance ){ std::list> constructed_segments; - for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { - if (it->second.size() == 1) { - auto& M = it->first; + std::set processed_vertices; - const std::pair* q = nullptr; + while (true) { + // The idea was to peal off 1-degree vertices when projecting them did not result into + // nearby intersections with the outer perimiter. This in case there would be turns near + // the perimeter, which would be eliminated by pealing off the vertices, which would then + // require out of the loop because of invalidated iterators. For now we decided to stick + // to a projection of the vertex onto the perimeter segment when the projection distance + // exceeds a threshold. + bool broke_out = false; - if (midpoint_to_segment.find(M) == midpoint_to_segment.end()) { - typename K::FT min_sq_distance = std::numeric_limits::infinity(); - for (auto& pa : midpoint_to_segment) { - if (CGAL::squared_distance(pa.first, M) < min_sq_distance) { - q = &pa.second; - min_sq_distance = CGAL::squared_distance(pa.first, M); - } + for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { + if (it->second.size() == 1) { + auto& M = it->first; + + if (processed_vertices.find(M) != processed_vertices.end()) { + continue; } - } else { - q = &midpoint_to_segment.find(M)->second; - } - if (q == nullptr) { - continue; - } + const std::pair* q = nullptr; - bool handled_as_graph_path = false; + if (midpoint_to_segment.find(M) == midpoint_to_segment.end()) { + typename K::FT min_sq_distance = std::numeric_limits::infinity(); + for (auto& pa : midpoint_to_segment) { + if (CGAL::squared_distance(pa.first, M) < min_sq_distance) { + q = &pa.second; + min_sq_distance = CGAL::squared_distance(pa.first, M); + } + } + } else { + q = &midpoint_to_segment.find(M)->second; + } - // distance from unioned - shoot ray? - if (segment_to_input_facet.find(*q)->second.size() == 2) { - for (auto& bnd : inner_offset) { - // 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) { - closest_segment = seg; - closest_intersection_point = *xp; - sq_distance_along_ray = dist; + if (q == nullptr) { + continue; + } + + bool handled_as_graph_path = false; + + // distance from unioned - shoot ray? + if (segment_to_input_facet.find(*q)->second.size() == 2) { + 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_projection_distance * max_projection_distance)) { + closest_segment = seg; + closest_intersection_point = *xp; + sq_distance_along_ray = dist; + } else { + + } + } } } } - } - if (closest_intersection_point) { - constructed_segments.push_front({M, *closest_intersection_point}); - break; + if (closest_intersection_point) { + constructed_segments.push_front({M, *closest_intersection_point}); + processed_vertices.insert(M); + break; #if 0 Graph2D GGG(bnd); GGG.refine(*GGG.query(*closest_intersection_point, 0.01), *closest_intersection_point); @@ -1217,12 +1985,35 @@ std::list> extend_end_vertices_based_on_input( break; } #endif - } else { - std::cerr << "Warning: no intersection found when extending end vertex, this will likely result in invalid topology" << std::endl; + } 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_point) { + constructed_segments.push_front({M, *closest_point}); + processed_vertices.insert(M); + } + } } } } - } #if 0 if (!handled_as_graph_path) { @@ -1267,6 +2058,11 @@ std::list> extend_end_vertices_based_on_input( constructed_segments.push_front({avg, R}); } #endif + } + } + + if (!broke_out) { + break; } } @@ -1355,8 +2151,6 @@ void fuse_corridor_halves_with_input(Arrangement_2& arr, Graph2D& G, SegmentL } } -#include - class Segment_2_less { public: bool operator()(const Segment_2& a, const Segment_2& b) const { @@ -1367,7 +2161,112 @@ class Segment_2_less { } }; -void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { +std::vector arrangement_cell_iou(Arrangement_2& left, Arrangement_2& right) { + + using Walk_pl = CGAL::Arr_walk_along_line_point_location; + Walk_pl walk_pl(right); + + std::set visited_faces_on_right; + + std::vector return_values; + + for (auto it = left.faces_begin(); it != left.faces_end(); ++it) { + if (!it->is_unbounded()) { + // convert arr facet to polygon with holes + auto polygon_exterior = circ_to_poly(it->outer_ccb()); + Polygon_with_holes_2 pwh(polygon_exterior); + for (auto hit = it->inner_ccbs_begin(); hit != it->inner_ccbs_end(); ++hit) { + pwh.add_hole(circ_to_poly(*hit)); + } + + CGAL::Polygon_triangulation_decomposition_2 decompositor; + std::vector temp; + decompositor(pwh, std::back_inserter(temp)); + + std::set visited_points; + + while (true) { + // select triangle edge that has largest squared edge length times distance from polygon exterior + K::FT max_score = -std::numeric_limits::infinity(); + Point_2 best_point; + for (auto& tri : temp) { + for (size_t i = 0; i < 3; ++i) { + size_t j = (i + 1) % 3; + auto& pi = tri.vertex(i); + auto& pj = tri.vertex(j); + + auto center_point = CGAL::ORIGIN + (((pi - CGAL::ORIGIN) + (pj - CGAL::ORIGIN)) / 2); + + K::FT min_dist = std::numeric_limits::infinity(); + for (auto eit = polygon_exterior.edges_begin(); eit != polygon_exterior.edges_end(); ++eit) { + auto ep = eit->source(); + auto eq = eit->target(); + Segment_2 seg(ep, eq); + auto dist = CGAL::squared_distance(center_point, seg); + if (dist < min_dist) { + min_dist = dist; + } + } + + auto sq_length = CGAL::squared_distance(pi, pj); + + auto score = sq_length * min_dist; + if (score > max_score && visited_points.count(center_point) == 0) { + max_score = score; + best_point = center_point; + } + } + } + + auto res = walk_pl.locate(best_point); + if (auto* v = variant_get(&res)) { + if (visited_faces_on_right.count(*v) > 0) { + return_values.push_back(0); + } else { + // convert arr facet to polygon with holes + auto polygon_exterior = circ_to_poly((*v)->outer_ccb()); + Polygon_with_holes_2 pwh_right(polygon_exterior); + for (auto hit = (*v)->inner_ccbs_begin(); hit != (*v)->inner_ccbs_end(); ++hit) { + pwh_right.add_hole(circ_to_poly(*hit)); + } + + // compute intersection over union of pwh and the original polygon + if (CGAL::do_intersect(pwh, pwh_right)) { + std::vector result; + CGAL::intersection(pwh, pwh_right, std::back_inserter(result)); + typename K::FT intersection_area = 0; + for (auto& r : result) { + auto poly_area = r.outer_boundary().area(); + for (auto& h : r.holes()) { + poly_area -= h.area(); + } + intersection_area += poly_area; + } + CGAL::Polygon_with_holes_2 poly12; + CGAL::join(pwh, pwh_right, poly12); + typename K::FT union_area = poly12.outer_boundary().area(); + for (auto& h : poly12.holes()) { + union_area -= h.area(); + } + return_values.push_back(intersection_area / union_area); + } else { + return_values.push_back(0); + } + } + visited_faces_on_right.insert(*v); + break; + } else { + // Not in facet on right, retry another point + continue; + } + } + } + } + + return return_values; +} + +void clean_noisy_paths(DebugWriter& debug_output, Arrangement_2& arr, SegmentLookup& segment_lookup, double& threshold) { using SK = CGAL::Simple_cartesian; CGAL::Cartesian_converter C{}; @@ -1418,7 +2317,7 @@ void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { auto [dv, dl] = get_dir(s); best = std::min(best, angle(dv)); } - return (best + 0.1) / own_length; + return (best + 0.01) / own_length; }; std::map badnesses; @@ -1426,7 +2325,6 @@ void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { badnesses[e] = edge_badness(e); } - double thr; { std::vector tmp; tmp.reserve(badnesses.size()); @@ -1435,12 +2333,12 @@ void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { } std::nth_element(tmp.begin(), tmp.begin() + tmp.size() / 2, tmp.end()); double med = tmp[tmp.size() / 2]; - thr = 10.0 * med; + threshold = 4.0 * med; } std::set bad_edges; for (auto& p : badnesses) { - if (p.second > thr) { + if (p.second > threshold) { bad_edges.insert(p.first); } } @@ -1568,7 +2466,46 @@ void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { return best_x; }; + auto process_modifications = [&]( + Arrangement_2& arr_, + const std::set>& to_remove_, + const std::vector>& to_insert_) { + for (auto& e : to_remove_) { + bool removed = false; + for (auto he = arr_.edges_begin(); he != arr_.edges_end(); ++he) { + auto a = he->source()->point(); + auto b = he->target()->point(); + if ((a == e.first && b == e.second) || (a == e.second && b == e.first)) { + CGAL::remove_edge(arr_, he); + removed = true; + break; + } + } + if (!removed) { + std::cerr << "Warning: unable to locate edge for removal, skipping" << std::endl; + } + } + + for (auto& pq : to_insert_) { + if (pq.first == pq.second) { + continue; + } + CGAL::insert(arr_, Segment_2(pq.first, pq.second)); + } + }; + + size_t path_index = 0; for (auto& path : bad_paths) { + decltype(to_remove) to_remove_this_path; + decltype(to_insert) to_insert_this_path; + + for (size_t i = 0; i < path.size() - 1; ++i) { + auto& a = path[i]; + auto& b = path[i + 1]; + + debug_output.write_segment(a, b, "arr_bad_path path_nr_" + std::to_string(path_index)); + } + auto x = collapse_path(path); if (!x) { // std::cerr << "Unable to collapse path, skipping" << std::endl; @@ -1589,12 +2526,10 @@ void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { continue; } - std::cerr << "new_length: " << new_length << " orig_length: " << orig_length << std::endl; - for (size_t i = 0; i < path.size(); ++i) { auto& v = path[i]; if (CGAL::squared_distance(v, *x) < 1.e-5) { - std::cerr << "Collapsing path would create near-duplicate vert to previous path, skipping" << std::endl; + // std::cerr << "Collapsing path would create near-duplicate vert to previous path, skipping" << std::endl; continue; } } @@ -1604,75 +2539,314 @@ void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { auto& b = path[i + 1]; if (a < b) { to_remove.insert({a, b}); + to_remove_this_path.insert({a, b}); } else { to_remove.insert({b, a}); + to_remove_this_path.insert({b, a}); } } auto s = path.front(); auto t = path.back(); if (s != *x) { to_insert.push_back({s, *x}); + to_insert_this_path.push_back({s, *x}); + + debug_output.write_segment(s, *x, "corrected_path path_nr_" + std::to_string(path_index)); } if (t != *x) { to_insert.push_back({t, *x}); + to_insert_this_path.push_back({t, *x}); + + debug_output.write_segment(t, *x, "corrected_path path_nr_" + std::to_string(path_index)); } + + path_index += 1; + +#if 1 + process_modifications(arr, to_remove_this_path, to_insert_this_path); +#else + auto arr_copy = arr; + process_modifications(arr_copy, to_remove_this_path, to_insert_this_path); + auto ious = arrangement_cell_iou(arr, arr_copy); + for (auto& iou : ious) { + std::cerr << " - cell iou: " << CGAL::to_double(iou) << std::endl; + } + std::swap(arr_copy, arr); +#endif } - /* - using Walk_pl = CGAL::Arr_walk_along_line_point_location; - Walk_pl walk_pl(arr); + process_modifications(arr, to_remove, to_insert); +} - for (auto& e : to_remove) { - // debug_output.write_segment(e->source()->point(), e->target()->point(), "arr_bad_remove"); - auto res = walk_pl.locate(e.first); - if (auto* v = boost::get(&res)) { - Arrangement_2::Halfedge_around_vertex_circulator first, curr; - first = curr = (*v)->incident_halfedges(); - size_t i = 0; - std::array pts; - std::array hes; - do { - Arrangement_2::Vertex_const_handle u = curr->source(); - hes[i] = curr; - pts[i++] = u->point(); - } while (++curr != first); +template +void next_circular(typename Vec::const_iterator& it, const Vec& vec) { + std::advance(it, 1); + if (it == vec.end()) { + it = vec.begin(); + } +} +template +void previous_circular(typename Vec::const_iterator& it, const Vec& vec) { + if (it == vec.begin()) { + it = vec.end(); + } + std::advance(it, -1); +} - if ((*v)->point() != e.first) { - std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; - continue; +template +std::size_t circular_distance(typename Vec::const_iterator first, + typename Vec::const_iterator last, + const Vec& vec) { + if (first <= last) { + return static_cast(last - first); + } + return static_cast(vec.end() - first) + static_cast(last - vec.begin()); +} + +template +std::pair +longest_wrapping_true_run(const Vec& v, Pred pred) { + using It = typename Vec::const_iterator; + + const auto n = v.size(); + if (n == 0) { + return {v.end(), v.end()}; + } + + // Find best non-wrapping run + std::size_t best_len = 0; + std::size_t best_start = 0; + + std::size_t curr_len = 0; + std::size_t curr_start = 0; + + for (std::size_t i = 0; i < n; ++i) { + if (pred(v[i])) { + if (curr_len == 0) { + curr_start = i; + } + ++curr_len; + if (curr_len > best_len) { + best_len = curr_len; + best_start = curr_start; } } else { - std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; - continue; + curr_len = 0; } } - */ - for (auto& e : to_remove) { - bool removed = false; - for (auto he = arr.edges_begin(); he != arr.edges_end(); ++he) { - auto a = he->source()->point(); - auto b = he->target()->point(); - if ((a == e.first && b == e.second) || (a == e.second && b == e.first)) { - CGAL::remove_edge(arr, he); - removed = true; - break; + // Count leading true + std::size_t leading = 0; + while (leading < n && pred(v[leading])) { + ++leading; + } + + // All true + if (leading == n) { + return {v.begin(), v.end()}; + } + + // Count trailing true + std::size_t trailing = 0; + while (trailing < n && pred(v[n - 1 - trailing])) { + ++trailing; + } + + // Wrapped run = [n - trailing, n) + [0, leading) + const std::size_t wrapped_len = leading + trailing; + + if (wrapped_len > best_len) { + It first = v.begin() + static_cast(n - trailing); + It last = v.begin() + static_cast(leading); + return {first, last}; + } + + It first = v.begin() + static_cast(best_start); + It last = first + static_cast(best_len); + return {first, last}; +} + +void clean_noisy_bounds(DebugWriter& debug_output, Arrangement_2& arr, SegmentLookup& segment_lookup, double threshold) { + using SK = CGAL::Simple_cartesian; + CGAL::Cartesian_converter C{}; + + auto other = [](const Segment_2& e, const Point_2& v) { + return (e.source() == v) ? e.target() : e.source(); + }; + + auto edge_badness = [&](const Segment_2& e) -> double { + auto closest = segment_lookup.n_closest_input_segments(e, 2); + if (closest.size() != 2) { + throw std::runtime_error("Unable to locate two nearby edges"); + } + + auto get_dir = [&](const Segment_2& s) { + auto a = C(s.source()); + auto b = C(s.target()); + SK::Vector_2 v = b - a; + double l = std::sqrt(v.squared_length()); + if (l <= 1e-12) { + return std::make_pair(SK::Vector_2(0, 0), 0.); + } + return std::make_pair(v / l, l); + }; + + auto [own_dir, own_length] = get_dir(e); + + auto angle = [&](const SK::Vector_2& ov) { + double d = std::abs(own_dir * ov); + if (d > 1.0) { + d = 1.0; + } + return std::acos(d); + }; + + double best = std::numeric_limits::infinity(); + for (auto& s : closest) { + auto [dv, dl] = get_dir(s); + best = std::min(best, angle(dv)); + } + return (best + 0.01) / own_length; + }; + + size_t facet_index = 0; + for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it, ++facet_index) { + if (!it->is_unbounded()) { + std::set> to_remove; + std::vector> to_insert; + + std::vector segs; + std::vector vertices; + std::vector halfedges; + + auto circ = it->outer_ccb(); + do { + auto a = circ->source()->point(); + auto b = circ->target()->point(); + segs.emplace_back(a, b); + vertices.push_back(circ->source()); + halfedges.push_back(circ); + ++circ; + } while (circ != it->outer_ccb()); + + std::vector badnesses; + for (auto& e : segs) { + badnesses.push_back(edge_badness(e)); + } + + auto bit = std::min_element(badnesses.begin(), badnesses.end()); + if (*bit > threshold) { + // std::cerr << "All edges are good, skipping" << std::endl; + continue; + } + + auto it_pair = longest_wrapping_true_run(badnesses, [&](double d) { return d > threshold; }); + auto N = circular_distance(it_pair.first, it_pair.second, badnesses); + + if (N == 0) { + // std::cerr << "Unable to find run of bad edges, skipping" << std::endl; + continue; + } + + std::vector> incoming_paths; + + auto jt = it_pair.first; + for (std::size_t k = 0; k < N; ++k, next_circular(jt, badnesses)) { + + auto he = halfedges[std::distance(badnesses.cbegin(), jt)]; + to_remove.insert({he->source()->point(), he->target()->point()}); + debug_output.write_segment(he->source()->point(), he->target()->point(), "arr_bad_bound facet_" + std::to_string(facet_index)); + + Arrangement_2::Vertex_handle v = he->source(); + + // circle around other edges onto v + Arrangement_2::Halfedge_around_vertex_circulator first, curr; + first = curr = v->incident_halfedges(); + do { + Arrangement_2::Vertex_handle u = curr->source(); + if (curr->face() != it && curr->twin()->face() != it) { + + // loop until we find a 3-degree vertex, or we come back to the start + std::vector path{v->point(), u->point()}; + auto he = curr; + + while (u->degree() == 2 && u != v && path.size() < 10) { + std::vector hes; + + { + Arrangement_2::Halfedge_around_vertex_circulator first, curr; + first = curr = u->incident_halfedges(); + do { + hes.push_back(curr); + curr++; + } while (curr != first); + } + + auto next_he = hes.front() != he && hes.front() != he->twin() ? hes.front() : hes.back(); + auto next_v = next_he->target() != u ? next_he->target() : next_he->source(); + + path.push_back(next_v->point()); + u = next_v; + } + incoming_paths.push_back(std::move(path)); + } + } while (++curr != first); + } + + const std::size_t start = + static_cast(std::distance(badnesses.cbegin(), it_pair.first)); + + auto n = badnesses.size(); + + auto wrap = [n](std::ptrdiff_t i) -> std::size_t { + i %= static_cast(n); + if (i < 0) { + i += static_cast(n); + } + return static_cast(i); + }; + + const std::size_t ib = start; + const std::size_t ia = wrap(static_cast(start) - 1); + const std::size_t ic = wrap(static_cast(start + N)); + const std::size_t id = wrap(static_cast(start + N + 1)); + + auto a = vertices.begin() + static_cast(ia); + auto b = vertices.begin() + static_cast(ib); + auto c = vertices.begin() + static_cast(ic); + auto d = vertices.begin() + static_cast(id); + + CGAL::Ray_2 r1((*a)->point(), (*b)->point()); + CGAL::Ray_2 r2((*d)->point(), (*c)->point()); + + auto x = CGAL::intersection(r1, r2); + if (x) { + if (auto* xp = variant_get>(&*x)) { + to_insert.emplace_back((*b)->point(), *xp); + to_insert.emplace_back((*c)->point(), *xp); + + debug_output.write_segment((*b)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); + debug_output.write_segment((*c)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); + } + } else { + CGAL::Line_2 r1((*a)->point(), (*b)->point()); + CGAL::Line_2 r2((*d)->point(), (*c)->point()); + + auto x = CGAL::intersection(r1, r2); + if (x) { + if (auto* xp = variant_get>(&*x)) { + to_insert.emplace_back((*b)->point(), *xp); + to_insert.emplace_back((*c)->point(), *xp); + + debug_output.write_segment((*b)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); + debug_output.write_segment((*c)->point(), *xp, "corrected_bound facet_" + std::to_string(facet_index)); + } + } } } - if (!removed) { - std::cerr << "Warning: unable to locate edge for removal, skipping" << std::endl; - } } - - for (auto& pq : to_insert) { - if (pq.first == pq.second) { - continue; - } - CGAL::insert(arr, Segment_2(pq.first, pq.second)); - // debug_output.write_segment(pq.first, pq.second, "arr_bad_insert"); - } - } void remove_colinear_vertices(Arrangement_2& arr) { @@ -1725,20 +2899,31 @@ class timer { public: class entry { public: + entry() {} + entry(std::map::const_iterator start_it) : start_it(start_it) {} + void stop() { - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration(end - start_it->second).count(); - std::cerr << "Timing for " << start_it->first << ": " << duration << " ms" << std::endl; + if (start_it) { + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration(end - start_it.value()->second).count(); + std::cerr << "Timing for " << start_it.value()->first << ": " << duration << " ms" << std::endl; + } } private: - std::map::const_iterator start_it; + std::optional::const_iterator> start_it; }; + timer(bool enabled = true) : enabled_(enabled) {} + entry start(const std::string& name) { - return entry(timings_.insert({name, std::chrono::high_resolution_clock::now()}).first); + if (enabled_) { + return entry(timings_.insert({name, std::chrono::high_resolution_clock::now()}).first); + } else { + return entry(); + } } private: @@ -1746,27 +2931,30 @@ class timer { std::string, std::chrono::high_resolution_clock::time_point> timings_; + + bool enabled_; }; -void arrange_cgal_polygons(const std::vector& input_polygons_, std::vector& output_polygons, double polygon_offset_distance = -1.) { +void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std::vector& input_polygons_, std::vector& output_polygons, double polygon_offset_distance = -1.) { static const double OVERLAP_RESOLUTION_DISTANCE = 1.e-1; // even larger amount of inset so that outer perimeter is safely within all input polygons even when overlap resolution is applied // no, `1.e-2 + 1.e-5` creates issues with the outer perimeter, are there other tolerances in play? static const double OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT = 1.e-5; -#ifdef SVGFILL_DEBUG - auto t = std::time(nullptr); - auto tm = *std::localtime(&t); + DebugWriter debug_output; + if (settings.debug_output) { + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); - std::ostringstream oss; - oss << std::put_time(&tm, "arrangement_%Y%m%d%H%M%S"); - auto now = oss.str(); - DebugWriter debug_output(true, now); -#else - DebugWriter debug_output(false, ""); -#endif + std::ostringstream oss; + oss << std::put_time(&tm, "arrangement_%Y%m%d%H%M%S"); + auto now = oss.str(); + debug_output = DebugWriter(true, now); + } else { + debug_output = DebugWriter(false, ""); + } - timer timer; + timer timer(settings.debug_output); auto t0 = timer.start("input"); @@ -1794,7 +2982,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v t0.stop(); t0 = timer.start("overlap elimination"); - eliminate_overlaps(OVERLAP_RESOLUTION_DISTANCE, input_polygons); + eliminate_overlaps(debug_output, OVERLAP_RESOLUTION_DISTANCE, input_polygons); t0.stop(); @@ -1813,79 +3001,79 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v debug_output.write_polygons(input_polygons, "processed_input"); -#if 1 - t0 = timer.start("outer perimeter"); - - // Find the outer perimeter using offset - union - negative offset - std::vector offset_polygons; - for (auto& r : input_polygons) { - auto R = r; - if (!R.is_counterclockwise_oriented()) { - R.reverse_orientation(); - } - - // Overlap removal can also result in close points causing problems when converted into non-exact nt - remove_close_points(R); - - auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); - for (auto& p : ps) { - if (!p.is_simple()) { - throw std::runtime_error("Complex polygon originated from offset"); - } - } - offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); - } - - debug_output.write_polygons(offset_polygons, "offset_input"); - - // Perform Boolean union on the offset polygons - std::vector unioned_polygons; - CGAL::join(offset_polygons.begin(), offset_polygons.end(), std::back_inserter(unioned_polygons)); - - if (unioned_polygons.size() > 1) { - // @todo this is currently one of the major limitations in the code that still can be eliminated - // by grouping the input polygons by their perimiter polygon in unioned_polygons - std::sort(unioned_polygons.begin(), unioned_polygons.end(), [](auto& p, auto& q) { return p.outer_boundary().area() > q.outer_boundary().area(); }); - } - - debug_output.write_polygon(unioned_polygons.front().outer_boundary(), "offset_joined"); - - Polygon_2 fused_removed_close_points = unioned_polygons.front().outer_boundary(); - remove_close_points(fused_removed_close_points, 1.e-4); - - // Apply negative offset to get the outer perimeter polygon - auto outer_perimiter = create_and_convert_offset_polygon( - // Because polygon_offset is inexact, make sure our inset distance is slightly larger - // std::nexttoward(-polygon_offset_distance, -std::numeric_limits::infinity()), - - // 1.e-8 even was too little and still resulted in slivers of triangle around the perimeter - -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, - fused_removed_close_points); - - debug_output.write_polygons(outer_perimiter, "outer_perimiter"); -#else - std::map> neighbour_map; - build_radial_neighbour_map(input_polygons, polygon_offset_distance, neighbour_map); - - auto start_vertex = neighbour_map.rbegin()->first; - auto next_vertex = neighbour_map.rbegin()->second.front(); - - std::vector cycle = {start_vertex, next_vertex}; - while (cycle.back() != cycle.front()) { - const auto& incoming_from = *(cycle.rbegin() + 1); - const auto& nb = neighbour_map[cycle.back()]; - auto it = std::find(nb.begin(), nb.end(), incoming_from); - // cycle it -1 around nb - if (it == nb.begin()) { - it == nb.end() - 1; - } else { - --it; - } - cycle.push_back(*it); - } std::vector outer_perimiter; - outer_perimiter.emplace_back(cycle.begin(), cycle.end()); -#endif + if (settings.outer_perimiter_algo == 0) { + t0 = timer.start("outer perimeter"); + + // Find the outer perimeter using offset - union - negative offset + std::vector offset_polygons; + for (auto& r : input_polygons) { + auto R = r; + if (!R.is_counterclockwise_oriented()) { + R.reverse_orientation(); + } + + // Overlap removal can also result in close points causing problems when converted into non-exact nt + remove_close_points(R); + + auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); + for (auto& p : ps) { + if (!p.is_simple()) { + throw std::runtime_error("Complex polygon originated from offset"); + } + } + offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); + } + + debug_output.write_polygons(offset_polygons, "offset_input"); + + // Perform Boolean union on the offset polygons + std::vector unioned_polygons; + CGAL::join(offset_polygons.begin(), offset_polygons.end(), std::back_inserter(unioned_polygons)); + + if (unioned_polygons.size() > 1) { + // @todo this is currently one of the major limitations in the code that still can be eliminated + // by grouping the input polygons by their perimiter polygon in unioned_polygons + std::sort(unioned_polygons.begin(), unioned_polygons.end(), [](auto& p, auto& q) { return p.outer_boundary().area() > q.outer_boundary().area(); }); + } + + debug_output.write_polygon(unioned_polygons.front().outer_boundary(), "offset_joined"); + + Polygon_2 fused_removed_close_points = unioned_polygons.front().outer_boundary(); + remove_close_points(fused_removed_close_points, 1.e-4); + + // Apply negative offset to get the outer perimeter polygon + outer_perimiter = create_and_convert_offset_polygon( + // Because polygon_offset is inexact, make sure our inset distance is slightly larger + // std::nexttoward(-polygon_offset_distance, -std::numeric_limits::infinity()), + + // 1.e-8 even was too little and still resulted in slivers of triangle around the perimeter + -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, + fused_removed_close_points); + + debug_output.write_polygons(outer_perimiter, "outer_perimiter"); + } else { + std::map> neighbour_map; + build_radial_neighbour_map(input_polygons, polygon_offset_distance, neighbour_map); + + auto start_vertex = neighbour_map.rbegin()->first; + auto next_vertex = neighbour_map.rbegin()->second.front(); + + std::vector cycle = {start_vertex, next_vertex}; + while (cycle.back() != cycle.front()) { + const auto& incoming_from = *(cycle.rbegin() + 1); + const auto& nb = neighbour_map[cycle.back()]; + auto it = std::find(nb.begin(), nb.end(), incoming_from); + // cycle it -1 around nb + if (it == nb.begin()) { + it = nb.end() - 1; + } else { + --it; + } + cycle.push_back(*it); + } + outer_perimiter.emplace_back(cycle.begin(), cycle.end()); + } t0.stop(); t0 = timer.start("corridor creation"); @@ -1911,8 +3099,10 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // subdivide difference_result to have better more detailed triangulation and therefore less-pronounced artefacts in midpoint network + auto subdivision_length = polygon_offset_distance / settings.subdivision_factor; + for (auto& pwh : difference_result) { - difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 8., pwh)); + difference_result_subdivided.push_back(subdivide_polygon(subdivision_length, pwh)); // difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 64., pwh)); } @@ -1940,7 +3130,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v SegmentLookup segment_lookup(input_polygons); - auto [line_graph, midpoint_to_segment, segment_to_input_facet] = build_line_graph(input_polygons, segment_lookup, triangular_polygons); + auto [line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length] = build_line_graph(input_polygons, segment_lookup, triangular_polygons); for (auto& p : line_graph) { for (auto& q : p.second) { debug_output.write_segment(p.first, q, "network_1"); @@ -1950,38 +3140,48 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v t0.stop(); t0 = timer.start("center line cleaning"); - - auto triangles = find_triangles(line_graph); - // For every triangle found in the network we eliminate one edge to break the cycle - // The edge we eliminate is the edge with the greatest angle with any of it's neighbours - auto eliminated_segments = eliminate_triangles(line_graph); + Graph2D G; + if (settings.line_cleaning_algo == 0) { + G = join_segment_runs(debug_output, line_graph, midpoint_to_edge_length); + Arrangement_2 arr; + G.to_arrangement(arr); + Graph2D G2; + G2.from_arrangement(arr); + 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"); + } + } else { + auto eliminated_segments = eliminate_triangles(line_graph); - Graph2D G2(line_graph); - for (auto& e : eliminated_segments) { - debug_output.write_segment(e.first, e.second, "eliminated"); - G2.remove_edge(e.first, e.second); - } + Graph2D G2(line_graph); + for (auto& e : eliminated_segments) { + debug_output.write_segment(e.first, e.second, "eliminated"); + G2.remove_edge(e.first, e.second); + } - auto G = G2.weld_vertices(); + 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"); - } + for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { + debug_output.write_segment(it->first, it->second, "network_2"); + } - eliminate_colinear_vertices(G); + eliminate_colinear_vertices(G); - edge_slide(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"); + for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { + debug_output.write_segment(it->first, it->second, "network_3"); + } } t0.stop(); t0 = timer.start("topology"); - auto segments = extend_end_vertices_based_on_input(G, midpoint_to_segment, segment_to_input_facet, outer_perimiter, segment_lookup); + auto segments = extend_end_vertices_based_on_input(G, midpoint_to_segment, segment_to_input_facet, outer_perimiter, segment_lookup, subdivision_length * 4); // Now plot the edges on an arrangement in order to find planar cycles // and merge the corridor-halves with their neighbouring input polygon @@ -1997,31 +3197,31 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v debug_output.write_segment(pq.first, pq.second, "extended_segments"); } -#if 0 - // Write input polygons to arrangement_2 - // We no longer do this because we add the outer perimiter now, subdivided by the corridor network which is extended and intersected with the outer perimiter - for (auto& poly : input_polygons) { - for (size_t i = 0; i != poly.size(); ++i) { - auto j = (i + 1) % poly.size(); - if (poly.vertex(i) == poly.vertex(j)) { - continue; + if (settings.topology_reconstruction_algo != 0) { + // Write input polygons to arrangement_2 + // We no longer do this because we add the outer perimiter now, subdivided by the corridor network which is extended and intersected with the outer perimiter + for (auto& poly : input_polygons) { + for (size_t i = 0; i != poly.size(); ++i) { + auto j = (i + 1) % poly.size(); + if (poly.vertex(i) == poly.vertex(j)) { + continue; + } + CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); + } + } + } else { + // Write outer perimeter to arrangement_2 + for (auto& p : outer_perimiter) { + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + auto source = it->source(); + auto target = it->target(); + if (source == target) { + continue; + } + CGAL::insert(arr, Segment_2(source, target)); } - CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); } } -#else - // Write outer perimeter to arrangement_2 - for (auto& p : outer_perimiter) { - for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { - auto source = it->source(); - auto target = it->target(); - if (source == target) { - continue; - } - CGAL::insert(arr, Segment_2(source, target)); - } - } -#endif // Just for the automatic numbering, create a full vector std::vector temp; @@ -2047,12 +3247,17 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // corridor network we know it needs to be joined with an input polygon. In that // case the edges need to be eliminated that correspond to original geometry. -#if 0 - fuse_corridor_halves_with_input(arr, G, segment_lookup, input_polygons, debug_output); -#else - remove_colinear_vertices(arr); - clean_noisy_paths(arr, segment_lookup); -#endif + if (settings.topology_reconstruction_algo != 0) { + fuse_corridor_halves_with_input(arr, G, segment_lookup, input_polygons, debug_output); + } + + if (settings.perform_cleanup && settings.line_cleaning_algo != 0) { + remove_colinear_vertices(arr); + double threshold; + clean_noisy_paths(debug_output, arr, segment_lookup, threshold); + remove_colinear_vertices(arr); + clean_noisy_bounds(debug_output, arr, segment_lookup, threshold); + } t0.stop(); @@ -2068,8 +3273,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v #ifndef SVGFILL_MAIN -bool svgfill::arrange_polygons(const std::vector& polygons, std::vector& arranged) -{ +bool svgfill::arrange_polygons(arrange_polygon_settings settings, const std::vector& polygons, std::vector& arranged) { std::vector cgal_polygons, cgal_polygons_out; std::transform(polygons.begin(), polygons.end(), std::back_inserter(cgal_polygons), [](auto& poly) { Polygon_2 result; @@ -2078,7 +3282,7 @@ bool svgfill::arrange_polygons(const std::vector& polygons, }); return result; }); - arrange_cgal_polygons(cgal_polygons, cgal_polygons_out); + arrange_cgal_polygons(settings, cgal_polygons, cgal_polygons_out); std::transform(cgal_polygons_out.begin(), cgal_polygons_out.end(), std::back_inserter(arranged), [](auto& poly) { svgfill::polygon_2 result; std::transform(poly.begin(), poly.end(), std::back_inserter(result.boundary), [](auto& pt) { @@ -2128,7 +3332,7 @@ int main(int argc, char** argv) { input_polygons.back().push_back(CGAL::Point_2(x, y)); } } - arrange_cgal_polygons(input_polygons, output); + arrange_cgal_polygons(arrange_polygon_settings{}, input_polygons, output); break; } return 0; @@ -2141,7 +3345,7 @@ int main(int argc, char** argv) { input_polygons = { rect1, rect2, rect3, rect4, rect5 }; } - arrange_cgal_polygons(input_polygons, output); + arrange_cgal_polygons(arrange_polygon_settings{}, input_polygons, output); return 0; } diff --git a/src/svgfill/src/graph_2d.h b/src/svgfill/src/graph_2d.h index da2b5014ec..d19418ff62 100644 --- a/src/svgfill/src/graph_2d.h +++ b/src/svgfill/src/graph_2d.h @@ -346,6 +346,13 @@ public: } } + template + void from_arrangement(T& arr) { + for (auto it = arr.edges_begin(); it != arr.edges_end(); ++it) { + insert(it->source()->point(), it->target()->point()); + } + } + void assert_symmetric() { #ifdef SVGFILL_DEBUG #if 0 diff --git a/src/svgfill/src/svgfill.cpp b/src/svgfill/src/svgfill.cpp index 8a2a1bb008..cf45881c93 100644 --- a/src/svgfill/src/svgfill.cpp +++ b/src/svgfill/src/svgfill.cpp @@ -483,6 +483,18 @@ public: return ps; } + size_t delete_same_facet_edge_pairs() { + size_t n_deleted = 0; + for (auto it = arr.edges_begin(); it != arr.edges_end();) { + decltype(it) current = it++; + if (current->face() == current->twin()->face()) { + arr.remove_edge(current); + n_deleted++; + } + } + return n_deleted; + } + void merge(const std::vector& edge_indices) { if (edge_indices.empty()) { return; diff --git a/src/svgfill/src/svgfill.h b/src/svgfill/src/svgfill.h index 396fc9c924..2588636a8d 100644 --- a/src/svgfill/src/svgfill.h +++ b/src/svgfill/src/svgfill.h @@ -67,6 +67,7 @@ namespace svgfill { virtual std::vector get_face_pairs() = 0; virtual size_t num_edges() = 0; virtual size_t num_faces() = 0; + virtual size_t delete_same_facet_edge_pairs() = 0; }; class SVGFILL_API context { @@ -101,6 +102,7 @@ namespace svgfill { void write(std::vector>&); size_t num_edges() { return arr_->num_edges(); } size_t num_faces() { return arr_->num_faces(); } + size_t delete_same_facet_edge_pairs() { return arr_->delete_same_facet_edge_pairs(); } ~context() { delete arr_; @@ -113,7 +115,25 @@ namespace svgfill { SVGFILL_API std::string polygons_to_svg(const std::vector>& polygons, bool random_color=false); SVGFILL_API std::string polygons_to_svg(const std::vector& polygons, bool random_color = false); SVGFILL_API bool svg_to_polygons(const std::string& data, const boost::optional& class_name, std::vector& polygons); - SVGFILL_API bool arrange_polygons(const std::vector& polygons, std::vector& arranged); -} + + struct SVGFILL_API arrange_polygon_settings { + bool debug_output = false; + // -1: compute from average edge length + double polygon_offset_distance = -1.; + // 0: use offset - union - negative offset to find the outer perimeter + // 1: radial walk along vertices; exact, but can only reuse vertices, not create new positions by means of intersections + int outer_perimiter_algo = 0; + // 0: outer perimiter and corridor center lines + // 1: input polygons, corridor center lines and segments connecting corridor center lines to input polygons + int topology_reconstruction_algo = 0; + // 0: join segment runs + // 1: local badness reduction + int line_cleaning_algo = 0; + bool perform_cleanup = true; + double subdivision_factor = 16.; + }; + + SVGFILL_API bool arrange_polygons(arrange_polygon_settings settings, const std::vector& polygons, std::vector& arranged); + } #endif From e7db239647d98580dbb30c3af1bf68a66158f64c Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 10 Apr 2026 21:45:21 +0200 Subject: [PATCH 24/25] inverse access in schema --- src/ifcparse/IfcSchema.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcparse/IfcSchema.h b/src/ifcparse/IfcSchema.h index 3dedd47a8e..349a81532d 100644 --- a/src/ifcparse/IfcSchema.h +++ b/src/ifcparse/IfcSchema.h @@ -358,6 +358,7 @@ class IFC_PARSE_API entity : public declaration { const std::vector& subtypes() const { return subtypes_; } const std::vector& attributes() const { return attributes_; } + const std::vector& inverse_attributes() const { return inverse_attributes_; } const std::vector& derived() const { return derived_; } const std::vector all_attributes() const { From 002b7c5d6ee97075eff7e46106c27c8ae0551a4f Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 10 Apr 2026 22:09:56 +0100 Subject: [PATCH 25/25] ifcquery, ifcmcp: better bot selector syntax hints --- src/ifcmcp/ifcmcp/core.py | 14 ++++++++++++-- src/ifcquery/ifcquery/select.py | 11 ++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/ifcmcp/ifcmcp/core.py b/src/ifcmcp/ifcmcp/core.py index ed9f91c3c9..137cdbdce0 100644 --- a/src/ifcmcp/ifcmcp/core.py +++ b/src/ifcmcp/ifcmcp/core.py @@ -278,7 +278,12 @@ class IfcSession: return info.info(model, element) def ifc_select(self, query: str) -> list[dict[str, Any]]: - """Filter elements using ifcopenshell selector syntax (e.g. 'IfcWall', 'IfcWindow').""" + """Filter elements using ifcopenshell selector syntax. + + Examples: ``IfcWall``, ``IfcWall, IfcColumn``, ``! IfcWall``, + ``IfcWall, Name = "My Wall"``, ``type = "Concrete Wall"``, + ``material = "Concrete"``. + """ return select.select(self._require_model(), query) def ifc_relations(self, element_id: int, traverse: str = "") -> dict[str, Any] | list[dict[str, Any]]: @@ -536,7 +541,12 @@ class IfcSession: { "type": "function", "name": "ifc_select", - "description": "Select elements using ifcopenshell selector syntax (e.g. 'IfcWall').", + "description": ( + "Select elements using ifcopenshell selector syntax. " + "Examples: 'IfcWall', 'IfcWall, IfcColumn', '! IfcWall', " + "'IfcWall, Name = \"My Wall\"', 'type = \"Concrete Wall\"', " + "'material = \"Concrete\"'." + ), "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, diff --git a/src/ifcquery/ifcquery/select.py b/src/ifcquery/ifcquery/select.py index c28a159b90..f3ee1dceaa 100644 --- a/src/ifcquery/ifcquery/select.py +++ b/src/ifcquery/ifcquery/select.py @@ -26,7 +26,16 @@ import ifcopenshell.util.selector def select(model: ifcopenshell.file, query: str) -> list[dict[str, Any]]: - """Filter elements using selector syntax and return matching element summaries.""" + """Filter elements using ifcopenshell selector syntax and return matching element summaries. + + Examples: + - ``IfcWall`` — all walls + - ``IfcWall, IfcColumn`` — walls and columns + - ``! IfcWall`` — everything except walls + - ``IfcWall, Name = "My Wall"`` — walls with a specific name attribute + - ``type = "Concrete Wall"`` — elements assigned that type product + - ``material = "Concrete"`` — elements with that material + """ elements = ifcopenshell.util.selector.filter_elements(model, query) results = [] for element in sorted(elements, key=lambda e: e.id()):