From f5686fff2619578b5a3fb486cdb599f6e74e6566 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 10 Feb 2026 09:25:02 +0100 Subject: [PATCH 001/131] Simplify destructor by removing null check #7650 --- src/ifcgeom/Converter.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ifcgeom/Converter.cpp b/src/ifcgeom/Converter.cpp index 6b70ac7f51..a50a43cc66 100644 --- a/src/ifcgeom/Converter.cpp +++ b/src/ifcgeom/Converter.cpp @@ -12,11 +12,8 @@ ifcopenshell::geometry::Converter::Converter(std::unique_ptrsettings(); } -ifcopenshell::geometry::Converter::~Converter() -{ - if (mapping_ != nullptr) { - delete mapping_; - } +ifcopenshell::geometry::Converter::~Converter() { + delete mapping_; } namespace { From c6072e416cf3cc05ade4fa669e2afece292ee607 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 10 Feb 2026 11:04:43 +0100 Subject: [PATCH 002/131] N-Section Lofting for Non-Polygonal (Curved) Shapes #7658 --- src/ifcgeom/kernels/opencascade/loft.cpp | 100 +++++++++++++++-------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp index 60997d24ad..74c9c187df 100644 --- a/src/ifcgeom/kernels/opencascade/loft.cpp +++ b/src/ifcgeom/kernels/opencascade/loft.cpp @@ -82,49 +82,79 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re } if (non_polygonal) { - if (loft->children.size() == 2) { - BRep_Builder BB; - TopoDS_Shell comp; - BB.MakeShell(comp); + if (loft->children.size() < 2) { + Logger::Error("Not enough sections to loft"); + return false; + } + std::vector> sections; + sections.reserve(loft->children.size()); - TopoDS_Shape f0, f1; - if (!convert(std::static_pointer_cast(loft->children.front()), f0) || - !convert(std::static_pointer_cast(loft->children.back()), f1)) - { + TopoDS_Shape f0, f1; + + // Convert all children to vectors of wires + for (const auto& child : loft->children) { + TopoDS_Shape shape; + if (!convert(std::static_pointer_cast(child), shape)) { + return false; + } + if (shape.ShapeType() != TopAbs_FACE) { + return false; + } + // At least make sure to have outer wire consistent, but in reality + // this is probably not a concern given how to build up these faces + auto f = TopoDS::Face(shape); + + if (child == loft->children.front()) { + f0 = f; + } else if (child == loft->children.back()) { + f1 = f; + } + + auto outer = BRepTools::OuterWire(f); + sections.emplace_back(); + sections.back().push_back(outer); + for (TopoDS_Iterator it(f); it.More(); it.Next()) { + if (outer != it.Value()) { + sections.back().push_back(TopoDS::Wire(it.Value())); + } + } + } + + auto first_wire_count = sections.front().size(); + for (auto& section : sections) { + if (section.size() != first_wire_count) { + Logger::Error("Inconsistent number of wires in sections"); return false; } - if (f0.ShapeType() != TopAbs_FACE || f1.ShapeType() != TopAbs_FACE) { + } + + BRep_Builder BB; + TopoDS_Shell comp; + BB.MakeShell(comp); + + for (size_t i = 0; i < first_wire_count; ++i) { + // Rule=True uses linear interpolation. + // This is critical for preventing twists in roads/railings. + BRepOffsetAPI_ThruSections builder(false, true); + for (auto& ws : sections) { + builder.AddWire(ws[i]); + } + builder.Build(); + if (!builder.IsDone()) { return false; } - - TopExp_Explorer exp1(f0, TopAbs_WIRE); - TopExp_Explorer exp2(f1, TopAbs_WIRE); - for (; exp1.More() && exp2.More(); exp1.Next(), exp2.Next()) { - const auto& w1 = TopoDS::Wire(exp1.Current()); - const auto& w2 = TopoDS::Wire(exp2.Current()); - BRepOffsetAPI_ThruSections builder; - builder.AddWire(w1); - builder.AddWire(w2); - builder.Build(); - if (!builder.IsDone()) { - return false; - } - for (TopExp_Explorer exp(builder.Shape(), TopAbs_FACE); exp.More(); exp.Next()) { - BB.Add(comp, exp.Current()); - } + for (TopExp_Explorer exp(builder.Shape(), TopAbs_FACE); exp.More(); exp.Next()) { + BB.Add(comp, exp.Current()); } - - BB.Add(comp, f0.Reversed()); - BB.Add(comp, f1); - - result = BRepBuilderAPI_MakeSolid(comp).Solid(); - - return true; - } else { - Logger::Error("Lofting more than two sections is not supported"); - return false; } + + BB.Add(comp, f0.Reversed()); + BB.Add(comp, f1); + + result = BRepBuilderAPI_MakeSolid(comp).Solid(); + + return true; } TopTools_ListOfShape faces; From e6780973da0d08771734aab5019998148706ef21 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 10 Feb 2026 14:01:10 +0100 Subject: [PATCH 003/131] arrange_poly: Refactor into logical blocks; add timing --- src/svgfill/src/arrange_polygons.cpp | 1778 ++++++++++++-------------- src/svgfill/src/graph_2d.h | 12 + 2 files changed, 825 insertions(+), 965 deletions(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 7bddc2e41a..0271d76146 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -164,31 +164,6 @@ boost::optional subtract_retain_largest(const T& lhs, const T& rhs) { return boost::none; } -// Function to write polygons as line segments in OBJ format -void write_polygon_to_obj(std::ofstream& ofs, size_t& vertex_index, bool as_line, const Polygon_2& polygon, const std::string& name) { - ofs << "o " << name << "\n"; // Object name - - // Write vertices - for (auto vit = polygon.vertices_begin(); vit != polygon.vertices_end(); ++vit) { - ofs << "v " << CGAL::to_double(vit->x()) << " " << CGAL::to_double(vit->y()) << " 0\n"; - } - - if (as_line) { - // Write line segments (edges) - for (size_t j = 0; j < polygon.size(); ++j) { - ofs << "l " << vertex_index + j << " " << vertex_index + (j + 1) % polygon.size() << "\n"; - } - } else { - ofs << "f"; - for (size_t j = 0; j < polygon.size(); ++j) { - ofs << " " << vertex_index + j; - } - ofs << "\n"; - } - - vertex_index += polygon.size(); -} - Polygon_2 circ_to_poly(typename Arrangement_2::Ccb_halfedge_const_circulator circ) { Polygon_2 poly; @@ -208,27 +183,6 @@ Polygon_with_holes_2 circ_to_poly(typename Arrangement_2::Ccb_halfedge_const_cir return poly; } -void write_polygon_to_svg(std::ostream& ofs, const Polygon_2& polygon) { - ofs << "x()) << "," << CGAL::to_double(vit->y()) << " "; - } - ofs << "\" style=\"fill:none;stroke-width:1\" />\n"; -} - -// Function to write a Polygon_with_holes_2 to an SVG file -void write_polygon_with_holes_to_svg(std::ostream& ofs, const Polygon_with_holes_2& polygon_with_holes) { - // Write the outer boundary (main polygon) - if (!polygon_with_holes.is_unbounded()) { - write_polygon_to_svg(ofs, polygon_with_holes.outer_boundary()); - } - - // Write the holes (if any) with a different color (e.g., red) - for (auto hit = polygon_with_holes.holes_begin(); hit != polygon_with_holes.holes_end(); ++hit) { - write_polygon_to_svg(ofs, *hit); - } -} - Polygon_2 fuse_with_offset(const std::vector& polygons, double polygon_offset_distance) { // Find the outer perimeter using offset - union - negative offset std::vector offset_polygons; @@ -285,78 +239,77 @@ Polygon_2 fuse_with_offset(const std::vector& polygons, double polygo return inner_offset.front(); } -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-2; - // 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; +double estimate_polygon_offset_distance(const std::vector& polygons) { + double total_edge_length = 0.; + size_t num_edges = 0; + for (auto& p : polygons) { + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + total_edge_length += std::sqrt(CGAL::to_double(CGAL::squared_distance(it->start(), it->end()))); + num_edges += 1; + } + } + return total_edge_length / num_edges / 2; +} - if (polygon_offset_distance < 0.) { - double total_edge_length = 0.; - size_t num_edges = 0; - for (auto& p : input_polygons_) { - for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { - total_edge_length += std::sqrt(CGAL::to_double(CGAL::squared_distance(it->start(), it->end()))); - num_edges += 1; +void clean_polygon(Polygon_2& poly) { + // Ensure counterclockwise orientation and remove duplicate last point if present also remove close points + if (!poly.is_counterclockwise_oriented()) { + poly.reverse_orientation(); + } + std::vector> ps(poly.begin(), poly.end()); + if (ps.front() == ps.back()) { + ps.pop_back(); + } + poly = Polygon_2(ps.begin(), ps.end()); + remove_close_points(poly); +} + +void smooth_polygon(double factor, Polygon_2& poly) { + auto ps = create_and_convert_offset_polygon(-factor, poly); + if (ps.size() == 1) { + auto r2 = ps.front(); + ps = create_and_convert_offset_polygon(+factor, r2); + if (ps.size() == 1) { + poly = ps.front(); + } + } +} + +template +void split_self_intersecting_polygon(const CGAL::Polygon_2& poly, OutIt output_it) { + if (poly.is_simple()) { + *output_it++ = poly; + return; + } + Arrangement_2 arr; + for (auto it = poly.edges_begin(); it != poly.edges_end(); ++it) { + CGAL::insert(arr, Segment_2(it->start(), it->end())); + } + for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { + if (it->is_unbounded()) { + for (auto jt = it->inner_ccbs_begin(); jt != it->inner_ccbs_end(); ++jt) { + auto inner = circ_to_poly(*jt); + // reverse because it's an inner bound to the infinite outer facet + inner.reverse_orientation(); + *output_it++ = inner; } } - polygon_offset_distance = total_edge_length / num_edges / 2; } +} - auto input_polygons__ = input_polygons_; - decltype(input_polygons__) input_polygons; - - for (auto& i : input_polygons__) { - std::vector> ps(i.begin(), i.end()); - if (ps.front() == ps.back()) { - ps.pop_back(); - } - input_polygons.emplace_back(ps.begin(), ps.end()); - } - - for (auto& polygon : input_polygons) { - if (!polygon.is_counterclockwise_oriented()) { - polygon.reverse_orientation(); - } - } - - for (auto& polygon : input_polygons) { - remove_close_points(polygon); - } - -#ifdef SVGFILL_DEBUG - std::ofstream obj("obj.obj"); - size_t vi = 1; - - std::ofstream svg("svg.svg"); - svg << "\n"; - - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "input_poly_" + std::to_string(std::distance(input_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); - } - - obj << std::flush; -#endif - +std::set> +find_overlaps(const std::vector& polygons) { typedef CGAL::Box_intersection_d::Box_with_handle_d Box; + std::vector boxes; + std::vector>> input_triangulated; - std::vector boxes; - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { + for (auto it = polygons.begin(); it != polygons.end(); ++it) { constexpr double offset = 1.e-3; auto b = it->bbox(); boxes.emplace_back( CGAL::Bbox_2(b.xmin() - offset, b.ymin() - offset, b.xmax() + offset, b.ymax() + offset), - std::distance(input_polygons.begin(), it) - ); - - if (!it->is_simple()) { -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, *it, "self-intersecting"); -#endif - throw std::runtime_error("Self-intersecting input"); - } + std::distance(polygons.begin(), it)); CGAL::Polygon_triangulation_decomposition_2 decompositor; std::vector temp; @@ -378,10 +331,8 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v bool registered_overlap = false; for (auto& t2 : input_triangulated[b.handle()]) { if (CGAL::squared_distance(t1, t2) < (1.e-3 * 1.e-3)) { - overlaps.insert({ - (a.handle() < b.handle()) ? a.handle() : b.handle(), - (a.handle() < b.handle()) ? b.handle() : a.handle() - }); + overlaps.insert({(a.handle() < b.handle()) ? a.handle() : b.handle(), + (a.handle() < b.handle()) ? b.handle() : a.handle()}); registered_overlap = true; break; } @@ -393,297 +344,319 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } }); - if (true) { - // solve overlaps by means of subtraction - // loop over overlaps and subtract the smaller polygon from the larger one + return overlaps; +} - std::set eliminated_polies; - std::map overlap_counts; - for (auto& p : overlaps) { - overlap_counts[p.first]++; - overlap_counts[p.second]++; +class DebugWriter { + public: + DebugWriter(bool enabled, const std::string& filename_prefix) + : enabled_(enabled) { + if (enabled_) { + obj.open(filename_prefix + ".obj"); + vi = 1; + svg.open(filename_prefix + ".svg"); + svg << "\n"; } - - for (const auto& edge : overlaps) { - // Skip eliminated - if (eliminated_polies.find(edge.first) != eliminated_polies.end() || - eliminated_polies.find(edge.second) != eliminated_polies.end()) { - continue; - } - - // Many overlaps indicate an aggregated polygon, skip them - /* - if (overlap_counts[edge.first] > 10 || overlap_counts[edge.second] > 10) { - if (overlap_counts[edge.first] > 10) { - eliminated_polies.insert(edge.first); - } - if (overlap_counts[edge.second] > 10) { - eliminated_polies.insert(edge.second); - } - continue; - } - */ - - // these are pointers now, because otherwise swap would not work? - auto* poly1 = &input_polygons[edge.first]; - auto* poly2 = &input_polygons[edge.second]; - - // Populate eliminated_polies with small polygons - // This can happen over time when modifications are made to the polygons to solve overlaps - bool skip = false; - if (poly1->area() < 1.e-2) { - eliminated_polies.insert(edge.first); - skip = true; - } - if (poly2->area() < 1.e-2) { - eliminated_polies.insert(edge.second); - skip = true; - } - // Small slivers are also just eliminated - if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly1))) { - eliminated_polies.insert(edge.first); - skip = true; - } - if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly2))) { - eliminated_polies.insert(edge.second); - skip = true; - } - if (skip) { - continue; - } - - // Skip polygons that have a very high intersection over union - // ratio, which indicates that they are very likely duplicates - if (CGAL::do_intersect(*poly1, *poly2)) { - std::vector result; - CGAL::intersection(*poly1, *poly2, 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(*poly1, *poly2, poly12); - typename K::FT union_area = poly12.outer_boundary().area(); - for (auto& h : poly12.holes()) { - union_area -= h.area(); - } - if (union_area > 0 && intersection_area / union_area > 0.99) { - // std::cerr << intersection_area / union_area << std::endl; - eliminated_polies.insert(edge.first); - continue; - } - } - - if (!(poly1->is_simple() && poly2->is_simple())) { - continue; - } - - { - std::vector result; - // std::cerr << poly1.area() << " " << poly2.area() << std::endl; - // std::cerr.flush(); - - boost::optional mp1, mp2, mp3, mp4; - bool swap = false; - - swap = poly1->area() <= poly2->area(); - if (swap) { - std::swap(poly1, poly2); - } - - bool success = false; - if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) { - if ((mp2 = subtract_retain_largest(*poly1, *mp1))) { - if ((mp3 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE * 2, *mp2)))) { - if ((mp4 = subtract_retain_largest(*poly2, *mp3))) { - *poly1 = *mp2; - *poly2 = *mp4; - success = true; - } - } - } - } - - /* - if (swap) { - // swap back to retain original ordering - // what's the point in swapping back here? - std::swap(poly1, poly2); - } - */ - - if (!success) { - eliminated_polies.insert(swap ? edge.first : edge.second); - continue; - } - } - } - - // iterate over the eliminated polygons and remove them from the input polygons - for (auto it = eliminated_polies.rbegin(); it != eliminated_polies.rend(); ++it) { - input_polygons.erase(input_polygons.begin() + *it); + } + ~DebugWriter() { + if (enabled_) { + svg << "\n"; + obj << std::flush; + obj.close(); + svg.close(); } } + void write_polygon(const Polygon_2& polygon, const std::string& name) { + if (enabled_) { + write_polygon_to_obj_(obj, vi, true, polygon, name); + write_polygon_to_svg_(svg, polygon, name); + obj << std::flush; + } + } + + void write_segment(const Point_2& p, const Point_2& q, const std::string& name) { + if (enabled_) { + if (last_segment_name_ != name) { + last_segment_name_ = name; + obj << "o " << name << "\n"; + } + obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; + obj << "v " << CGAL::to_double(q.x()) << " " << CGAL::to_double(q.y()) << " 0\n"; + obj << "l " << vi++; + obj << " " << vi++ << "\n"; + + svg << ""; + + obj << std::flush; + } + } + + void write_polygon(const Polygon_with_holes_2& polygon, const std::string& name) { + if (enabled_) { + write_polygon(polygon.outer_boundary(), name); + for (auto hit = polygon.holes_begin(); hit != polygon.holes_end(); ++hit) { + write_polygon(*hit, name); + } + } + } + + void write_polygons(const std::vector& polygons, const std::string& name) { + if (enabled_) { + size_t i = 0; + for (auto& polygon : polygons) { + write_polygon_to_obj_(obj, vi, true, polygon, name + "_" + std::to_string(i++)); + write_polygon_to_svg_(svg, polygon, name); + } + obj << std::flush; + } + } + + void write_polygons(const std::vector& polygons, const std::string& name) { + if (enabled_) { + size_t i = 0; + for (auto& polygon : polygons) { + write_polygon_to_obj_(obj, vi, true, polygon.outer_boundary(), name + "_" + std::to_string(i)); + write_polygon_to_svg_(svg, polygon.outer_boundary(), name); + for (auto hit = polygon.holes_begin(); hit != polygon.holes_end(); ++hit) { + write_polygon_to_obj_(obj, vi, true, *hit, name + "_" + std::to_string(i)); + write_polygon_to_svg_(svg, *hit, name); + } + } + obj << std::flush; + } + } + + private: + std::ofstream obj; + size_t vi; + std::ofstream svg; + bool enabled_; + std::string last_segment_name_; + + void write_polygon_to_svg_(std::ostream& ofs, const Polygon_2& polygon, const std::string& class_name = "") { + ofs << "x()) << "," << -CGAL::to_double(vit->y()) << " "; + } + ofs << "\"/>\n"; + } + + void write_polygon_to_obj_(std::ofstream& ofs, size_t& vertex_index, bool as_line, const Polygon_2& polygon, const std::string& name) { + ofs << "o " << name << "\n"; // Object name + + // Write vertices + for (auto vit = polygon.vertices_begin(); vit != polygon.vertices_end(); ++vit) { + ofs << "v " << CGAL::to_double(vit->x()) << " " << CGAL::to_double(vit->y()) << " 0\n"; + } + + if (as_line) { + // Write line segments (edges) + for (size_t j = 0; j < polygon.size(); ++j) { + ofs << "l " << vertex_index + j << " " << vertex_index + (j + 1) % polygon.size() << "\n"; + } + } else { + ofs << "f"; + for (size_t j = 0; j < polygon.size(); ++j) { + ofs << " " << vertex_index + j; + } + ofs << "\n"; + } + + vertex_index += polygon.size(); + } +}; + +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 + + std::set eliminated_polies; + /* - if constexpr (false) { - // solve overlap by means of union into components - std::vector> adj(input_polygons.size()); - for (const auto& edge : overlaps) { - adj[edge.first].push_back(edge.second); - adj[edge.second].push_back(edge.first); - } - - std::vector visited(input_polygons.size(), false); - std::vector> connected_components; - - for (size_t v = 0; v < input_polygons.size(); ++v) { - if (!visited[v]) { - connected_components.emplace_back(); - - std::stack stack; - stack.push(v); - visited[v] = true; - - while (!stack.empty()) { - size_t u = stack.top(); - stack.pop(); - connected_components.back().push_back(u); - - for (size_t neighbor : adj[u]) { - if (!visited[neighbor]) { - visited[neighbor] = true; - stack.push(neighbor); - } - } - } - } - } - - std::vector fused_polies; - - for (auto& comp : connected_components) { - std::vector comp_polies; - if (comp.size() == 1) { - fused_polies.push_back(input_polygons[comp.front()]); - } else { - for (auto& c : comp) { - comp_polies.push_back(input_polygons[c]); - } - fused_polies.push_back(fuse_with_offset(comp_polies, 1.e-2)); - } - } - -#ifdef SVGFILL_DEBUG - for (auto it = fused_polies.begin(); it != fused_polies.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "fused_poly_" + std::to_string(std::distance(fused_polies.begin(), it))); - write_polygon_to_svg(svg, *it); - } -#endif - - input_polygons = fused_polies; + std::map overlap_counts; + for (auto& p : overlaps) { + overlap_counts[p.first]++; + overlap_counts[p.second]++; } */ - { - // [NB Nov 6] we cannot do this anymore because it could revert the spacing between input polygons - // that touch in the corner. - // Now that overlaps/touches at corners are handled more locally only a small indent is produced - // which would be undone by means of an inset+offset. - // - // [NB Nov 10] this is actually still necessary though, but we apply a much smaller distance now - // to keep the overlap eliminations in tact - // - // Inset-offset to remove tiny details that may cause enourmous spikes in offsets - for (auto& r : input_polygons) { - auto ps = create_and_convert_offset_polygon(-polygon_offset_distance / 10000., r); - if (ps.size() == 1) { - auto r2 = ps.front(); - ps = create_and_convert_offset_polygon(+polygon_offset_distance / 10000., r2); - if (ps.size() == 1) { - r = ps.front(); + auto overlaps = find_overlaps(polygons); + + for (const auto& edge : overlaps) { + // Skip eliminated + if (eliminated_polies.find(edge.first) != eliminated_polies.end() || + eliminated_polies.find(edge.second) != eliminated_polies.end()) { + continue; + } + + // Many overlaps indicate an aggregated polygon, skip them + /* + if (overlap_counts[edge.first] > 10 || overlap_counts[edge.second] > 10) { + if (overlap_counts[edge.first] > 10) { + eliminated_polies.insert(edge.first); + } + if (overlap_counts[edge.second] > 10) { + eliminated_polies.insert(edge.second); + } + continue; + } + */ + + // these are pointers now, because otherwise swap would not work? + auto* poly1 = &polygons[edge.first]; + auto* poly2 = &polygons[edge.second]; + + // @todo this is applied during overlap processing, maybe better after the boolean operation, + // because they can be come small or narrow when overlaps are resolved + + // Populate eliminated_polies with small polygons + // This can happen over time when modifications are made to the polygons to solve overlaps + bool skip = false; + if (poly1->area() < 1.e-2) { + eliminated_polies.insert(edge.first); + skip = true; + } + if (poly2->area() < 1.e-2) { + eliminated_polies.insert(edge.second); + skip = true; + } + // Small slivers are also just eliminated + if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly1))) { + eliminated_polies.insert(edge.first); + skip = true; + } + if (!maybe_take_first_if_single_item(create_and_convert_offset_polygon(-1.e-1, *poly2))) { + eliminated_polies.insert(edge.second); + skip = true; + } + if (skip) { + continue; + } + + // Skip polygons that have a very high intersection over union + // ratio, which indicates that they are very likely duplicates + if (CGAL::do_intersect(*poly1, *poly2)) { + std::vector result; + CGAL::intersection(*poly1, *poly2, 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(*poly1, *poly2, poly12); + typename K::FT union_area = poly12.outer_boundary().area(); + for (auto& h : poly12.holes()) { + union_area -= h.area(); + } + if (union_area > 0 && intersection_area / union_area > 0.99) { + // std::cerr << intersection_area / union_area << std::endl; + eliminated_polies.insert(edge.first); + continue; + } + } + + if (!(poly1->is_simple() && poly2->is_simple())) { + continue; + } + + { + std::vector result; + + boost::optional mp1, mp2, mp3, mp4; + bool swap = false; + + swap = poly1->area() <= poly2->area(); + if (swap) { + std::swap(poly1, poly2); + } + + bool success = false; + if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) { + if ((mp2 = subtract_retain_largest(*poly1, *mp1))) { + if ((mp3 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE * 2, *mp2)))) { + if ((mp4 = subtract_retain_largest(*poly2, *mp3))) { + *poly1 = *mp2; + *poly2 = *mp4; + success = true; + } + } + } + } + + if (!success) { + eliminated_polies.insert(swap ? edge.first : edge.second); + continue; } } } -#ifdef SVGFILL_DEBUG - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "processed_input_poly_" + std::to_string(std::distance(input_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); + // iterate over the eliminated polygons and remove them from the input polygons + for (auto it = eliminated_polies.rbegin(); it != eliminated_polies.rend(); ++it) { + polygons.erase(polygons.begin() + *it); } -#endif +} - // Unfortunately CGAL does not seem to have a ready to use aabb primitive for segments in 2D, - // so we have to use 3D segments and aabb tree for 2D polygons. - std::list> all_segs; - std::unordered_map*, decltype(input_polygons.begin())> seg_to_poly; +class SegmentLookup { + public: + typedef std::vector::const_iterator PolygonIt; - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - for (auto eit = it->edges_begin(); eit != it->edges_end(); ++eit) { - CGAL::Segment_3 seg3d( - CGAL::Point_3(eit->source().x(), eit->source().y(), 0), - CGAL::Point_3(eit->target().x(), eit->target().y(), 0) - ); - all_segs.push_back(seg3d); - seg_to_poly[&all_segs.back()] = it; - } - } - - using TreeTraits = CGAL::AABB_traits>::iterator>>; - using Tree = CGAL::AABB_tree; - - Tree tree(all_segs.begin(), all_segs.end()); - tree.accelerate_distance_queries(); - - auto input_polygon_boundary = - [&](const Point_2& p, double tol = 1e-5) -> decltype(input_polygons.begin()) + SegmentLookup(const std::vector& polygons) + : polygons_ref_(polygons) { + // Unfortunately CGAL does not seem to have a ready to use aabb primitive for segments in 2D, + // so we have to use 3D segments and aabb tree for 2D polygons. + for (auto it = polygons.begin(); it != polygons.end(); ++it) { + for (auto eit = it->edges_begin(); eit != it->edges_end(); ++eit) { + CGAL::Segment_3 seg3d( + CGAL::Point_3(eit->source().x(), eit->source().y(), 0), + CGAL::Point_3(eit->target().x(), eit->target().y(), 0)); + all_segs.push_back(seg3d); + seg_to_poly[&all_segs.back()] = it; + } + } + tree_ = Tree(all_segs.begin(), all_segs.end()); + tree_.accelerate_distance_queries(); + } + + // This part is the most computationally expensive. Caching effectively halves the lookup time here, since every vertex on the subdivided corridor mesh has on average two outgoing edges. + PolygonIt input_polygon_boundary(const Point_2& p, double tol = 1e-5) { + auto it = input_polygon_boundary_cache_.find(p); + if (it != input_polygon_boundary_cache_.end()) { + return it->second; + } + // Find closest point & corresponding segment - auto closest = tree.closest_point_and_primitive(CGAL::Point_3(p.x(), p.y(), 0)); + auto closest = tree_.closest_point_and_primitive(CGAL::Point_3(p.x(), p.y(), 0)); const auto& closest_pt = closest.first; auto seg_ptr = &*closest.second; double d = CGAL::to_double(CGAL::squared_distance(p, Point_2(closest_pt.x(), closest_pt.y()))); + + PolygonIt res; if (d < (tol * tol)) { - return seg_to_poly.find(seg_ptr)->second; + res = seg_to_poly.find(seg_ptr)->second; + } else { + res = polygons_ref_.end(); } - return input_polygons.end(); + + input_polygon_boundary_cache_[p] = res; + return res; }; - /* - auto input_polygon_boundary = [&input_polygons](const CGAL::Point_2& p, double tol = 1.e-5) { - // unfortunately some imprecision slept into the code so we can't - // so we can't just use has_on_boundary() anymore - double D = std::numeric_limits::infinity(); - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { - for (auto jt = it->edges_begin(); jt != it->edges_end(); ++jt) { - const auto& seg = *jt; - auto d = std::sqrt(CGAL::to_double(CGAL::squared_distance(seg, p))); - if (d < D) { - D = d; - } - if (d < tol) { - return it; - } - } - } - return input_polygons.end(); - }; - */ - - auto close_input_point = [&input_polygons](const CGAL::Point_2& P) { + std::pair> close_input_point(const CGAL::Point_2& P) const { + // @todo use tree CGAL::Point_2 closest; double closest_distance = std::numeric_limits::infinity(); - auto input_it = input_polygons.end(); + auto input_it = polygons_ref_.end(); // unfortunately some imprecision slept into the code so we can't // so we can't just use has_on_boundary() anymore - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { + for (auto it = polygons_ref_.begin(); it != polygons_ref_.end(); ++it) { for (auto& p : *it) { auto d = std::sqrt(CGAL::to_double(CGAL::squared_distance(P, p))); if (d < closest_distance) { @@ -697,14 +670,16 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v return std::make_pair(input_it, closest); }; - auto project_input_point = [&input_polygons](const CGAL::Point_2& P) { + std::pair> project_input_point(const CGAL::Point_2& P) const { + // @todo use tree + CGAL::Point_2 closest; typename K::FT closest_sq_distance = std::numeric_limits::infinity(); - auto input_it = input_polygons.end(); + auto input_it = polygons_ref_.end(); // unfortunately some imprecision slept into the code so we can't // so we can't just use has_on_boundary() anymore - for (auto it = input_polygons.begin(); it != input_polygons.end(); ++it) { + for (auto it = polygons_ref_.begin(); it != polygons_ref_.end(); ++it) { for (auto jt = it->edges_begin(); jt != it->edges_end(); ++jt) { auto Pp = jt->supporting_line().projection(P); auto d = CGAL::squared_distance(Pp, P); @@ -719,224 +694,52 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v return std::make_pair(input_it, closest); }; - // 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(); - } +private: + using TreeTraits = CGAL::AABB_traits>::iterator>>; + using Tree = CGAL::AABB_tree; - // Overlap removal can also result in close points causing problems when converted into non-exact nt - remove_close_points(R); + const std::vector& polygons_ref_; + std::list> all_segs; + std::unordered_map*, PolygonIt> seg_to_poly; + Tree tree_; - auto ps = create_and_convert_offset_polygon(polygon_offset_distance, R); - for (auto& p : ps) { - if (!p.is_simple()) { - /*{ - std::cerr << "input ["; - bool first = true; - for (auto& pp : r) { - if (!first) { - std::cerr << ","; - } - first = false; - std::cerr << "(" << pp.x() << "," << pp.y() << ")"; - } - std::cerr << "]" << std::endl; - } + std::map::const_iterator> input_polygon_boundary_cache_; +}; - { - std::cerr << "["; - bool first = true; - for (auto& pp : p) { - if (!first) { - std::cerr << ","; - } - first = false; - std::cerr << "(" << pp.x() << "," << pp.y() << ")"; - } - std::cerr << "]" << std::endl; - }*/ - - throw std::runtime_error("Complex polygon originated from offset"); - } - } - offset_polygons.insert(offset_polygons.end(), ps.begin(), ps.end()); - } - -#ifdef SVGFILL_DEBUG - for (auto it = offset_polygons.begin(); it != offset_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, true, *it, "offset_poly_" + std::to_string(std::distance(offset_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); - } -#endif - - // 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(); }); - } - -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, unioned_polygons.front().outer_boundary(), "offset_poly_joined"); - write_polygon_to_svg(svg, unioned_polygons.front().outer_boundary()); - -#endif - - Polygon_2 fused_removed_close_points; - { - std::vector> ps; - auto& p = unioned_polygons.front().outer_boundary(); - ps.reserve(p.size()); - auto I = p.begin(); - auto J = I + 1; - for (;; ++J) { - bool last = false; - if (J == p.end()) { - J = p.begin(); - last = true; - } - // if (CGAL::squared_distance(*I, *J) > (polygon_offset_distance * polygon_offset_distance)) { - if (CGAL::squared_distance(*I, *J) > (1.e-4 * 1.e-4)) { - ps.push_back(*J); - I = J; - } - if (last) { - break; - } - } - fused_removed_close_points = Polygon_2(ps.begin(), ps.end()); - } - - // Apply negative offset to get the outer perimeter polygon - auto inner_offset = 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); - -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, inner_offset.front(), "joined_inset"); - write_polygon_to_svg(svg, inner_offset.front()); -#endif - - /* - // there is non-insignificant chance that around the outer boundary, vertices are located in - // between of the input polyhedra, but intermediate vertices result in triangles that will no longer - // span between the two spaces with two edges and therefore cause the topological centre line - // to no run up to the center. Eliminate all vertices that are not on the polyhedral boundary of polygon. - - // this theory proved to be false. once we have topological end points in our graph that are - // connected to input polyhedra to form closed cells, we move those topological end points to - // the average of the input polyhedra corner points, thus effectively also moving them outwards. - { - for (auto& i : inner_offset) { - std::vector> ps; - for (auto& p : i) { - if (input_polygon_boundary(p, 1.e-3) != input_polygons.end()) { - ps.push_back(p); - } - } - i = Polygon_2(ps.begin(), ps.end()); +Polygon_2 subdivide_polygon(double max_distance, const Polygon_2 & p) { + std::vector points; + for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { + const auto& seg = *it; + auto num_splits = (int)std::ceil(std::sqrt(CGAL::to_double(seg.squared_length())) / max_distance) - 1; + points.push_back(seg.source()); + for (auto i = 0; i < num_splits; ++i) { + auto d = (seg.target() - seg.source()) / (num_splits + 1) * (i + 1); + points.push_back(seg.source() + d); } } + return Polygon_2(points.begin(), points.end()); +}; -#ifdef SVGFILL_DEBUG - write_polygon_to_obj(obj, vi, true, inner_offset.front(), "joined_inset_cleaned"); - write_polygon_to_svg(svg, inner_offset.front()); -#endif - */ - - // Subtract original polygons from outer perimeter - std::vector difference_result, difference_result_subdivided; - for (auto& i : inner_offset) { - std::vector working_copy; - working_copy.emplace_back(i); - - for (auto& r : input_polygons) { - std::vector temp_working_copy; - for (auto& wc : working_copy) { - CGAL::difference(wc, r, std::back_inserter(temp_working_copy)); - } - working_copy = temp_working_copy; - } - difference_result.insert(difference_result.end(), working_copy.begin(), working_copy.end()); +Polygon_with_holes_2 subdivide_polygon(double max_distance, const Polygon_with_holes_2& pwh) { + Polygon_2 outer = subdivide_polygon(max_distance, pwh.outer_boundary()); + std::vector holes; + for (auto hit = pwh.holes_begin(); hit != pwh.holes_end(); ++hit) { + holes.push_back(subdivide_polygon(max_distance, *hit)); } + return Polygon_with_holes_2(outer, holes.begin(), holes.end()); +}; - // subdivide difference_result to have better behave triangulation - - { - const double max_distance = polygon_offset_distance / 8.; - auto subdivide_polygon = [max_distance](const Polygon_2& p) { - std::vector points; - for (auto it = p.edges_begin(); it != p.edges_end(); ++it) { - const auto& seg = *it; - auto num_splits = (int)std::ceil(std::sqrt(CGAL::to_double(seg.squared_length())) / max_distance) - 1; - points.push_back(seg.source()); - for (auto i = 0; i < num_splits; ++i) { - auto d = (seg.target() - seg.source()) / (num_splits + 1) * (i + 1); - points.push_back(seg.source() + d); - } - } - return Polygon_2(points.begin(), points.end()); - }; - - for (auto& pwh : difference_result) { - // Subdivide outer boundary - Polygon_2 outer = subdivide_polygon(pwh.outer_boundary()); - // Subdivide holes - std::vector holes; - for (auto hit = pwh.holes_begin(); hit != pwh.holes_end(); ++hit) { - holes.push_back(subdivide_polygon(*hit)); - } - // Construct new Polygon_with_holes_2 - difference_result_subdivided.push_back(Polygon_with_holes_2(outer, holes.begin(), holes.end())); - } - } - -#ifdef SVGFILL_DEBUG - for (auto it = difference_result_subdivided.begin(); it != difference_result_subdivided.end(); ++it) { - auto i = std::distance(difference_result_subdivided.begin(), it); - write_polygon_to_obj(obj, vi, true, it->outer_boundary(), "difference_result_subdivided_" + std::to_string(i)); - write_polygon_to_svg(svg, it->outer_boundary()); - for (auto& p : it->holes()) { - write_polygon_to_obj(obj, vi, true, p, "difference_result_subdivided_" + std::to_string(i)); - write_polygon_to_svg(svg, p); - } - } -#endif - - std::list> triangular_polygons; - - for (auto& pwh : difference_result_subdivided) { - CGAL::Polygon_triangulation_decomposition_2 decompositor; - decompositor(pwh, std::back_inserter(triangular_polygons)); - } - - triangular_polygons.erase(std::remove_if(triangular_polygons.begin(), triangular_polygons.end(), [](const CGAL::Polygon_2& p) { - return CGAL::to_double(p.area()) < 1.e-8; - }), triangular_polygons.end()); - -#ifdef SVGFILL_DEBUG - for (auto it = triangular_polygons.begin(); it != triangular_polygons.end(); ++it) { - write_polygon_to_obj(obj, vi, false, *it, "tri_" + std::to_string(std::distance(triangular_polygons.begin(), it))); - write_polygon_to_svg(svg, *it); - } -#endif - +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) { // 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, 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*, std::vector>> facet_to_segment; for (auto& tri : triangular_polygons) { for (size_t i = 0; i < 3; ++i) { @@ -950,26 +753,14 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } - // This part is the most computationally expensive. Caching effectively halves the lookup time here, since every vertex has two outgoing edges. - std::map input_polygon_boundary_cache; - auto cached_input_polygon_boundary = [&](const Point_2& p, double tol = 1e-5) -> decltype(input_polygons.begin()) - { - auto it = input_polygon_boundary_cache.find(p); - if (it == input_polygon_boundary_cache.end()) { - auto index = input_polygon_boundary(p, tol); - input_polygon_boundary_cache[p] = index; - return index; - } else { - return it->second; - } - }; + // @todo The smarter thing to do probably after creating the corridor mesh, register segments wrt to originating input polygon(s) and maintain that mapping when subdividing // Register midpoints on the edges within the 'corridor mesh' that span multiple input polygons for (auto& p : segment_to_facet) { auto center = CGAL::ORIGIN + (((p.first.first - CGAL::ORIGIN) + (p.first.second - CGAL::ORIGIN)) / 2); - auto p1index = cached_input_polygon_boundary(p.first.first); - auto p2index = cached_input_polygon_boundary(p.first.second); + auto p1index = segment_lookup.input_polygon_boundary(p.first.first); + auto p2index = segment_lookup.input_polygon_boundary(p.first.second); segment_to_input_facet[p.first].push_back(&*p1index); segment_to_input_facet[p.first].push_back(&*p2index); @@ -978,17 +769,8 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v segment_to_midpoint[p.first] = center; midpoint_to_segment[center] = p.first; } - - if (p1index != input_polygons.end() && p2index != input_polygons.end() && p1index != p2index) { - segment_to_midpoint[p.first] = center; - midpoint_to_segment[center] = p.first; - } } -#ifdef SVGFILL_DEBUG - obj << "o network_1\n"; -#endif - // Observe corridor mesh topology to join edge midpoints into a network std::map> line_graph; for (auto& p : segment_to_midpoint) { @@ -1000,20 +782,15 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v decltype(segment_to_midpoint)::const_iterator it; if ((it = segment_to_midpoint.find(r)) != segment_to_midpoint.end()) { line_graph[p.second].push_back(it->second); - -#ifdef SVGFILL_DEBUG - obj << "v " << CGAL::to_double(p.second.x()) << " " << CGAL::to_double(p.second.y()) << " 0\n"; - obj << "v " << CGAL::to_double(it->second.x()) << " " << CGAL::to_double(it->second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - - svg << "second.x()) << "\" y2=\"" << CGAL::to_double(it->second.y()) << "\" />"; -#endif } } } } + return {line_graph, midpoint_to_segment, segment_to_input_facet}; +} + +std::set> find_triangles(const std::map>& line_graph) { // Find triangles in this network often occuring at junctions in the corridor mesh std::set> triangles; std::function&)> find_triangles_recursive; @@ -1024,7 +801,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v const std::vector& neighbors_current = line_graph.at(path.back()); if (std::find(neighbors_current.begin(), neighbors_current.end(), path.front()) != neighbors_current.end()) { // We found a triangle, add it to the set - Triangle triangle = { path[0], path[1], path[2] }; + Triangle triangle = {path[0], path[1], path[2]}; std::sort(triangle.begin(), triangle.end()); triangles.insert(triangle); } @@ -1037,29 +814,28 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v if (std::find(path.begin(), path.end(), neighbor) == path.end()) { path.push_back(neighbor); find_triangles_recursive(path); - path.pop_back(); // Backtrack + path.pop_back(); // Backtrack } } }; for (auto& p : line_graph) { - std::vector ps = { p.first }; + std::vector ps = {p.first}; find_triangles_recursive(ps); } - // 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 + return triangles; +} - // non exact time, we need sqrt +std::set> eliminate_triangles(const std::map>& line_graph) { + auto triangles = find_triangles(line_graph); + + // @todo this currently uses a simple cartesian kernel for performance for support of sqrt, but + // this should be possible to rewrite as ratios/slopes in the exact kernel as well using SK = CGAL::Simple_cartesian; CGAL::Cartesian_converter C{}; -#ifdef SVGFILL_DEBUG - obj << "o eliminated\n"; -#endif - std::set> eliminated_segments; - for (auto& t : triangles) { Triangle st; std::transform(t.begin(), t.end(), st.begin(), C); @@ -1075,7 +851,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v double max_abs_dot = 0.; { - auto& ni = line_graph[t[i]]; + auto& ni = line_graph.find(t[i])->second; for (auto& n : ni) { if (std::find(t.begin(), t.end(), n) == t.end()) { // not contained in triangle @@ -1092,7 +868,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } { - auto& nj = line_graph[t[j]]; + auto& nj = line_graph.find(t[j])->second; for (auto& n : nj) { if (std::find(t.begin(), t.end(), n) == t.end()) { // not contained in triangle @@ -1106,7 +882,6 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } } - } if (max_abs_dot < global_min_abs_dot) { @@ -1119,176 +894,132 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v auto i = global_min_abs_dot_index; auto j = (i + 2) % 3; - eliminated_segments.insert({ t[i], t[j] }); - eliminated_segments.insert({ t[j], t[i] }); - -#ifdef SVGFILL_DEBUG - obj << "v " << st[j].x() << " " << st[j].y() << " 0\n"; - obj << "v " << st[i].x() << " " << st[i].y() << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - - svg << ""; -#endif + eliminated_segments.insert({t[i], t[j]}); + eliminated_segments.insert({t[j], t[i]}); } - } - Graph2D G2(line_graph); - for (auto& e : eliminated_segments) { - G2.remove_edge(e.first, e.second); + return eliminated_segments; +} + +bool is_parallel_2degree_node(Graph2D::vertex_const_iterator vit) { + auto it = vit->second.begin(); + auto& P = *it++; + auto& Q = *it++; + auto e1 = P - vit->first; + auto e2 = vit->first - Q; + if (e1.squared_length() == 0 || e2.squared_length() == 0) { + // @todo why does this happen? + return false; } + e1 /= std::sqrt(CGAL::to_double(e1.squared_length())); + e2 /= std::sqrt(CGAL::to_double(e2.squared_length())); + return std::abs(CGAL::to_double(e1 * e2)) > (1. - 1.e-5); +}; - auto G = G2.weld_vertices(); -#ifdef SVGFILL_DEBUG - obj << "o network_2\n"; - for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - obj << "v " << CGAL::to_double(it->first.x()) << " " << CGAL::to_double(it->first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(it->second.x()) << " " << CGAL::to_double(it->second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - } - obj << std::flush; -#endif - - auto is_parallel_2degree_node = [](decltype(G)::vertex_const_iterator vit) { - auto it = vit->second.begin(); - auto& P = *it++; - auto& Q = *it++; - auto e1 = P - vit->first; - auto e2 = vit->first - Q; - if (e1.squared_length() == 0 || e2.squared_length() == 0) { - // @todo why does this happen? - return false; - } - e1 /= std::sqrt(CGAL::to_double(e1.squared_length())); - e2 /= std::sqrt(CGAL::to_double(e2.squared_length())); - return std::abs(CGAL::to_double(e1 * e2)) > (1. - 1.e-5); - }; - - { - // Remove colinear vertices - size_t n_vertices_removed = 0; - for (auto vit = G.vertices_begin(); vit != G.vertices_end();) { - if (vit->second.size() == 2) { - if (is_parallel_2degree_node(vit)) { - vit = G.eliminate_vertex(vit); - ++n_vertices_removed; - } else { - ++vit; - } +void eliminate_colinear_vertices(Graph2D& G) { + size_t n_vertices_removed = 0; + for (auto vit = G.vertices_begin(); vit != G.vertices_end();) { + if (vit->second.size() == 2) { + if (is_parallel_2degree_node(vit)) { + vit = G.eliminate_vertex(vit); + ++n_vertices_removed; } else { ++vit; } + } else { + ++vit; } - // std::cout << "Eliminated " << n_vertices_removed << " vertices" << std::endl; } +} - // Ortho edge slide - { - std::list> edges_to_remove, edges_to_insert; +void edge_slide(Graph2D& G) { + std::list> edges_to_remove, edges_to_insert; - for (auto vit = G.vertices_begin(); vit != G.vertices_end(); ++vit) { - auto& selected = vit->first; + for (auto vit = G.vertices_begin(); vit != G.vertices_end(); ++vit) { + auto& selected = vit->first; - if (vit->second.size() >= 3) { - for (auto vjt = vit->second.begin(); vjt != vit->second.end(); ++vjt) { - auto& neighbour = *vjt; - bool processed_neighbour = false; + if (vit->second.size() >= 3) { + for (auto vjt = vit->second.begin(); vjt != vit->second.end(); ++vjt) { + auto& neighbour = *vjt; + bool processed_neighbour = false; - if (G.find(neighbour)->second.size() == 2 && !is_parallel_2degree_node(G.find(neighbour))) { - auto vkt = G.find(neighbour)->second.begin(); - if (selected == *vkt) { - vkt++; - } - auto& other = *vkt; + if (G.find(neighbour)->second.size() == 2 && !is_parallel_2degree_node(G.find(neighbour))) { + auto vkt = G.find(neighbour)->second.begin(); + if (selected == *vkt) { + vkt++; + } + auto& other = *vkt; - if ((other - neighbour).squared_length() < (neighbour - selected).squared_length()) { - continue; - } + if ((other - neighbour).squared_length() < (neighbour - selected).squared_length()) { + continue; + } - auto incoming = CGAL::Ray_2(other, neighbour - other); - boost::optional> closest_neighbouring_segment; - boost::optional> closest_intersection_point; - K::FT sq_distance_along_ray = std::numeric_limits::infinity(); + auto incoming = CGAL::Ray_2(other, neighbour - other); + boost::optional> closest_neighbouring_segment; + boost::optional> closest_intersection_point; + K::FT sq_distance_along_ray = std::numeric_limits::infinity(); - for (auto vlt = vit->second.begin(); vlt != vit->second.end(); ++vlt) { - auto& other_neighbour = *vlt; - if (vlt != vjt) { - CGAL::Segment_2 neighbouring_segment(selected, other_neighbour); - auto x = CGAL::intersection(incoming, neighbouring_segment); - if (x) { - if (auto* xp = variant_get>(&*x)) { - auto dist = ((*xp) - other).squared_length(); - if (dist < sq_distance_along_ray) { - closest_neighbouring_segment = neighbouring_segment; - closest_intersection_point = *xp; - sq_distance_along_ray = dist; - } + for (auto vlt = vit->second.begin(); vlt != vit->second.end(); ++vlt) { + auto& other_neighbour = *vlt; + if (vlt != vjt) { + CGAL::Segment_2 neighbouring_segment(selected, other_neighbour); + auto x = CGAL::intersection(incoming, neighbouring_segment); + if (x) { + if (auto* xp = variant_get>(&*x)) { + auto dist = ((*xp) - other).squared_length(); + if (dist < sq_distance_along_ray) { + closest_neighbouring_segment = neighbouring_segment; + closest_intersection_point = *xp; + sq_distance_along_ray = dist; } } } } - - if (closest_intersection_point && closest_neighbouring_segment) { - edges_to_remove.push_back(*closest_neighbouring_segment); - edges_to_remove.push_back({ neighbour, selected }); - edges_to_insert.push_back({ closest_neighbouring_segment->source(), *closest_intersection_point }); - edges_to_insert.push_back({ closest_neighbouring_segment->target(), *closest_intersection_point }); - edges_to_insert.push_back({ neighbour, *closest_intersection_point }); - - processed_neighbour = true; - } } - if (processed_neighbour) { - // Only one neigbour is processed because otherwise we obtain intersections - break; + + if (closest_intersection_point && closest_neighbouring_segment) { + edges_to_remove.push_back(*closest_neighbouring_segment); + edges_to_remove.push_back({neighbour, selected}); + edges_to_insert.push_back({closest_neighbouring_segment->source(), *closest_intersection_point}); + edges_to_insert.push_back({closest_neighbouring_segment->target(), *closest_intersection_point}); + edges_to_insert.push_back({neighbour, *closest_intersection_point}); + + processed_neighbour = true; } } + if (processed_neighbour) { + // Only one neigbour is processed because otherwise we obtain intersections + break; + } } } - - for (auto& s : edges_to_remove) { - G.remove_edge(s.source(), s.target()); - } - - - for (auto& s : edges_to_insert) { - G.insert(s.source(), s.target()); - } - -#ifdef SVGFILL_DEBUG - obj << "o network_3\n"; - for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - obj << "v " << CGAL::to_double(it->first.x()) << " " << CGAL::to_double(it->first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(it->second.x()) << " " << CGAL::to_double(it->second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - } -#endif } - // Now plot the edges on an arrangement in order to find planar cycles - // and merge the corridor-halves with their neighbouring input polygon - - Arrangement_2 arr; - - for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { - if (it->first == it->second) { - continue; - } - CGAL::insert(arr, Segment_2(it->first, it->second)); + for (auto& s : edges_to_remove) { + G.remove_edge(s.source(), s.target()); } - std::list> move_ops; - std::list> edge_ops; + for (auto& s : edges_to_insert) { + G.insert(s.source(), s.target()); + } +} + +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 +){ + std::list> constructed_segments; for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { if (it->second.size() == 1) { auto& M = it->first; - decltype(midpoint_to_segment)::mapped_type* q = nullptr; + 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(); @@ -1299,7 +1030,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } } else { - q = &midpoint_to_segment[M]; + q = &midpoint_to_segment.find(M)->second; } if (q == nullptr) { @@ -1309,7 +1040,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v bool handled_as_graph_path = false; // distance from unioned - shoot ray? - if (segment_to_input_facet[*q].size() == 2) { + 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_bounded_side(M)) { @@ -1339,10 +1070,10 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v Graph2D GGG(bnd); GGG.refine(*GGG.query(*closest_intersection_point, 0.01), *closest_intersection_point); - std::array>, 2> input_points = { { {}, {} } }; + std::array>, 2> input_points = {{{}, {}}}; size_t i = 0; - for (auto& fac : segment_to_input_facet[*q]) { + for (auto& fac : segment_to_input_facet.find(*q)->second) { for (auto it = fac->vertices_begin(); it != fac->vertices_end(); ++it) { auto seg = GGG.query(*it, 0.01); if (seg) { @@ -1361,13 +1092,13 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v if (!a1.empty() && !a2.empty()) { if (M != *closest_intersection_point) { - edge_ops.push_front({ M, *closest_intersection_point }); + constructed_segments.push_front({M, *closest_intersection_point}); } for (auto it = a1.begin(); it != a1.end() && std::next(it) != a1.end(); ++it) { - edge_ops.push_front({ *it, *(std::next(it)) }); + constructed_segments.push_front({*it, *(std::next(it))}); } for (auto it = a2.begin(); it != a2.end() && std::next(it) != a2.end(); ++it) { - edge_ops.push_front({ *it, *(std::next(it)) }); + constructed_segments.push_front({*it, *(std::next(it))}); } handled_as_graph_path = true; @@ -1381,8 +1112,8 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v if (!handled_as_graph_path) { // else we choose to map point to the midpoint of the found two close points. - auto pq = close_input_point(q->first); - auto pr = close_input_point(q->second); + auto pq = segment_lookup.close_input_point(q->first); + auto pr = segment_lookup.close_input_point(q->second); auto Q = pq.second; auto R = pr.second; @@ -1392,16 +1123,16 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // where Q and R are co-located, because the point R' is further away // in that case M + M-Q should gives is x that we then project onto the // input boundary - // - // - // ┌───────┐ - // │ │ - // │ │ - // │ │ - // └───────o <--Q,R - // - // ────────o <--M - // + // + // + // ┌───────┐ + // │ │ + // │ │ + // │ │ + // └───────o <--Q,R + // + // ────────o <--M + // // ┌───────x───────────────o <---R' // │ │ // │ │ @@ -1410,93 +1141,22 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // └───────────────────────┘ // @todo is this projection actually necessary or is it already 'exact enough'? - R = project_input_point(M + (M - Q)).second; + R = segment_lookup.project_input_point(M + (M - Q)).second; } auto avg = CGAL::ORIGIN + ((Q - CGAL::ORIGIN) + (R - CGAL::ORIGIN)) / 2; - move_ops.push_front({ M, avg }); - edge_ops.push_front({ avg, Q }); - edge_ops.push_front({ avg, R }); - + constructed_segments.push_front({M, avg}); + constructed_segments.push_front({avg, Q}); + constructed_segments.push_front({avg, R}); } } } -#ifdef SVGFILL_DEBUG - obj << "o network_4\n"; -#endif + return constructed_segments; +} - // note that we actually don't move but draw an edge - for (auto& pq : move_ops) { - if (pq.first == pq.second) { - continue; - } - CGAL::insert(arr, Segment_2(pq.first, pq.second)); - -#ifdef SVGFILL_DEBUG - obj << "v " << CGAL::to_double(pq.first.x()) << " " << CGAL::to_double(pq.first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(pq.second.x()) << " " << CGAL::to_double(pq.second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; -#endif - } - - - for (auto& pq : edge_ops) { - if (pq.first == pq.second) { - continue; - } - CGAL::insert(arr, Segment_2(pq.first, pq.second)); - -#ifdef SVGFILL_DEBUG - obj << "v " << CGAL::to_double(pq.first.x()) << " " << CGAL::to_double(pq.first.y()) << " 0\n"; - obj << "v " << CGAL::to_double(pq.second.x()) << " " << CGAL::to_double(pq.second.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; -#endif - } - - // Plot input polygons - 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))); - } - } - - -#ifdef SVGFILL_DEBUG - { - obj << "o arrangement_1\n"; - for (auto it = arr.edges_begin(); it != arr.edges_end(); ++it) { - auto& p = it->source()->point(); - auto& q = it->target()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - obj << "v " << CGAL::to_double(q.x()) << " " << CGAL::to_double(q.y()) << " 0\n"; - obj << "l " << vi++; - obj << " " << vi++ << "\n"; - } - } -#endif - - /* { - // debug, add outer bounds so that we can plot the face for any remaining edges - auto poly = unioned_polygons.front().outer_boundary(); - for (size_t i = 0; i != poly.size(); ++i) { - auto j = (i + 1) % poly.size(); - CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); - } - } */ - - // Now loop over the arrangement faces, when a face coincides with a point on the - // 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. - - size_t face_id = 0; +void fuse_corridor_halves_with_input(Arrangement_2& arr, Graph2D& G, SegmentLookup& segment_lookup, const Polygon_list& input_polygons, DebugWriter& debug_output) { std::set edges_to_remove; for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { @@ -1535,7 +1195,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v auto& p = curr->source()->point(); auto& q = curr->target()->point(); auto center = CGAL::ORIGIN + (((p - CGAL::ORIGIN) + (q - CGAL::ORIGIN)) / 2); - auto p1index = input_polygon_boundary(center); + auto p1index = segment_lookup.input_polygon_boundary(center); const bool on_orig_bound = p1index != input_polygons.end(); if (on_orig_bound) { if (edges_to_remove.find(curr->twin()) != edges_to_remove.end()) { @@ -1553,7 +1213,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v auto& p = curr->source()->point(); auto& q = curr->target()->point(); auto center = CGAL::ORIGIN + (((p - CGAL::ORIGIN) + (q - CGAL::ORIGIN)) / 2); - auto p1index = input_polygon_boundary(center); + auto p1index = segment_lookup.input_polygon_boundary(center); const bool on_orig_bound = p1index != input_polygons.end(); if (on_orig_bound) { if (edges_to_remove.find(curr->twin()) != edges_to_remove.end()) { @@ -1569,126 +1229,314 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v } } } - -#ifdef SVGFILL_DEBUG - write_polygon_to_svg(svg, circ_to_poly(it->outer_ccb())); - - obj << "o " << "face_"; - if (is_corridor) { - obj << "corri_"; - } - obj << face_id++ << "\n"; - - std::ostringstream oss; - - { - auto vv = vi; - auto curr = it->outer_ccb(); - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == it->outer_ccb()) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != it->outer_ccb()); - } - - for (auto jt = it->inner_ccbs_begin(); jt != it->inner_ccbs_end(); ++jt) { - auto vv = vi; - auto curr = *jt; - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == *jt) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != *jt); - } - - obj << oss.str(); -#endif } size_t remove_id = 0; for (auto& e : edges_to_remove) { -#ifdef SVGFILL_DEBUG - obj << "o " << "remove_" << remove_id++ << "\n"; - { - auto& p = e->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - } - { - auto& p = e->target()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - } - obj << "l " << vi++; - obj << " " << vi++ << std::endl; -#endif + debug_output.write_segment(e->source()->point(), e->target()->point(), "arr_remove_edge_" + std::to_string(remove_id++)); CGAL::remove_edge(arr, e); } +} + +class timer { + class entry { + public: + 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; + } + + private: + std::map::const_iterator start_it; + }; + + public: + entry start(const std::string& name) { + return timings_.insert({name, std::chrono::high_resolution_clock::now()}).first; + } + + private: + std::map< + std::string, + std::chrono::high_resolution_clock::time_point> + timings_; +}; + +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-2; + // 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); + + 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; + + auto t0 = timer.start("input"); + + debug_output.write_polygons(input_polygons_, "input"); + + if (polygon_offset_distance < 0.) { + polygon_offset_distance = estimate_polygon_offset_distance(input_polygons_); + } + + // Create copy to make mutable for cleaning + auto input_polygons = input_polygons_; + + for (auto& polygon : input_polygons) { + clean_polygon(polygon); + } + + { + decltype(input_polygons) split_polygons; + for (auto& poly : input_polygons) { + split_self_intersecting_polygon(poly, std::back_inserter(split_polygons)); + } + std::swap(input_polygons, split_polygons); + } + + t0.stop(); + t0 = timer.start("overlap elimination"); + + eliminate_overlaps(OVERLAP_RESOLUTION_DISTANCE, input_polygons); + + t0.stop(); + + // [NB Nov 6] we cannot do this anymore because it could revert the spacing between input polygons + // that touch in the corner. + // Now that overlaps/touches at corners are handled more locally only a small indent is produced + // which would be undone by means of an inset+offset. + // + // [NB Nov 10] this is actually still necessary though, but we apply a much smaller distance now + // to keep the overlap eliminations in tact + // + // Inset-offset to remove tiny details that may cause enourmous spikes in offsets + for (auto& r : input_polygons) { + smooth_polygon(-polygon_offset_distance / 10000., r); + } + + debug_output.write_polygons(input_polygons, "processed_input"); + + SegmentLookup segment_lookup(input_polygons); + + 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 inner_offset = 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(inner_offset, "outer_perimiter"); + + t0.stop(); + t0 = timer.start("corridor creation"); + + // Subtract original polygons from outer perimeter + std::vector difference_result, difference_result_subdivided; + for (auto& i : inner_offset) { + std::vector working_copy; + working_copy.emplace_back(i); + + for (auto& r : input_polygons) { + std::vector temp_working_copy; + for (auto& wc : working_copy) { + CGAL::difference(wc, r, std::back_inserter(temp_working_copy)); + } + working_copy = temp_working_copy; + } + difference_result.insert(difference_result.end(), working_copy.begin(), working_copy.end()); + } + + t0.stop(); + t0 = timer.start("corridor triangulation"); + + // subdivide difference_result to have better more detailed triangulation and therefore less-pronounced artefacts in midpoint network + + for (auto& pwh : difference_result) { + difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 8., pwh)); + // difference_result_subdivided.push_back(subdivide_polygon(polygon_offset_distance / 64., pwh)); + } + + debug_output.write_polygons(difference_result_subdivided, "corridor_subdivided"); + + std::vector> triangular_polygons; + for (auto& pwh : difference_result_subdivided) { + CGAL::Polygon_triangulation_decomposition_2 decompositor; + decompositor(pwh, std::back_inserter(triangular_polygons)); + } + + t0.stop(); + + /* + * // @todo decide whether this is smart or not + * // Would this not hurt topology too much? + triangular_polygons.erase(std::remove_if(triangular_polygons.begin(), triangular_polygons.end(), [](const CGAL::Polygon_2& p) { + return CGAL::to_double(p.area()) < 1.e-8; + }), triangular_polygons.end()); + */ + + t0 = timer.start("center line"); + + debug_output.write_polygons(triangular_polygons, "triangulated_corridor"); + + 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"); + } + } + + 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 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(); + + 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); + + edge_slide(G); + + 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, inner_offset, 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 + Arrangement_2 arr; + G.to_arrangement(arr); + + for (auto& pq : segments) { + if (pq.first == pq.second) { + continue; + } + CGAL::insert(arr, Segment_2(pq.first, pq.second)); + + debug_output.write_segment(pq.first, pq.second, "extended_segments"); + } + + // Write input polygons to arrangement_2 + 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))); + } + } + + // Just for the automatic numbering, create a full vector + std::vector temp; + for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { + if (it->is_unbounded()) { + continue; + } + temp.push_back(circ_to_poly(it->outer_ccb())); + } + debug_output.write_polygons(temp, "arr_faces"); + + + /* { + // debug, add outer bounds so that we can plot the face for any remaining edges + auto poly = unioned_polygons.front().outer_boundary(); + for (size_t i = 0; i != poly.size(); ++i) { + auto j = (i + 1) % poly.size(); + CGAL::insert(arr, Segment_2(poly.vertex(i), poly.vertex(j))); + } + } */ + + // Now loop over the arrangement faces, when a face coincides with a point on the + // 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. + + fuse_corridor_halves_with_input(arr, G, segment_lookup, input_polygons, debug_output); + + t0.stop(); for (auto it = arr.faces_begin(); it != arr.faces_end(); ++it) { if (it->is_unbounded()) { continue; } - output_polygons.push_back(circ_to_poly(it->outer_ccb())); - -#ifdef SVGFILL_DEBUG - write_polygon_to_svg(svg, circ_to_poly(it->outer_ccb())); - - obj << "o " << "merged_face_"; - obj << face_id++ << "\n"; - - std::ostringstream oss; - - { - auto vv = vi; - auto curr = it->outer_ccb(); - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == it->outer_ccb()) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != it->outer_ccb()); - } - - for (auto jt = it->inner_ccbs_begin(); jt != it->inner_ccbs_end(); ++jt) { - auto vv = vi; - auto curr = *jt; - do { - auto& p = curr->source()->point(); - obj << "v " << CGAL::to_double(p.x()) << " " << CGAL::to_double(p.y()) << " 0\n"; - oss << "l " << vi++; - ++curr; - if (curr == *jt) { - oss << " " << vv << "\n"; - } else { - oss << " " << vi << "\n"; - } - } while (curr != *jt); - } - - obj << oss.str(); -#endif } -#ifdef SVGFILL_DEBUG - svg << "\n"; -#endif + debug_output.write_polygons(output_polygons, "arr_faces_merged"); } #ifndef SVGFILL_MAIN diff --git a/src/svgfill/src/graph_2d.h b/src/svgfill/src/graph_2d.h index aaaa059b4e..da2b5014ec 100644 --- a/src/svgfill/src/graph_2d.h +++ b/src/svgfill/src/graph_2d.h @@ -2,8 +2,10 @@ #define GRAPH_2D_H #ifdef SVGFILL_DEBUG +#if 0 #include #endif +#endif template class Graph2D { @@ -334,6 +336,16 @@ public: return Graph2D(input_adjacency_list); } + template + void to_arrangement(T& arr) { + for (auto it = edges_begin(); it != edges_end(); ++it) { + if (it->first == it->second) { + continue; + } + CGAL::insert(arr, CGAL::Segment_2(it->first, it->second)); + } + } + void assert_symmetric() { #ifdef SVGFILL_DEBUG #if 0 From b7a8c9b3309caad78f5b6029a20422669bfa17ed Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 11 Feb 2026 11:43:58 +0100 Subject: [PATCH 004/131] Fix for 4.2 schema after a46cdbb907974c03be91bad002ce4f88aae682dd --- src/ifcgeom/mapping/IfcObjectPlacement.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ifcgeom/mapping/IfcObjectPlacement.cpp b/src/ifcgeom/mapping/IfcObjectPlacement.cpp index a0a37dd30f..c4f2f44b1f 100644 --- a/src/ifcgeom/mapping/IfcObjectPlacement.cpp +++ b/src/ifcgeom/mapping/IfcObjectPlacement.cpp @@ -37,7 +37,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { } auto self_places = placement->PlacesObject(); - inst->ReferencedByPlacements(); for (auto iter = self_places->begin(); iter != self_places->end(); ++iter) { if ((placement_rel_to_type_ && (*iter)->declaration().is(*placement_rel_to_type_)) || (placement_rel_to_instance_ && (*iter)->as() == placement_rel_to_instance_)) { @@ -47,12 +46,16 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { // Look for two levels deep, we want to know if we're at or *above* the // element we're ignoring, but we don't want to traverse the entire model. +#ifdef SCHEMA_IfcObjectPlacement_HAS_ReferencedByPlacements if (depth < 2) { auto refs = placement->ReferencedByPlacements(); for (auto& ref : *refs) { q.emplace_back(ref, depth + 1); } } +#else + Logger::Warning("Using --site-local-placement or --building-local-placement on IFC4.2 might have issues") +#endif } } From 6bb8abdc5a53024320e451b2bc57bd6cffe880af Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 11 Feb 2026 12:11:23 +0100 Subject: [PATCH 005/131] Fix for 4.2 schema after a46cdbb907974c03be91bad002ce4f88aae682dd --- src/ifcgeom/mapping/IfcObjectPlacement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcgeom/mapping/IfcObjectPlacement.cpp b/src/ifcgeom/mapping/IfcObjectPlacement.cpp index c4f2f44b1f..499d9d787b 100644 --- a/src/ifcgeom/mapping/IfcObjectPlacement.cpp +++ b/src/ifcgeom/mapping/IfcObjectPlacement.cpp @@ -54,7 +54,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { } } #else - Logger::Warning("Using --site-local-placement or --building-local-placement on IFC4.2 might have issues") + Logger::Warning("Using --site-local-placement or --building-local-placement on IFC4.2 might have issues"); #endif } } From 348e48b49c5b0bf1c4be2ed0ff79f00ab8b0be12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Aliste?= Date: Tue, 10 Feb 2026 18:17:52 -0300 Subject: [PATCH 006/131] Add get_angle_snap_value() helper to tool/snap.py This function retrieves the angle snap increment from Blender's tool_settings.snap_angle_increment property, which was added in Blender 4.2. This allows users to configure the angle snap value through Blender's native UI instead of using hardcoded values. Co-Authored-By: Claude Opus 4.5 --- src/bonsai/bonsai/tool/snap.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 5c538dfc43..7ee06cd44e 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -114,6 +114,15 @@ class Snap(bonsai.core.tool.Snap): return increment + @classmethod + def get_angle_snap_value(cls, context: bpy.types.Context) -> float: + """Get the angle snap increment from Blender's tool settings. + + :param context: Blender context + :return: Angle snap increment in degrees + """ + return context.scene.tool_settings.snap_angle_increment + @classmethod def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): matrix = obj.matrix_world.copy() From cd95f46db5e77d22ad0173137996fb9f99118a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Aliste?= Date: Tue, 10 Feb 2026 18:18:31 -0300 Subject: [PATCH 007/131] Use Blender's angle snap setting in tool/polyline.py Replace hardcoded 5-degree angle snapping with Blender's snap_angle_increment setting in calculate_distance_and_angle(). Co-Authored-By: Claude Opus 4.5 --- src/bonsai/bonsai/tool/polyline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index b288260d7d..5fbc909910 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -191,7 +191,8 @@ class Polyline(bonsai.core.tool.Polyline): orientation_angle = 0 if input_ui: if should_round: - angle = 5 * round(angle / 5) if distance < angle_round_threshold else angle + angle_snap = tool.Snap.get_angle_snap_value(context) + angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle factor = tool.Snap.get_increment_snap_value(context) distance = factor * round(distance / factor) input_ui.set_value("X", mouse_vector.x) From 067e04b56443a9bca3eb1e68fefc950d115e8b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Aliste?= Date: Tue, 10 Feb 2026 18:19:07 -0300 Subject: [PATCH 008/131] Use Blender's angle snap setting in model/polyline.py Replace hardcoded 5-degree angle snapping with Blender's snap_angle_increment setting in handle_lock_axis() for: - Initial angle rounding when locking axis (A key) - Angle rounding and increments on Shift+Wheel scroll Co-Authored-By: Claude Opus 4.5 --- src/bonsai/bonsai/bim/module/model/polyline.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index de169796f6..92ec36581b 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -211,22 +211,21 @@ class PolylineOperator: context.workspace.status_text_set(draw_instructions) def handle_lock_axis(self, context: bpy.types.Context, event: bpy.types.Event) -> None: + angle_snap = tool.Snap.get_angle_snap_value(context) if event.value == "PRESS" and event.type == "A": self.tool_state.lock_axis = False if self.tool_state.lock_axis else True if self.tool_state.lock_axis: self.tool_state.snap_angle = self.input_ui.get_number_value("WORLD_ANGLE") - # Round to the closest 5 - self.tool_state.snap_angle = round(self.tool_state.snap_angle / 5) * 5 + self.tool_state.snap_angle = round(self.tool_state.snap_angle / angle_snap) * angle_snap if event.shift and event.type in {"WHEELUPMOUSE", "WHEELDOWNMOUSE"}: self.tool_state.lock_axis = True self.tool_state.snap_angle = self.input_ui.get_number_value("WORLD_ANGLE") - # Round to the closest 5 - self.tool_state.snap_angle = round(self.tool_state.snap_angle / 5) * 5 + self.tool_state.snap_angle = round(self.tool_state.snap_angle / angle_snap) * angle_snap if event.type in {"WHEELUPMOUSE"}: - self.tool_state.snap_angle += 5 + self.tool_state.snap_angle += angle_snap else: - self.tool_state.snap_angle -= 5 + self.tool_state.snap_angle -= angle_snap self.handle_mouse_move(context, event) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) From 740fcf77684909a1d560f93029d21616bc5a34fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Aliste?= Date: Tue, 10 Feb 2026 18:20:18 -0300 Subject: [PATCH 009/131] Use Blender's angle snap setting in wall.py and profile.py Replace hardcoded 5-degree angle snapping with Blender's snap_angle_increment setting in create_wall_from_2_points() and create_profile_from_2_points(). Co-Authored-By: Claude Opus 4.5 --- src/bonsai/bonsai/bim/module/model/profile.py | 6 +++--- src/bonsai/bonsai/bim/module/model/wall.py | 4 ++-- src/bonsai/bonsai/tool/snap.py | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 572898c484..4e16f9b296 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -17,7 +17,7 @@ # along with Bonsai. If not, see . import copy -from math import atan2, degrees, pi +from math import atan2, degrees, pi, radians from typing import Any, Literal, Optional, Union import bmesh @@ -195,8 +195,8 @@ class DumbProfileGenerator: if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) - # Round to nearest 5 degrees - nearest_degree = (pi / 180) * 5 + angle_snap = tool.Snap.get_angle_snap_value(bpy.context) + nearest_degree = radians(angle_snap) self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_profile() diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index ba3d93586b..7f4579df31 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -916,8 +916,8 @@ class DumbWallGenerator: if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) - # Round to nearest 5 degrees - nearest_degree = (math.pi / 180) * 5 + angle_snap = tool.Snap.get_angle_snap_value(bpy.context) + nearest_degree = math.radians(angle_snap) self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_wall() diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 7ee06cd44e..bc35505826 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -118,10 +118,14 @@ class Snap(bonsai.core.tool.Snap): def get_angle_snap_value(cls, context: bpy.types.Context) -> float: """Get the angle snap increment from Blender's tool settings. + Uses snap_angle_increment_3d (Blender 5.0+) or snap_angle_increment (Blender 4.x). + :param context: Blender context :return: Angle snap increment in degrees """ - return context.scene.tool_settings.snap_angle_increment + if bpy.app.version >= (5, 0, 0): + return math.degrees(context.scene.tool_settings.snap_angle_increment_3d) + return math.degrees(context.scene.tool_settings.snap_angle_increment) @classmethod def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): From 7b4889d2ec21dbb7c2aebb0a3d9d12e9e286b265 Mon Sep 17 00:00:00 2001 From: ssg3d <64012165+ssg3d@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:26:25 +0800 Subject: [PATCH 010/131] Update IfcParse.cpp IfcOpenshell read file, and write file without changes. This round trip introduces truncation noise. It should not hurt to increase the precision to keep this clean. --- src/ifcparse/IfcParse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index f0f880b7d6..95ab95c0db 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -746,7 +746,7 @@ namespace { static std::string format_double(const double& d) { std::ostringstream oss; oss.imbue(std::locale::classic()); - oss << std::setprecision(std::numeric_limits::digits10) << d; + oss << std::setprecision(std::numeric_limits::max_digits10) << d; const std::string str = oss.str(); oss.str(""); std::string::size_type e = str.find('e'); From 7978f1fb0887b67a93dc503dd30e73448d62b2c9 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 13 Feb 2026 10:17:28 +0100 Subject: [PATCH 011/131] Fix compilation on gcc #7666 --- src/svgfill/src/arrange_polygons.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 0271d76146..0e77a28886 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1239,6 +1239,7 @@ void fuse_corridor_halves_with_input(Arrangement_2& arr, Graph2D& G, SegmentL } class timer { + public: class entry { public: entry(std::map::const_iterator start_it) @@ -1253,9 +1254,8 @@ class timer { std::map::const_iterator start_it; }; - public: entry start(const std::string& name) { - return timings_.insert({name, std::chrono::high_resolution_clock::now()}).first; + return entry(timings_.insert({name, std::chrono::high_resolution_clock::now()}).first); } private: From a88c5938dc7933461e2a9ccf5c4e763a351fbecd Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 14 Feb 2026 10:18:06 -0600 Subject: [PATCH 012/131] typos --- src/bonsai/bonsai/bim/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index d224761609..32240a3fee 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -467,12 +467,12 @@ class DocPreferences(bpy.types.PropertyGroup): classes_to_wireframe: StringProperty( default="IfcVirtualElement", name="Classes to Wireframe", - description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace", + description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualElement, IfcSpace", ) classes_no_cut: StringProperty( default="IfcVirtualElement, IfcSpace", name="Classes that are not cut", - description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace", + description="The cut decorator will be turned off for these classes\nEx: IfcVirtualElement, IfcSpace", ) if TYPE_CHECKING: From b246998f68c710e36d82247bd5133e19aa1851f5 Mon Sep 17 00:00:00 2001 From: falken10vdl <33285113+falken10vdl@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:28:43 +0100 Subject: [PATCH 013/131] Linked IFC projects enhancement (multiple links to same project file) (#7607) * Linked IFC projects enhancement (multiple links to same project file) - Implement link management system using UUIDs as identifiers to support multiple links to the same IFC file - Add georeferencing compatibility detection and UI display (NONE, NOT_COMPATIBLE, PARTIAL_COMPATIBLE, FULL_COMPATIBLE) - Support for duplicate link creation with Shift+D shortcut and automatic position offset - Add false origin and project north calculation from 3D cursor for MANUAL mode - Only store one cache per file, regardless of the amount of links - Prevent duplicate links based on filepath and position comparison - Improve error handling for missing files and loading failures - Update tests * Remove duplicate georef UI I try to avoid duplicate UI (especially for one that can be as sophisticated as georef - e.g. missing is WCS) as it means double the code, double the tests, potential user confusion. BTW the note about vertical datum isn't quite accurate as it may be included in the CRS definition so vertical datum is optional. * Remove depsgraph_update_post handler for update_link_ui_on_transform as per core developer feedback * Move get_projected_crs to geolocation module * Refactor get_projected_crs to simplify as per core developer feedback * Remove unused import of bonsai.tool from project module * Use IfcDocumentInformation per linked file and IfcDocumentReference for locaiton information * Refactor SaveBlendMetadataFile operator to remove try-except blocks and remove linked projects collections since they are recreated by bonsai * Cleanup removing empty collection instances for linked models in metadata.blend file and call determine_georeferencing_compatibility on link reload * Add locking mechanism for linked models and update UI to reflect lock status * Update logic that track IFC to execute_ifc_duplicate_operator instead of having it in execute() which does not track IFC undo/redo * Refactor link handling to use get_link_empty_handle and set_link_empty_handle methods which in turn use the standard blender-ifc integrations patters (tool.Ifc.get_object(doc_reference) and tool.Ifc.link(doc_reference, empty_handle) * remove operator.DuplicateLink and move it to tool.Project.duplicate_link() * Refactor link handling to use sequential identifiers (no need for STEP ID DocRef) * Refactor IFC linking logic to handle cases without a parent IFC file loaded. Firts link flase origin becomes parent origin * Lock should not affect selection. This makes it consistent with grid / spatial lock, and also toggle selectability is already implemented. * Remove unnecessary check for loaded library as Blender seems to do this internally already * Rename util to get_crs because in IFC4X3 you can also have geographic CRS not just projected * Remove unnecessary call to determine_georeferencing_compatibility This function is already always called prior to calculate_link_position so shouldn't be called here. It's also a very expensive function: as it currently stands, just to link a single IFC, ifcopenshell.open() is called 3 times. This reduces it to 2. * Store CRS as metadata for linked models, and compare metadata when indicating georeferencing compatibility Previously, to check georeferencing compatibility, ifcopenshell.open() was used. When linking large models, this adds considerable time and memory usage. This instead captures the georef as standard metadata in our .cache.json. This now reduces the ifcopenshell.open() calls back down to only 1 as necessary (see previous commit). * Use link index instead of link name to fetch link collection item Link name runs into issues with name uniqueness. This is why you created a function for "get next link ID". After this refactoring, we can no longer worry about uniqueness and that function may be removed. * Simplify reloadlink into just unload and reload (with cache disabled) This function should not be responsible for editing any data. * Remove unnecessary get_next_link_id as names no longer need uniqueness This now frees up the name variable to track a more meaningful, human name like IfcDocumentInformation's Name attribute. * Rewrite get / set link_empty_handle to just use the link directly This prevents needless logic to fetch the link and also removes issues related to duplicate names. * Temporarily remove logic in prop callback Right now, pretty much all the logic is done in a prop callback. In general logic in prop callbacks should be minimised, since it's hard to test and easily triggered as a domino effect of another change, and may also impact undo/redo. * Remove code that unnecessarily removes cache This code removes cache, which means any project unlinking an IFC auto clears the cache for any other project which doesn't make sense, and also breaks the ability to readd it quickly. * Rewrite link, unlink, load, and unload IFC There were a few issues tackled here: - Operators that change any IFC data must use tool.Ifc.Operator and _execute, otherwise undo/redo will break. That's one of the risks of using prop callbacks, as it is not explicit when an IFC edit happens. - The usage of IfcDocumentReference was not correct. The Location should store the URL, _not_ the position. The position should be in the Identification attribute. - The URL was stored in IfcDocumentInformation location, which does not work in IFC2X3. There are a few changes here to make it IFC2X3 compatible. - Generally move logic in operators, not prop callback. * Remove restriction around manual mode. Users should be able to use manual mode if they want. * Restore AUTOMATIC mode to identical behaviour to file open This is the first step to reusing cache files agnostic of the host. * Revert tests for a fresh start for updating tests * Revert "test_feature - clean up .ifc.cache. files after test was executed" This reverts commit 99ae768ddf409e19c05d4ea1e44c9bdfd03702f9. * Update tests and reimplement calculations for matrix of empty handle Previously, the empty would always be placed at the origin, unless a "position" offset was present. This is a problem, because the "position" is simply a local offset relative to the Blender cache! If the cache was regenerated, the offsets would be outdated. Also, the cache appeared in different locations depending on the false origin mode, so the offset would mean different things to different people. Instead, a more robust method is: 1. When you link a file, a Blender cache is generated. The Blender origin of this cache is arbitrary! It depends on the user's false origin mode and is purely a Blender session specific thing. 2. When you load a link, a link is _always_ loaded into the correct location with regards to IFC global coordinates. All math is done from the perspective of IFC. 3. If you choose to transform (move / rotate / scale!?) this link from its correct location, that gets recorded as a 4x4 transformation matrix. Note: I haven't implemented this properly yet. Tests all pass, with a minor modification to the new behaviour that false origin mode now won't affect the location it ends up in, only the generation of the cache. * Remove arbitrary convention around display name Not needed anymore now that A/M/D is a detail and not significant on actual coordinates, and also that the UUID is no longer needed. * Simplify implementation of loading linked models when opening an IFC * Move link matrix calculation from operator to tool for reuse * Implement editing link location and calculation of transformation matrix I changed my mind on the is_locked thing, since it isn't clear to the user that locking need to be done to save changes. * Remove old is_locked, prop update callback no longer needed (dedicated operator instead), remove old calculation code * Simplify code related to placed_as_per_georef * For now, simple skip for duplicate / delete IMO duplicate / delete / move a link are very rare and explicit operations. * Update tests * Remove host_model coordinate data as cache is no longer host model dependent * Move icons outside list because there are too many * Minor tweaks --------- Co-authored-by: Dion Moult Co-authored-by: Dion Moult --- .gitignore | 2 + src/bonsai/bonsai/bim/export_ifc.py | 1 - .../bonsai/bim/module/drawing/operator.py | 4 +- src/bonsai/bonsai/bim/module/drawing/ui.py | 2 +- .../bonsai/bim/module/geometry/operator.py | 9 + .../bim/module/georeference/operator.py | 2 +- .../bonsai/bim/module/georeference/prop.py | 12 +- .../bonsai/bim/module/project/__init__.py | 9 +- .../bonsai/bim/module/project/operator.py | 367 ++++++++++++------ src/bonsai/bonsai/bim/module/project/prop.py | 46 ++- src/bonsai/bonsai/bim/module/project/ui.py | 93 +++-- src/bonsai/bonsai/bim/operator.py | 74 ++-- src/bonsai/bonsai/core/georeference.py | 4 +- src/bonsai/bonsai/tool/document.py | 8 + src/bonsai/bonsai/tool/georeference.py | 18 + src/bonsai/bonsai/tool/project.py | 125 +++--- src/bonsai/test/bim/feature/project.feature | 111 +++--- src/bonsai/test/bim/test_feature.py | 51 +-- src/bonsai/test/tool/test_project.py | 142 ++++--- .../ifcopenshell/util/geolocation.py | 11 +- 20 files changed, 641 insertions(+), 450 deletions(-) diff --git a/.gitignore b/.gitignore index bee04658cd..8a7482ab8e 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,8 @@ src/bonsai/bonsai/translations.py # bonsai test temp files src/bonsai/test/files/temp +src/bonsai/test/files/basic.ifc.cache.blend +src/bonsai/test/files/basic.ifc.cache.sqlite # bonsai data src/bonsai/bonsai/bim/data/build/ diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 7295c72e34..cfab523942 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -52,7 +52,6 @@ class IfcExporter: self.set_header() IfcStore.update_cache() self.sync_all_objects() - tool.Project.save_linked_models_to_ifc() extension = self.ifc_export_settings.output_file.split(".")[-1].lower() if extension == "ifczip": with tempfile.TemporaryDirectory() as unzipped_path: diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index d02e4c6c76..db389427e6 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -928,7 +928,7 @@ class CreateDrawing(bpy.types.Operator): props = tool.Project.get_project_props() for link in props.get_loaded_links_for_drawings(): - files[link.name] = self.get_linked_file(link) + files[link.filepath] = self.get_linked_file(link) target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"] self.setup_serialiser(target_view) @@ -1374,7 +1374,7 @@ class CreateDrawing(bpy.types.Operator): return True def get_linked_file(self, link: "Link") -> ifcopenshell.file: - link_path = link.name + link_path = link.filepath ifc_file = IfcStore.session_files.get(link_path, None) if ifc_file is not None: return ifc_file diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 556459b8b6..dc98715686 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -89,7 +89,7 @@ class BIM_PT_camera(Panel): for link in links: row = panel.row(align=True) split = row.split(factor=0.9) - split.label(text=link.name, icon="FILE") + split.label(text=link.filepath, icon="FILE") split.prop(link, "include_in_drawings", text="") else: panel.label(text="No IFC projects linked and loaded.") diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index fbd694d219..7937acca30 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -902,6 +902,7 @@ class OverrideDelete(bpy.types.Operator): if not is_valid_data_block: continue + element = tool.Ifc.get_entity(obj) if element: if tool.Geometry.is_locked(element): @@ -912,6 +913,9 @@ class OverrideDelete(bpy.types.Operator): if ifcopenshell.util.element.get_pset(element, "BBIM_Array"): self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") continue + if element.is_a("IfcDocumentReference"): + self.report({"INFO"}, "Linked models cannot be deleted.") + continue if element.is_a("IfcGridAxis"): # Deleting the last W axis is OK if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or ( @@ -1208,6 +1212,11 @@ class OverrideDuplicateMove(bpy.types.Operator): operator.report({"ERROR"}, f"Drawing '{obj.name}' not duplicated.") continue + if element.is_a("IfcDocumentReference"): + objects_to_remove.add(obj) + operator.report({"ERROR"}, f"Linked model '{obj.name}' not duplicated.") + continue + if tool.Geometry.is_locked(element): objects_to_remove.add(obj) operator.report({"ERROR"}, lock_error_message(obj.name)) diff --git a/src/bonsai/bonsai/bim/module/georeference/operator.py b/src/bonsai/bonsai/bim/module/georeference/operator.py index 6c82e6684c..c2af3d9225 100644 --- a/src/bonsai/bonsai/bim/module/georeference/operator.py +++ b/src/bonsai/bonsai/bim/module/georeference/operator.py @@ -51,7 +51,7 @@ class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Remove the georeferencing" def _execute(self, context): - core.remove_georeferencing(tool.Ifc) + core.remove_georeferencing(tool.Ifc, tool.Georeference) class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/georeference/prop.py b/src/bonsai/bonsai/bim/module/georeference/prop.py index 5a1a37f06f..1f4ca321ac 100644 --- a/src/bonsai/bonsai/bim/module/georeference/prop.py +++ b/src/bonsai/bonsai/bim/module/georeference/prop.py @@ -224,16 +224,12 @@ class BIMGeoreferenceProperties(PropertyGroup): x_axis_ordinate: StringProperty(name="X Axis Ordinate", update=update_grid_north_vector) x_axis_is_null: BoolProperty(name="X Axis Is Null") - # These are only for reference to capture data about a host model from a linked model - # If you relink a model from a new host origin, we can autodetect it in theory with this - host_model_origin: StringProperty(name="Host Model Origin") - host_model_origin_si: StringProperty(name="Host Model Origin SI") - host_model_project_north: StringProperty(name="Host Model Angle to Grid North") - # This is the ENH in project units and SI units of the Blender session's 0,0,0. # These are only for reference, using tool.Georeference.set_model_origin on # project load, project create, and when linking for the first time from an # empty Blender session. + model_is_georeferenced: BoolProperty(name="Model Is Georeferenced") + model_crs: StringProperty(name="Model CRS") model_origin: StringProperty(name="Model Origin") model_origin_si: StringProperty(name="Model Origin SI") model_project_north: StringProperty(name="Model Angle to Grid North") @@ -275,10 +271,6 @@ class BIMGeoreferenceProperties(PropertyGroup): x_axis_ordinate: str x_axis_is_null: bool - host_model_origin: str - host_model_origin_si: str - host_model_project_north: str - model_origin: str model_origin_si: str model_project_north: str diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 53ebaff14e..f4f4261eb5 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -31,27 +31,31 @@ classes = ( operator.BIM_OT_load_clipping_planes, operator.BIM_OT_save_clipping_planes, operator.ChangeLibraryElement, + operator.ClearMeasurement, operator.ClearRecentIFCProjects, operator.CreateClippingPlane, operator.CreateProject, operator.DisableCulling, operator.DisableEditingHeader, + operator.DisableEditingLink, operator.EditHeader, + operator.EditLink, operator.EditProjectLibrary, operator.EnableCulling, operator.EnableEditingHeader, + operator.EnableEditingLink, operator.ExportIFC, operator.FlipClippingPlane, operator.IFCFileHandlerOperator, operator.ImageScalingTool, operator.LinkIfc, + operator.LoadBlendMetadataAndIFC, operator.LoadLink, operator.LoadLinkedProject, operator.LoadProject, operator.LoadProjectElements, - operator.MeasureTool, operator.MeasureFaceAreaTool, - operator.ClearMeasurement, + operator.MeasureTool, operator.NewProject, operator.QueryLinkedElement, operator.RefreshClippingPlanes, @@ -69,7 +73,6 @@ classes = ( operator.UnassignLibraryDeclaration, operator.UnlinkIfc, operator.UnloadLink, - operator.LoadBlendMetadataAndIFC, workspace.ExploreHotkey, prop.LibraryBreadcrumb, prop.LibraryElement, diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index b6f63f13ca..e4e57275ba 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -18,6 +18,7 @@ import datetime import json +import math import logging import os import subprocess @@ -59,6 +60,15 @@ import bonsai.core.project as core import bonsai.tool as tool from bonsai.bim import export_ifc, import_ifc from bonsai.bim.ifc import IfcStore +from bonsai.bim.ui import IFCFileSelector +from bonsai.bim import import_ifc +from bonsai.bim import export_ifc +from math import radians, degrees +from pathlib import Path +from collections import defaultdict +from mathutils import Vector, Matrix +from bpy.app.handlers import persistent +from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.model.decorator import FaceAreaDecorator, PolylineDecorator from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.project.data import LinksData, ProjectLibraryData @@ -1321,7 +1331,7 @@ class ToggleFilterCategories(bpy.types.Operator): return {"FINISHED"} -class LinkIfc(bpy.types.Operator, ImportHelper): +class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator): bl_idname = "bim.link_ifc" bl_label = "Link IFC" bl_options = {"REGISTER", "UNDO"} @@ -1360,134 +1370,148 @@ class LinkIfc(bpy.types.Operator, ImportHelper): row = self.layout.row() row.prop(pprops, "project_north") - def execute(self, context): + def invoke(self, context, event): + pprops = tool.Project.get_project_props() + + # Populate false origin and project north from 3D cursor for user convenience + cursor = context.scene.cursor + cursor_loc = cursor.location + cursor_rot = cursor.rotation_euler + angle = -cursor_rot.z + + # Calculate false origin based on the provided formulas + # x = -3Dcursor.x * cos(angle) - 3Dcursor.y * sin(angle) + # y = 3Dcursor.x * sin(angle) - 3Dcursor.y * cos(angle) + # z = -3Dcursor.z + false_origin_x = -cursor_loc.x * math.cos(angle) - cursor_loc.y * math.sin(angle) + false_origin_y = cursor_loc.x * math.sin(angle) - cursor_loc.y * math.cos(angle) + false_origin_z = -cursor_loc.z + + # Set the false_origin value + pprops.false_origin = f"{false_origin_x:.3f},{false_origin_y:.3f},{false_origin_z:.3f}" + pprops.project_north = str(round(math.degrees(angle), 1)) + + return super().invoke(context, event) + + def _execute(self, context): start = time.time() files = [f.name for f in self.files] if self.files else [self.filepath] + + if not files or all(not f or not f.strip() for f in files): + self.report({"ERROR"}, "No file selected") + return {"CANCELLED"} + + existing_links = tool.Project.get_linked_models_documents() if tool.Ifc.get() else {} for filename in files: + if not filename or not filename.strip(): + continue filepath = Path(self.directory) / filename if bpy.data.filepath and filepath.samefile(bpy.data.filepath): self.report({"INFO"}, "Can't link the current .blend file") continue props = tool.Project.get_project_props() - new = props.links.add() filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) + + new = props.links.add() + if tool.Ifc.get(): + if not (document := existing_links.get(filepath)): + document = ifcopenshell.api.document.add_information(tool.Ifc.get()) + document.Name = Path(filepath).name + document.Scope = "LINKED_MODEL" + reference = ifcopenshell.api.document.add_reference(tool.Ifc.get(), information=document) + reference[1] = ",".join([str(o) for o in np.eye(4).flatten().tolist()]) + reference.Location = filepath.replace("\\", "/") + new.ifc_definition_id = reference.id() new.name = filepath - status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache) - if status == {"CANCELLED"}: - error_msg = ( - f'Error processing IFC file "{filepath}" ' - "was critical and blend file either wasn't saved or wasn't updated. " - "See logs above in system console for details." - ) - print(error_msg) - self.report({"ERROR"}, error_msg) - return {"FINISHED"} - print(f"Finished linking {len(files)} IFCs", time.time() - start) - return {"FINISHED"} + new.filepath = filepath + bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache) -class UnlinkIfc(bpy.types.Operator): +class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unlink_ifc" bl_label = "Unlink IFC" bl_options = {"REGISTER", "UNDO"} bl_description = "Remove the selected file from the link list" - filepath: bpy.props.StringProperty() + link_index: bpy.props.IntProperty(name="Link Index") - def execute(self, context): - filepath = Path(self.filepath).as_posix() - bpy.ops.bim.unload_link(filepath=filepath) + def _execute(self, context): props = tool.Project.get_project_props() - index = props.links.find(filepath) - if index != -1: - props.links.remove(index) - return {"FINISHED"} + link = props.links[self.link_index] + bpy.ops.bim.unload_link(link_index=self.link_index) + if tool.Ifc.get(): + reference = tool.Ifc.get().by_id(link.ifc_definition_id) + document = tool.Document.get_reference_document(reference) + ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference) + if document and not tool.Document.get_document_references(document): + ifcopenshell.api.document.remove_information(tool.Ifc.get(), document) + props.links.remove(self.link_index) -class UnloadLink(bpy.types.Operator): +class UnloadLink(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unload_link" bl_label = "Unload Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Unload the selected linked file" - filepath: bpy.props.StringProperty() + link_index: bpy.props.IntProperty(name="Link Index") - def execute(self, context): - filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.filepath)) - if filepath.suffix.lower() == ".ifc": - filepath = filepath.with_suffix(".ifc.cache.blend") - - for library in list(bpy.data.libraries): - if tool.Blender.ensure_blender_path_is_abs(Path(library.filepath)) == filepath: + def _execute(self, context): + link = tool.Project.get_project_props().links[self.link_index] + if obj := tool.Project.get_link_empty_handle(link): + collection = obj.instance_collection + library = collection.library + tool.Ifc.unlink(obj=obj) + bpy.data.objects.remove(obj) + if collection.users == 0: + bpy.data.collections.remove(collection) + if not len([c for c in bpy.data.collections if c.library == library]): bpy.data.libraries.remove(library) - - props = tool.Project.get_project_props() - links = props.links - link = links[self.filepath] - # Let's assume that user might delete it. - if empty_handle := link.empty_handle: - bpy.data.objects.remove(empty_handle) - - # following lines removes the library also when use_relative_path=True, otherwise it doesn't - libraries = bpy.data.libraries - for library in libraries: - if library.name == self.filepath + ".cache.blend": - bpy.data.libraries.remove(library) - link.is_loaded = False - - if not any([l.is_loaded for l in links]): - ProjectDecorator.uninstall() - # we make sure we don't draw queried object from the file that was just unlinked - elif queried_obj := props.queried_obj: - queried_filepath = Path(queried_obj["ifc_filepath"]) - if queried_filepath == filepath: - ProjectDecorator.uninstall() - - return {"FINISHED"} + ProjectDecorator.uninstall() -class LoadLink(bpy.types.Operator): +class LoadLink(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.load_link" bl_label = "Load Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Load the selected file" - filepath: bpy.props.StringProperty(name="Link Filepath") + link_index: bpy.props.IntProperty(name="Link Index") use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) - filepath_: Path - - def execute(self, context): - filepath = Path(tool.Ifc.resolve_uri(self.filepath)) + def _execute(self, context): + self.link = tool.Project.get_project_props().links[self.link_index] + filepath = Path(tool.Ifc.resolve_uri(self.link.filepath)) if not filepath.exists(): self.report({"ERROR"}, f"File does not exist: '{filepath}'") return {"CANCELLED"} self.filepath_ = filepath - if filepath.suffix.lower().endswith(".blend"): - self.link_blend(filepath) - elif filepath.suffix.lower().endswith(".ifc"): - status = self.link_ifc() - if status: - return status - return {"FINISHED"} + if filepath.suffix.lower().endswith(".ifc"): + return self.link_ifc() def link_blend(self, filepath: Path) -> None: with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to): - data_to.scenes = data_from.scenes - link = tool.Project.get_project_props().links[self.filepath] - for scene in bpy.data.scenes: - if not scene.library or Path(scene.library.filepath) != filepath: + data_to.collections = [c for c in data_from.collections if "IfcProject" in c] + + # Find the linked collection + for collection in bpy.data.collections: + if not collection.library or Path(collection.library.filepath) != filepath: continue - for child in scene.collection.children: - if "IfcProject" not in child.name: - continue - empty = bpy.data.objects.new(child.name, None) - empty.instance_type = "COLLECTION" - empty.instance_collection = child - link.empty_handle = empty - bpy.context.scene.collection.objects.link(empty) - break + # Create unique empty instance for this link + empty_name = collection.name + empty = bpy.data.objects.new(empty_name, None) + empty.instance_type = "COLLECTION" + empty.instance_collection = collection + empty.matrix_world = Matrix(tool.Project.calculate_link_matrix(self.link)) + + tool.Project.set_link_empty_handle(self.link, empty) + bpy.context.scene.collection.objects.link(empty) + self.link.is_loaded = True + if tool.Ifc.get(): # For non-IFC projects, locking has no meaning + tool.Geometry.lock_object(empty) + tool.Blender.select_and_activate_single_object(bpy.context, empty) break - link.is_loaded = True - tool.Blender.select_and_activate_single_object(bpy.context, empty) + else: + print(f"WARNING: No IfcProject collection found in {filepath}") + self.link.is_loaded = False def link_ifc(self) -> Union[set[str], None]: blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend") @@ -1502,14 +1526,12 @@ class LoadLink(bpy.types.Operator): code = f""" import bpy +import sys def run(): import bonsai.tool as tool gprops = tool.Georeference.get_georeference_props() # Our model origin becomes their host model origin - gprops.host_model_origin = "{gprops.model_origin}" - gprops.host_model_origin_si = "{gprops.model_origin_si}" - gprops.host_model_project_north = "{gprops.model_project_north}" gprops.has_blender_offset = {gprops.has_blender_offset} gprops.blender_offset_x = "{gprops.blender_offset_x}" gprops.blender_offset_y = "{gprops.blender_offset_y}" @@ -1522,7 +1544,12 @@ def run(): pprops.false_origin = "{pprops.false_origin}" pprops.project_north = "{pprops.project_north}" # Use absolute path to be safe from cwd changes. - bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}") + try: + bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}") + except RuntimeError as e: + # Operator failed (returned CANCELLED with error report) + print(f"Failed to load linked project: {{e}}") + sys.exit(1) # Use str instead of as_posix to avoid issues with Windows shared paths. bpy.ops.wm.save_as_mainfile(filepath=r"{str(blend_filepath)}") @@ -1556,14 +1583,17 @@ except Exception as e: if not blend_filepath.exists() or blend_filepath.stat().st_mtime < t: return {"CANCELLED"} - self.set_model_origin_from_link() - + self.set_model_origin_from_link() + self.set_georeferencing_indicator() self.link_blend(blend_filepath) def set_model_origin_from_link(self) -> None: if tool.Ifc.get(): return # The current model's coordinates always take priority. + if len(tool.Project.get_project_props().links) > 1: + return # Only the first link sets the origin + json_filepath = self.filepath_.with_suffix(".ifc.cache.json") if not json_filepath.exists(): return @@ -1576,21 +1606,36 @@ except Exception as e: if (value := data.get(prop, None)) is not None: setattr(gprops, prop, value) + def set_georeferencing_indicator(self) -> None: + if not tool.Ifc.get(): + self.link.georeferenced = "NONE" + return + if not (crs_name := (ifcopenshell.util.geolocation.get_crs(tool.Ifc.get()) or {}).get("Name", "")): + self.link.georeferenced = "NONE" + return + reference = tool.Ifc.get().by_id(self.link.ifc_definition_id) + json_filepath = Path(reference.Location).with_suffix(".ifc.cache.json") + if not json_filepath.exists(): + self.link.georeferenced = "NONE" + return + with open(json_filepath, "r") as f: + data = json.load(f) + if not data["model_is_georeferenced"]: + self.link.georeferenced = "NONE" + else: + self.link.georeferenced = "FULL_COMPATIBLE" if crs_name == data["model_crs"] else "NOT_COMPATIBLE" + class ReloadLink(bpy.types.Operator): bl_idname = "bim.reload_link" bl_label = "Reload Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Reload the selected file" - filepath: bpy.props.StringProperty() + link_index: bpy.props.IntProperty(name="Link Index") def execute(self, context): - is_abs = os.path.isabs(Path(self.filepath)) - use_relative_path = not is_abs - bpy.ops.bim.unlink_ifc(filepath=self.filepath) - filepath = tool.Ifc.resolve_uri(self.filepath) - status = bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False, use_relative_path=use_relative_path) - return {"FINISHED"} + bpy.ops.bim.unload_link(link_index=self.link_index) + return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"} class ToggleLinkSelectability(bpy.types.Operator): @@ -1598,16 +1643,18 @@ class ToggleLinkSelectability(bpy.types.Operator): bl_label = "Toggle Link Selectability" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle selectability" - link: bpy.props.StringProperty(name="Linked IFC Filepath") + link_index: bpy.props.IntProperty(name="Link Index") def execute(self, context): props = tool.Project.get_project_props() - link = props.links[self.link] - self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) + link = props.links[self.link_index] + self.library_filepath = tool.Blender.ensure_blender_path_is_abs( + Path(link.filepath).with_suffix(".ifc.cache.blend") + ) link.is_selectable = (is_selectable := not link.is_selectable) for collection in self.get_linked_collections(): collection.hide_select = not is_selectable - if handle := link.empty_handle: + if handle := tool.Project.get_link_empty_handle(link): handle.hide_select = not is_selectable return {"FINISHED"} @@ -1624,13 +1671,15 @@ class ToggleLinkVisibility(bpy.types.Operator): bl_label = "Toggle Link Visibility" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle visibility between SOLID and WIREFRAME" - link: bpy.props.StringProperty(name="Linked IFC Filepath") + link_index: bpy.props.IntProperty(name="Link Index") mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) def execute(self, context): props = tool.Project.get_project_props() - link = props.links[self.link] - self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) + link = props.links[self.link_index] + self.library_filepath = tool.Blender.ensure_blender_path_is_abs( + Path(link.filepath).with_suffix(".ifc.cache.blend") + ) if self.mode == "WIREFRAME": self.toggle_wireframe(link) elif self.mode == "VISIBLE": @@ -1652,7 +1701,7 @@ class ToggleLinkVisibility(bpy.types.Operator): layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections) for layer_collection in layer_collections.values(): layer_collection.exclude = is_hidden - if handle := link.empty_handle: + if handle := tool.Project.get_link_empty_handle(link): handle.hide_set(is_hidden) def get_linked_collections(self) -> list[bpy.types.Collection]: @@ -1663,17 +1712,95 @@ class ToggleLinkVisibility(bpy.types.Operator): ] +class EnableEditingLink(bpy.types.Operator): + bl_idname = "bim.enable_editing_link" + bl_label = "Enable Editing Link" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Enable editing link location" + + def execute(self, context): + link = tool.Project.get_project_props().active_link + link.is_editing = True + tool.Geometry.unlock_object(tool.Project.get_link_empty_handle(link)) + return {"FINISHED"} + + +class DisableEditingLink(bpy.types.Operator): + bl_idname = "bim.disable_editing_link" + bl_label = "Disable Editing Link" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Disable editing link and restore to previously saved location" + + def execute(self, context): + link = tool.Project.get_project_props().active_link + link.is_editing = False + obj = tool.Project.get_link_empty_handle(link) + obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link)) + tool.Geometry.lock_object(obj) + return {"FINISHED"} + + +class EditLink(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_link" + bl_label = "Edit Link" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Disable editing link and restore to previously saved location" + + def _execute(self, context): + link = tool.Project.get_project_props().active_link + link.is_editing = False + obj = tool.Project.get_link_empty_handle(link) + new_obj_matrix = obj.matrix_world + + filepath = Path(tool.Ifc.resolve_uri(link.filepath)) + with open(filepath.with_suffix(".ifc.cache.json"), "r") as f: + metadata = json.load(f) + + rot = ifcopenshell.util.shape_builder.np_rotation_matrix( + radians(-float(metadata["model_project_north"])), 4, "Z" + ) + global_matrix = rot @ np.eye(4) + global_matrix[:, 3][:3] = [float(o) for o in metadata["model_origin_si"].split(",")] + + gprops = tool.Georeference.get_georeference_props() + rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z") + local_matrix = rot @ np.eye(4) + local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")] + + # obj_matrix is typically calculated as: + # obj_matrix = np.linalg.inv(local_matrix) @ transformation @ global_matrix + # So let's calculate the transformation + + transformed_global_matrix = local_matrix @ np.array(new_obj_matrix) + transformation = transformed_global_matrix @ np.linalg.inv(global_matrix) + if np.allclose(transformation, np.eye(4)): + link.has_transformation = True + transformation = ",".join(map(str, np.eye(4).reshape(-1))) + else: + link.has_transformation = False + transformation = ",".join(map(str, transformation.reshape(-1))) + + if tool.Ifc.get(): + reference = tool.Ifc.get().by_id(link.ifc_definition_id) + reference[1] = transformation + else: + link.transformation = transformation + + obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link)) + tool.Geometry.lock_object(obj) + + class SelectLinkHandle(bpy.types.Operator): bl_idname = "bim.select_link_handle" bl_label = "Select Link Handle" bl_options = {"REGISTER", "UNDO"} bl_description = "Select link empty object handle" - index: bpy.props.IntProperty(name="Link Index") + link_index: bpy.props.IntProperty(name="Link Index") def execute(self, context): props = tool.Project.get_project_props() - link = props.links[self.index] - handle = link.empty_handle + link = props.links[self.link_index] + handle = tool.Project.get_link_empty_handle(link) if not handle: self.report({"ERROR"}, "Link has no empty handle (probably it was deleted).") return {"CANCELLED"} @@ -1866,7 +1993,14 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper): print("Processing", self.filepath) self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath)) - self.file = ifcopenshell.open(self.filepath) + + try: + self.file = ifcopenshell.open(self.filepath) + except Exception as e: + self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}") + bpy.data.collections.remove(self.collection) + return {"CANCELLED"} + tool.Ifc.set(self.file) print("Finished opening") @@ -1897,20 +2031,13 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper): if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin: tool.Loader.set_manual_blender_offset(self.file) elif tool.Loader.settings.false_origin_mode == "AUTOMATIC": - if host_model_origin_si := gprops.host_model_origin_si: - host_model_origin_si = [float(o) / self.unit_scale for o in host_model_origin_si.split(",")] - tool.Loader.settings.false_origin = host_model_origin_si - tool.Loader.settings.project_north = float(gprops.host_model_project_north) - tool.Loader.set_manual_blender_offset(self.file) - else: - tool.Loader.guess_false_origin(self.file) + tool.Loader.guess_false_origin(self.file) tool.Georeference.set_model_origin() self.json_filepath = self.filepath + ".cache.json" data = { - "host_model_origin": gprops.host_model_origin, - "host_model_origin_si": gprops.host_model_origin_si, - "host_model_project_north": gprops.host_model_project_north, + "model_is_georeferenced": gprops.model_is_georeferenced, + "model_crs": gprops.model_crs, "model_origin": gprops.model_origin, "model_origin_si": gprops.model_origin_si, "model_project_north": gprops.model_project_north, diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 08859af1f9..a34b5726f7 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -16,8 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os +import math from collections.abc import Generator +from pathlib import Path from typing import TYPE_CHECKING, Literal, Union, assert_never, get_args import bpy @@ -219,29 +220,62 @@ class FilterCategory(PropertyGroup): class Link(PropertyGroup): - name: StringProperty( - name="Name", + name: StringProperty(name="Name") + filepath: StringProperty( + name="Filepath", description="Filepath to linked .ifc file, stored in posix format (could be relative to .ifc file, not to .blend)", ) + transformation: StringProperty( + name="Transformation", + description="4x4 matrix transformation as a flattened comma separated list for the linked model", + default="", + ) + georeferenced: EnumProperty( + name="Georeferenced", + description="Georeferencing status: compatibility between host and linked model", + items=[ + ("NONE", "No Georef", "Linked model has no georeferencing"), + ("NOT_COMPATIBLE", "Not Compatible", "Has geo data but CRS differ from host"), + ("FULL_COMPATIBLE", "Full Compatible", "Both CRS name and vertical datum match host"), + ], + default="NONE", + ) + has_transformation: BoolProperty( + name="Has Transformation", + description="Whether there is a transformation from its global coordinates", + default=False, + ) is_loaded: BoolProperty(name="Is Loaded", default=False) + is_editing: BoolProperty(name="Is Editing", description="Whether the link is being transformed", default=False) is_selectable: BoolProperty(name="Is Selectable", default=True) is_wireframe: BoolProperty(name="Is Wireframe", default=False) is_hidden: BoolProperty(name="Is Hidden", default=False) include_in_drawings: BoolProperty(name="Include in Drawings", default=True, options=set()) empty_handle: PointerProperty( name="Empty Object Handle", - description="We use empty object handle to allow simple manipulations with a linked model (moving, scaling, rotating)", + description="Storage for empty handle. Used in non-IFC scenarios or temporarily during link creation", type=bpy.types.Object, ) + ifc_definition_id: IntProperty( + name="IFC Definition ID", + description="STEP ID of the IfcDocumentReference when linked to a parent IFC project. Zero when no parent IFC exists", + default=0, + ) if TYPE_CHECKING: name: str + filepath: str + transformation: str + georeferenced: Literal["NONE", "NOT_COMPATIBLE", "FULL_COMPATIBLE"] + has_transformation: bool is_loaded: bool + is_editing: bool is_selectable: bool is_wireframe: bool is_hidden: bool include_in_drawings: bool empty_handle: Union[bpy.types.Object, None] + ifc_definition_id: int class EditedObj(PropertyGroup): @@ -424,6 +458,10 @@ class BIMProjectProperties(PropertyGroup): clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5) edited_objs: bpy.props.CollectionProperty(type=EditedObj) + @property + def active_link(self) -> Union[Link, None]: + return tool.Blender.get_active_uilist_element(self.links, self.active_link_index) + @property def active_clipping_plane(self) -> ObjProperty | None: return tool.Blender.get_active_uilist_element(self.clipping_planes, self.clipping_planes_active_index) diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 79ed2c7d7a..3d02e6ca05 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -22,6 +22,7 @@ import os from typing import TYPE_CHECKING import bpy +import math import ifcopenshell from bpy.types import Menu, Panel, UIList @@ -30,6 +31,7 @@ import bonsai.tool as tool from bonsai.bim.helper import draw_attributes, prop_with_search from bonsai.bim.ifc import IfcStore from bonsai.bim.module.project.data import LinksData, ProjectData +from typing import TYPE_CHECKING if TYPE_CHECKING: from bonsai.bim.module.project.prop import ( @@ -477,17 +479,27 @@ class BIM_PT_links(Panel): def draw(self, context): self.props = tool.Project.get_project_props() + row = self.layout.row(align=True) row.operator("bim.link_ifc") if self.props.links: - self.layout.template_list( - "BIM_UL_links", - "", - self.props, - "links", - self.props, - "active_link_index", - ) + if self.props.active_link: + row = self.layout.row(align=True) + row.alignment = "RIGHT" + index = self.props.active_link_index + if self.props.active_link.is_editing: + row.operator("bim.edit_link", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_link", text="", icon="CANCEL") + else: + row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL") + if self.props.active_link.is_loaded: + row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index + row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index + row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index + else: + row.operator("bim.load_link", text="", icon="LINKED").link_index = index + row.operator("bim.unlink_ifc", text="", icon="X").link_index = index + self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index") if LinksData.enable_culling: row = self.layout.row(align=True) @@ -607,47 +619,30 @@ class BIM_UL_links(UIList): active_propname, index, ): - if item: - row = layout.row(align=True) - if item.is_loaded: - row.label(text=item.name) - op = row.operator( - "bim.toggle_link_selectability", - text="", - icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON", - emboss=False, - ) - op.link = item.name - op = row.operator( - "bim.toggle_link_visibility", - text="", - icon="CUBE" if item.is_wireframe else "MESH_CUBE", - emboss=False, - ) - op.link = item.name - op.mode = "WIREFRAME" - op = row.operator( - "bim.toggle_link_visibility", - text="", - icon="HIDE_ON" if item.is_hidden else "HIDE_OFF", - emboss=False, - ) - op.link = item.name - op.mode = "VISIBLE" - op = row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA") - op.index = index - op = row.operator("bim.unload_link", text="", icon="UNLINKED") - op.filepath = item.name - op = row.operator("bim.reload_link", text="", icon="FILE_REFRESH") - op.filepath = item.name - else: - row.prop(item, "name", text="") - op = row.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER") - op.attribute_data_path = tool.Blender.get_full_data_path(item, "name") - op = row.operator("bim.load_link", text="", icon="LINKED") - op.filepath = item.name - op = row.operator("bim.unlink_ifc", text="", icon="X") - op.filepath = item.name + row = layout.row(align=True) + if item.is_loaded: + if item.georeferenced == "NONE": + row.label(text="", icon="QUESTION") + elif item.georeferenced == "NOT_COMPATIBLE": + row.label(text="", icon="ERROR") + elif item.georeferenced == "FULL_COMPATIBLE": + row.label(text="", icon="WORLD") + if item.has_transformation: + row.label(text="", icon="OBJECT_ORIGIN") + + row.label(text=item.filepath) + icon = "RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON" + row.operator("bim.toggle_link_selectability", text="", icon=icon, emboss=False).link_index = index + icon = "CUBE" if item.is_wireframe else "MESH_CUBE" + op = row.operator("bim.toggle_link_visibility", text="", icon=icon, emboss=False) + op.link_index = index + op.mode = "WIREFRAME" + icon = "HIDE_ON" if item.is_hidden else "HIDE_OFF" + op = row.operator("bim.toggle_link_visibility", text="", icon=icon, emboss=False) + op.link_index = index + op.mode = "VISIBLE" + else: + row.label(text=item.filepath) class BIM_PT_purge(Panel): diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 456d5ed5a0..f332c91401 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -268,63 +268,45 @@ class SaveBlendMetadataFile(bpy.types.Operator): import bpy # Ensure all styles are loaded before attempting to remove them -try: - bpy.ops.bim.load_styles() -except Exception: - pass +bpy.ops.bim.load_styles() # 1. Collect all IfcStyle material names ifcstyle_material_names = [] -try: - styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None) - if styles_props is None and bpy.data.scenes: - styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None) - if styles_props: - for style in list(styles_props.styles): - material = getattr(style, "blender_material", None) - if material and material.name: - ifcstyle_material_names.append(material.name) -except Exception: - pass +styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None) +if styles_props is None and bpy.data.scenes: + styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None) +if styles_props: + for style in list(styles_props.styles): + material = getattr(style, "blender_material", None) + if material and material.name: + ifcstyle_material_names.append(material.name) # 2. Purge IfcStore -try: - from bonsai.bim.ifc import IfcStore -except ImportError: - IfcStore = None - -if IfcStore: - try: - IfcStore.purge() - except Exception: - pass +from bonsai.bim.ifc import IfcStore +IfcStore.purge() # 3. Remove all collections named IfcProject* for collection in list(bpy.data.collections): if collection.name.startswith('IfcProject'): - try: - bpy.data.collections.remove(collection, do_unlink=True) - except Exception: - pass + bpy.data.collections.remove(collection, do_unlink=True) -# 4. Purge orphaned data blocks after removing IfcProject collections -try: - bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) -except Exception: - pass +# 4.1 Remove all collections from linked libraries (they will be recreated by bonsai) +for collection in list(bpy.data.collections): + if collection.library: + bpy.data.collections.remove(collection, do_unlink=True) -# 5. Remove all materials corresponding to the IfcStyles we collected -materials_removed = 0 -try: - for mat_name in ifcstyle_material_names: - if mat_name in bpy.data.materials: - try: - bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True) - materials_removed += 1 - except Exception: - pass -except Exception: - pass +# 4.2. Remove all empty objects that are collection instances for linked models +for obj in list(bpy.data.objects): + if obj.type == 'EMPTY' and obj.instance_type == 'COLLECTION' and obj.name.startswith('IfcProject/'): + bpy.data.objects.remove(obj, do_unlink=True) + +# 5. Purge orphaned data blocks after removing IfcProject collections +bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + +# 6. Remove all materials corresponding to the IfcStyles we collected +for mat_name in ifcstyle_material_names: + if mat_name in bpy.data.materials: + bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True) bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}') """ diff --git a/src/bonsai/bonsai/core/georeference.py b/src/bonsai/bonsai/core/georeference.py index e74b5f577d..442c928b4e 100644 --- a/src/bonsai/bonsai/core/georeference.py +++ b/src/bonsai/bonsai/core/georeference.py @@ -29,6 +29,7 @@ if TYPE_CHECKING: def add_georeferencing(georeference: type[tool.Georeference]) -> None: georeference.add_georeferencing() + georeference.set_model_origin() def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None: @@ -37,8 +38,9 @@ def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None georeference.enable_editing() -def remove_georeferencing(ifc: type[tool.Ifc]) -> None: +def remove_georeferencing(ifc: type[tool.Ifc], georeference: type[tool.Georeference]) -> None: ifc.run("georeference.remove_georeferencing") + georeference.set_model_origin() def disable_editing_georeferencing(georeference: type[tool.Georeference]) -> None: diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 5d0aaf1a8e..910ca92cf4 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -251,11 +251,19 @@ class Document(bonsai.core.tool.Document): def get_document_references( cls, document: ifcopenshell.entity_instance ) -> tuple[ifcopenshell.entity_instance, ...]: + # TODO: migrate to util.document and replace all instances """Get IfcDocumentReference.ReferencedDocuments, compatible with IFC2X3.""" if document.file.schema == "IFC2X3": return document.DocumentReferences or () return document.HasDocumentReferences + @classmethod + def get_reference_document(cls, reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None: + # TODO: migrate to util.document and replace all instances + if reference.file.schema == "IFC2X3": + return (reference.ReferenceToDocument or (None))[0] + return reference.ReferencedDocument + @classmethod def clear_active_document(cls) -> None: props = cls.get_document_props() diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index eefdd12cd9..3fa3e51f62 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -315,6 +315,21 @@ class Georeference(bonsai.core.tool.Georeference): ) return coordinates + @classmethod + def global2local(cls, matrix, is_specified_in_map_units: bool) -> tuple[float, float, float]: + matrix = ifcopenshell.util.geolocation.auto_global2local(tool.Ifc.get(), matrix, is_specified_in_map_units=is_specified_in_map_units) + props = cls.get_georeference_props() + if props.has_blender_offset: + matrix = ifcopenshell.util.geolocation.global2local( + matrix, + float(props.blender_offset_x), + float(props.blender_offset_y), + float(props.blender_offset_z), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ) + return matrix + @classmethod def import_plot(cls, filepath: str) -> None: import bmesh @@ -385,6 +400,9 @@ class Georeference(bonsai.core.tool.Georeference): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) gprops = tool.Georeference.get_georeference_props() e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False) + crs = ifcopenshell.util.geolocation.get_crs(tool.Ifc.get()) or {} + gprops.model_is_georeferenced = bool(crs) + gprops.model_crs = crs.get("Name", "") or "" gprops.model_origin = f"{e},{n},{h}" gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}" angle = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get()) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index d4df4c80de..da613c3fa9 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -19,10 +19,14 @@ from __future__ import annotations import os +import json +import math import shutil +import numpy as np from collections import defaultdict +from math import radians from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple, Optional, Union +from typing import TYPE_CHECKING, NamedTuple, Optional import bpy import ifcopenshell @@ -58,6 +62,47 @@ class Project(bonsai.core.tool.Project): assert (scene := bpy.context.scene) return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue] + @classmethod + def get_link_empty_handle(cls, link) -> bpy.types.Object | None: + if tool.Ifc.get(): + return tool.Ifc.get_object(tool.Ifc.get().by_id(link.ifc_definition_id)) + return link.empty_handle + + @classmethod + def set_link_empty_handle(cls, link, empty: bpy.types.Object) -> None: + if tool.Ifc.get(): + tool.Ifc.link(tool.Ifc.get().by_id(link.ifc_definition_id), empty) + else: + link.empty_handle = empty + + @classmethod + def calculate_link_matrix(cls, link) -> None: + filepath = Path(tool.Ifc.resolve_uri(link.filepath)) + with open(filepath.with_suffix(".ifc.cache.json"), "r") as f: + metadata = json.load(f) + + rot = ifcopenshell.util.shape_builder.np_rotation_matrix( + radians(-float(metadata["model_project_north"])), 4, "Z" + ) + global_matrix = rot @ np.eye(4) + global_matrix[:, 3][:3] = [float(o) for o in metadata["model_origin_si"].split(",")] + + if tool.Ifc.get(): + transformation = tool.Ifc.get().by_id(link.ifc_definition_id)[1] # Identification + else: + transformation = link.transformation + + if transformation: + transformation = np.fromstring(transformation, sep=",", dtype=np.float64).reshape(4, 4) + if not np.allclose(transformation, np.eye(4)): + global_matrix = transformation @ global_matrix + + gprops = tool.Georeference.get_georeference_props() + rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z") + local_matrix = rot @ np.eye(4) + local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")] + return np.linalg.inv(local_matrix) @ global_matrix + @classmethod def append_all_types_from_template(cls, template: str) -> None: # TODO refactor @@ -249,68 +294,32 @@ class Project(bonsai.core.tool.Project): tool.Root.reload_grid_decorator() @classmethod - def get_linked_models_document(cls) -> Union[ifcopenshell.entity_instance, None]: - for document in tool.Ifc.get().by_type("IfcDocumentInformation"): - if document.Name == "BBIM_Linked_Models": - return document + def get_linked_models_documents(cls) -> dict[str, ifcopenshell.entity_instance]: + linked_docs = {} + for doc in tool.Ifc.get().by_type("IfcDocumentInformation"): + if doc.Scope == "LINKED_MODEL": + for reference in tool.Drawing.get_document_references(doc): + linked_docs[Path(reference.Location).as_posix()] = doc + break + return linked_docs @classmethod def load_linked_models_from_ifc(cls) -> None: links = tool.Project.get_project_props().links links.clear() - links_document = cls.get_linked_models_document() - if not links_document: - return - - references = tool.Document.get_document_references(links_document) - if not references: - return - - for reference in references: - link = links.add() - link.name = reference.Location - - @classmethod - def save_linked_models_to_ifc(cls) -> None: - ifc_file = tool.Ifc.get() - links = tool.Project.get_project_props().links - filepaths: set[Path] = set() - for link in links: - filepaths.add(Path(link.name)) - - links_document = cls.get_linked_models_document() - - if not filepaths and links_document is None: - return - - paths_to_add = filepaths.copy() - references_to_remove: list[ifcopenshell.entity_instance] = [] - if links_document: - references = tool.Document.get_document_references(links_document) - for reference in references: - # I guess got corrupted by the user. - if not (location := reference.Location): - references_to_remove.remove(reference) - continue - path = Path(location) - if path in paths_to_add: - paths_to_add.remove(path) - else: - references_to_remove.append(reference) - - if paths_to_add: - if links_document is None: - links_document = ifcopenshell.api.document.add_information(ifc_file) - links_document.Name = "BBIM_Linked_Models" - links_document.Description = "Bonsai internal document containing references to currently linked models" - - for path in paths_to_add: - reference = ifcopenshell.api.document.add_reference(ifc_file, links_document) - reference.Location = path.as_posix() - - if references_to_remove: - for reference in references_to_remove: - ifcopenshell.api.document.remove_reference(ifc_file, reference) + for doc in tool.Ifc.get().by_type("IfcDocumentInformation"): + if doc.Scope != "LINKED_MODEL": + continue + for reference in tool.Drawing.get_document_references(doc): + filepath = reference.Location + link = links.add() + link.name = filepath + link.filepath = filepath + link.ifc_definition_id = reference.id() + link.has_transformation = False + if reference[1]: + m = np.fromstring(reference[1], sep=",", dtype=np.float64).reshape(4, 4) + link.has_transformation = not np.allclose(m, np.eye(4)) @classmethod def get_project_library_elements( diff --git a/src/bonsai/test/bim/feature/project.feature b/src/bonsai/test/bim/feature/project.feature index cb9aeec21e..f536a17220 100644 --- a/src/bonsai/test/bim/feature/project.feature +++ b/src/bonsai/test/bim/feature/project.feature @@ -677,31 +677,43 @@ Scenario: Load project elements - all georeferencing coordinate situations with And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1" And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1" -Scenario: Link IFC +Scenario: Link IFC - from an empty IFC project Given an empty IFC project - When I link IFC project from "{cwd}/test/files/basic.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True" And the collection "IfcProject/basic.ifc" exists And the object "Chunk" exists And the object "Chunk" is placed in the collection "IfcProject/basic.ifc" -Scenario: Link IFC - disabled false origin mode - Given an empty IFC project - # Not currently possible via UI - And I set "scene.BIMProjectProperties.distance_limit" to "5" - And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" - Then the object "Chunk" exists - And the object "Chunk" has a vertex at "2,2,-1" - And the object "Chunk" has a vertex at "9,-1,-1" - And the object "Chunk" has a vertex at "17,4,-1" - Scenario: Link IFC - from an empty IFC project - automatic false origin mode (0,0,0 will be the false origin) Given an empty IFC project # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + Then the object "Chunk" exists + And the object "Chunk" has a vertex at "2,2,-1" + And the object "Chunk" has a vertex at "9,-1,-1" + And the object "Chunk" has a vertex at "17,4,-1" + +Scenario: Link IFC - from an empty IFC project - manual false origin mode + Given an empty IFC project + # Not currently possible via UI + And I set "scene.BIMProjectProperties.distance_limit" to "5" + And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL" + And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + Then the object "Chunk" exists + And the object "Chunk" has a vertex at "2,2,-1" + And the object "Chunk" has a vertex at "9,-1,-1" + And the object "Chunk" has a vertex at "17,4,-1" + +Scenario: Link IFC - from an empty IFC project - disabled false origin mode + Given an empty IFC project + # Not currently possible via UI + And I set "scene.BIMProjectProperties.distance_limit" to "5" + And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Chunk" exists And the object "Chunk" has a vertex at "2,2,-1" And the object "Chunk" has a vertex at "9,-1,-1" @@ -712,31 +724,42 @@ Scenario: Link IFC - from an empty Blender session - automatic false origin mode # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Chunk" exists And the object "Chunk" has a vertex at "-11,-2,0" And the object "Chunk" has a vertex at "-4,-5,0" And the object "Chunk" has a vertex at "4,0,0" -Scenario: Link IFC - manual false origin mode +Scenario: Link IFC - from an empty Blender session - manual false origin mode Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL" And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Chunk" exists And the object "Chunk" has a vertex at "-8,2,-1" And the object "Chunk" has a vertex at "-1,-1,-1" And the object "Chunk" has a vertex at "7,4,-1" -Scenario: Link IFC - automatic false origin mode - two different false origins and project norths - grid north is up because we start with geolocation.ifc +Scenario: Link IFC - from an empty Blender session - disabled false origin mode + Given an empty Blender session + # Not currently possible via UI + And I set "scene.BIMProjectProperties.distance_limit" to "5" + And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + Then the object "Chunk" exists + And the object "Chunk" has a vertex at "2,2,-1" + And the object "Chunk" has a vertex at "9,-1,-1" + And the object "Chunk" has a vertex at "17,4,-1" + +Scenario: Link IFC - from an empty Blender session - automatic false origin mode - two different false origins and project norths - grid north is up because we start with geolocation.ifc Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation.ifc" - And I link IFC project from "{cwd}/test/files/geolocation-mapconversion-angle.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-mapconversion-angle.ifc', use_cache=False)" Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-11,-2,0" @@ -746,13 +769,13 @@ Scenario: Link IFC - automatic false origin mode - two different false origins a And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "9.294,-9.366,0" And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "18.722,-9.036,0" -Scenario: Link IFC - automatic false origin mode - two different false origins and project norths - project north is up because we start with geolocation-mapconversion-angle.ifc +Scenario: Link IFC - from an empty Blender session - automatic false origin mode - two different false origins and project norths - project north is up because we start with geolocation-mapconversion-angle.ifc Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation-mapconversion-angle.ifc" - And I link IFC project from "{cwd}/test/files/geolocation.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-mapconversion-angle.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)" Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "-11,-2,0" @@ -762,14 +785,14 @@ Scenario: Link IFC - automatic false origin mode - two different false origins a And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-17.696,-7.866,0" And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-13.268,0.464,0" -Scenario: Link IFC - automatic false origin mode - three identical false origins but different project and map units +Scenario: Link IFC - from an empty Blender session - automatic false origin mode - three identical false origins but different project and map units Given an empty Blender session # Not currently possible via UI And I set "scene.BIMProjectProperties.distance_limit" to "5" And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC" - When I link IFC project from "{cwd}/test/files/geolocation-unit1.ifc" - And I link IFC project from "{cwd}/test/files/geolocation-unit2.ifc" - And I link IFC project from "{cwd}/test/files/geolocation-unit3.ifc" + When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit1.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit2.ifc', use_cache=False)" + And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit3.ifc', use_cache=False)" Then the object "Col:IfcProject/geolocation-unit1.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-unit2.ifc:Chunk" exists And the object "Col:IfcProject/geolocation-unit3.ifc:Chunk" exists @@ -779,54 +802,54 @@ Scenario: Link IFC - automatic false origin mode - three identical false origins Scenario: Toggle link visibility - wireframe mode Given an empty IFC project - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_visibility(link_index=0, mode='WIREFRAME')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True" And the object "Chunk" should display as "WIRE" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')" + When I press "bim.toggle_link_visibility(link_index=0, mode='WIREFRAME')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False" And the object "Chunk" should display as "TEXTURED" Scenario: Toggle link selectability Given an empty IFC project - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_selectability(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False" And the collection "IfcProject/basic.ifc" is unselectable - When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_selectability(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True" And the collection "IfcProject/basic.ifc" is selectable Scenario: Toggle link visibility - visible mode Given an empty IFC project - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.toggle_link_visibility(link_index=0, mode='VISIBLE')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True" And the object "IfcProject/basic.ifc" is not visible - When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')" + When I press "bim.toggle_link_visibility(link_index=0, mode='VISIBLE')" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False" And the object "IfcProject/basic.ifc" is visible Scenario: Unload link Given an empty Blender session - And I link IFC project from "{cwd}/test/files/basic.ifc" - When I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + When I press "bim.unload_link(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False" And the collection "IfcProject/basic.ifc" does not exist Scenario: Load link Given an empty Blender session - And I link IFC project from "{cwd}/test/files/basic.ifc" - And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" - When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.unload_link(link_index=0)" + When I press "bim.load_link(link_index=0)" Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True" And the object "IfcProject/basic.ifc" exists Scenario: Unlink IFC Given an empty Blender session - And I link IFC project from "{cwd}/test/files/basic.ifc" - And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')" - When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')" + And I press "bim.unload_link(link_index=0)" + When I press "bim.unlink_ifc(link_index=0)" Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None" And "scene.collection.children.get('IfcProject/basic.ifc')" is "None" And the object "Chunk" does not exist diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 9df6bbf627..a6d93c0006 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -70,10 +70,7 @@ Can be useful for debugging, but has caveats - can't use ``wm.read_homefile`` as resets the ``bpy.context`` and some it's members become `None`. """ -TMP = Path.cwd() / "test/files/temp" -TEST_FILES_DIR = Path.cwd() / "test/files" - -CLEAN_LINKED_FILES_CACHE = False +TMP = Path(f"{variables['cwd']}/test/files/temp") EPSET_DRAWING = Path.cwd() / "bonsai/bim/data/pset/EPset_Drawing.ifc" EPSET_DRAWING_BYTES = EPSET_DRAWING.read_bytes() @@ -311,25 +308,6 @@ def vectors_are_equal(v1, v2): return all(is_x(v1[i], v2[i]) for i in range(len(v1))) -@pytest.fixture(scope="function", autouse=True) -def run_for_each_test() -> Generator[None]: - # Code before this runs before each test - yield - # Code after this runs after each test - - global CLEAN_LINKED_FILES_CACHE - if CLEAN_LINKED_FILES_CACHE: - for filepath in TEST_FILES_DIR.glob("*.ifc.cache.*"): - filepath.unlink() - CLEAN_LINKED_FILES_CACHE = False - - # pset_template tests are editing EPset_Drawing.ifc, so we need to restore it. - global RELOAD_EPSET_DRAWING - if RELOAD_EPSET_DRAWING: - EPSET_DRAWING.write_bytes(EPSET_DRAWING_BYTES) - RELOAD_EPSET_DRAWING = False - - @given("an untestable scenario") def an_untestable_scenario(): pass @@ -384,16 +362,6 @@ def saving_ifc_project() -> None: tool.Project.save_test_project() -@given(parsers.parse('I link IFC project from "{filepath}"')) -@when(parsers.parse('I link IFC project from "{filepath}"')) -@then(parsers.parse('I link IFC project from "{filepath}"')) -def i_link_ifc_project_from_filepath(filepath: str) -> None: - global CLEAN_LINKED_FILES_CACHE - filepath = replace_variables(filepath) - CLEAN_LINKED_FILES_CACHE = True - bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False) - - @given("the Brickschema is stubbed") def the_brickschema_is_stubbed(): # This makes things run faster since we don't need to load the entire brick schema @@ -1587,10 +1555,19 @@ def the_object_name_has_a_vertex_at_location(name, location): is_pass = False target = Vector([float(co) for co in location.split(",")]) verts = [] - for v in obj.data.vertices: - verts.append(obj.matrix_world @ v.co) - if (verts[-1] - target).length < 0.001: - is_pass = True + depsgraph = bpy.context.evaluated_depsgraph_get() + obj_eval = obj.evaluated_get(depsgraph) + mesh = obj_eval.to_mesh(preserve_all_data_layers=False, depsgraph=depsgraph) + try: + for inst in depsgraph.object_instances: + if inst.object.original is obj: + mw = inst.matrix_world + for i, v in enumerate(mesh.vertices): + verts.append(mw @ v.co) + if (verts[-1] - target).length < 0.001: + is_pass = True + finally: + obj_eval.to_mesh_clear() assert is_pass, f"No verts found at {location}: {verts}" diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 06c3f8ec25..43dc61be8e 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -16,9 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import json import contextlib import tempfile +import numpy as np from pathlib import Path +from tempfile import NamedTemporaryFile import bpy import ifcopenshell @@ -263,7 +266,7 @@ class TestLoadLinkedModels(NewFile): props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" + document.Name = "X" tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() assert len(props.links) == 0 @@ -273,86 +276,81 @@ class TestLoadLinkedModels(NewFile): props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" + document.Scope = "LINKED_MODEL" reference = ifcopenshell.api.document.add_reference(ifc, document) - linked_model_path = "test.ifc" - reference.Location = linked_model_path + reference.Location = "test.ifc" + reference.Identification = "" + reference2 = ifcopenshell.api.document.add_reference(ifc, document) + reference2.Location = "test2.ifc" + reference2.Identification = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16" tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(props.links) == 1 - assert props.links[0].name == linked_model_path + assert len(props.links) == 2 + assert props.links[0].name == "test.ifc" + assert props.links[0].ifc_definition_id == reference.id() + assert props.links[0].has_transformation is False + assert props.links[1].name == "test2.ifc" + assert props.links[1].ifc_definition_id == reference2.id() + assert props.links[1].has_transformation is True -class TestSaveLinkedModelsToIfc(NewFile): - def test_save_linked_models_to_ifc_no_links(self): - ifc = ifcopenshell.file() - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - assert len(ifc.by_type("IfcDocumentInformation")) == 0 - assert len(ifc.by_type("IfcDocumentReference")) == 0 - - def test_save_linked_models_to_ifc_paths_to_add(self): - ifc = ifcopenshell.file() - ifcopenshell.api.root.create_entity(ifc, "IfcProject") +class TestCalculateLinkMatrix(NewFile): + def test_linking_a_model_without_an_offset_to_our_session_with_no_offset(self): props = tool.Project.get_project_props() - link = props.links.add() - linked_model_path = "test.ifc" - link.name = linked_model_path - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 - assert documents[0].Name == "BBIM_Linked_Models" - assert len(references := ifc.by_type("IfcDocumentReference")) == 1 - assert references[0].Location == linked_model_path + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + json.dump({"model_project_north": "0", "model_origin_si": "0,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "0,0,0" + assert np.allclose(subject.calculate_link_matrix(link), np.eye(4)) - def test_save_linked_models_to_ifc_already_created_references(self): - ifc = ifcopenshell.file() - links = tool.Project.get_project_props().links - ifcopenshell.api.root.create_entity(ifc, "IfcProject") + def test_linking_an_offset_model_to_our_session_with_no_offset(self): + props = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "0,0,0" + m = np.eye(4) + m[0][3] = 5 + assert np.allclose(subject.calculate_link_matrix(link), m) - document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" - document_id = document.id() - reference = ifcopenshell.api.document.add_reference(ifc, document) - linked_model_path = "test.ifc" - reference.Location = linked_model_path - reference_id = reference.id() + def test_linking_an_offset_model_to_our_session_with_offset(self): + props = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "2,0,0" + m = np.eye(4) + m[0][3] = 3 + assert np.allclose(subject.calculate_link_matrix(link), m) - link = links.add() - linked_model_path = "test.ifc" - link.name = linked_model_path - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - - # Information and references to stay intact. - assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 - assert documents[0].id() == document_id - assert documents[0].Name == "BBIM_Linked_Models" - assert len(references := ifc.by_type("IfcDocumentReference")) == 1 - assert references[0].id() == reference_id - assert references[0].Location == linked_model_path - - def test_save_linked_models_to_ifc_references_to_remove(self): - ifc = ifcopenshell.file() - links = tool.Project.get_project_props().links - ifcopenshell.api.root.create_entity(ifc, "IfcProject") - - document = ifcopenshell.api.document.add_information(ifc) - document.Name = "BBIM_Linked_Models" - document_id = document.id() - reference = ifcopenshell.api.document.add_reference(ifc, document) - linked_model_path = "test.ifc" - reference.Location = linked_model_path - - tool.Ifc.set(ifc) - subject.save_linked_models_to_ifc() - links.clear() - - # Remove reference for removed link. - assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1 - assert documents[0].id() == document_id - assert documents[0].Name == "BBIM_Linked_Models" - assert len(ifc.by_type("IfcDocumentReference")) == 0 + def test_linking_an_offset_model_to_our_session_with_offset_and_transformation(self): + props = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() + with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp: + link = props.links.add() + link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc") + transformation = np.eye(4) + transformation[0][3] = 4 + link.transformation = ",".join(map(str, transformation.reshape(-1))) + json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp) + tmp.flush() + gprops.model_project_north = "0" + gprops.model_origin_si = "2,0,0" + m = np.eye(4) + m[0][3] = 7 + assert np.allclose(subject.calculate_link_matrix(link), m) class TestLoadingIfcSqlite(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index fba8dfffaf..42aa28ca51 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -18,7 +18,7 @@ import math from decimal import ROUND_HALF_UP, Decimal -from typing import NamedTuple, Optional, Union +from typing import NamedTuple, Optional, Union, Any import numpy as np @@ -268,6 +268,15 @@ def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Option return HelmertTransformation(e, n, h, xaa, xao, scale, factor_x, factor_y, factor_z) +def get_crs(ifc_file: ifcopenshell.file) -> dict[str, Any]: + """Get CRS information from an IFC file.""" + if ifc_file.schema == "IFC2X3": + return ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], "ePSet_ProjectedCRS") + for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if operation := context.HasCoordinateOperation: + return operation[0].TargetCRS.get_info() + + def auto_z2e(ifc_file: ifcopenshell.file, z: float, should_return_in_map_units: bool = True) -> float: """Convert a Z coordinate to an elevation using model georeferencing data From 65d5df7801678158eee63193c2e529f32343ea63 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:15:04 -0800 Subject: [PATCH 014/131] IfcAxis2PlacementLinear mapping used with IfcSectionedSurface and IfcSectionedSolidHorizontal IfcSectionedSurface and IfcSectionedSolidHorizontal both of CrossSectionPositions attributes which are lists of IfcAxis2PlacementLinear. The implementation of each class used its own bespoke mapping of IfcAxis2PlacementLinear, which were identical to each other and slightly different than IfcAxis2PlacementLinear. Now the two sectioned classes use the one and only mapping for IfcAxis2PlacementLinear --- .../mapping/IfcSectionedSolidHorizontal.cpp | 30 ++--------------- src/ifcgeom/mapping/IfcSectionedSurface.cpp | 33 +++---------------- 2 files changed, 7 insertions(+), 56 deletions(-) diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index a2bc648f08..ad1e2e0d4c 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -61,33 +61,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); - // Corresponds to the profile X, Y directions (hopefully). - Eigen::Vector3d po( - pbde->OffsetLateral().get_value_or(0.), - // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane - pbde->OffsetVertical().get_value_or(0.), - 0. - ); - - profile_offsets.push_back(po); - - boost::optional rot; - if (csp->Axis() && csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents(), - taxonomy::cast(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3,3>(0,0); - } else if (csp->Axis()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } else if (csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - Eigen::Vector3d(0, 0, 1), - taxonomy::cast(map(csp->RefDirection()))->ccomponents() - ).ccomponents().block<3, 3>(0, 0); - } + auto linear_placement = taxonomy::cast(map(csp)); + profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3)); + boost::optional rot(linear_placement->ccomponents().block<3,3>(0,0)); profile_rotations.push_back(rot); } if (faces.size() != profile_offsets.size()) { diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp index 49fde15759..b91b7b62c2 100644 --- a/src/ifcgeom/mapping/IfcSectionedSurface.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp @@ -63,35 +63,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); - // Corresponds to the profile X, Y directions (hopefully). - Eigen::Vector3d po( - pbde->OffsetLateral().get_value_or(0.), - // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane - pbde->OffsetVertical().get_value_or(0.), - 0. - ); - - profile_offsets.push_back(po); - - boost::optional rot; - if (csp->Axis() && csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents(), - taxonomy::cast(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } else if (csp->Axis()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } else if (csp->RefDirection()) { - rot = taxonomy::matrix4( - Eigen::Vector3d(0, 0, 0), - Eigen::Vector3d(0, 0, 1), - taxonomy::cast(map(csp->RefDirection()))->ccomponents()) - .ccomponents() - .block<3, 3>(0, 0); - } - profile_rotations.push_back(rot); + auto linear_placement = taxonomy::cast(map(csp)); + profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3)); + boost::optional rot(linear_placement->ccomponents().block<3, 3>(0, 0)); + profile_rotations.push_back(rot); } #else return nullptr; From 1d1f158fe231f8cbb9b4a5a2082fb9ac6802005d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Feb 2026 08:23:35 +1100 Subject: [PATCH 015/131] Remove no longer relevant invoke code for linking IFCs --- .../bonsai/bim/module/project/operator.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index e4e57275ba..1d0e52976b 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1370,29 +1370,6 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator): row = self.layout.row() row.prop(pprops, "project_north") - def invoke(self, context, event): - pprops = tool.Project.get_project_props() - - # Populate false origin and project north from 3D cursor for user convenience - cursor = context.scene.cursor - cursor_loc = cursor.location - cursor_rot = cursor.rotation_euler - angle = -cursor_rot.z - - # Calculate false origin based on the provided formulas - # x = -3Dcursor.x * cos(angle) - 3Dcursor.y * sin(angle) - # y = 3Dcursor.x * sin(angle) - 3Dcursor.y * cos(angle) - # z = -3Dcursor.z - false_origin_x = -cursor_loc.x * math.cos(angle) - cursor_loc.y * math.sin(angle) - false_origin_y = cursor_loc.x * math.sin(angle) - cursor_loc.y * math.cos(angle) - false_origin_z = -cursor_loc.z - - # Set the false_origin value - pprops.false_origin = f"{false_origin_x:.3f},{false_origin_y:.3f},{false_origin_z:.3f}" - pprops.project_north = str(round(math.degrees(angle), 1)) - - return super().invoke(context, event) - def _execute(self, context): start = time.time() files = [f.name for f in self.files] if self.files else [self.filepath] From 379c74b31fceb50ead27d7a7c9c0e3dccc489c5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:45:21 +0000 Subject: [PATCH 016/131] Bump ruff from 0.15.0 to 0.15.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.0 to 0.15.1. - [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.0...0.15.1) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.1 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 74c3b0ee18..95b61e2c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "IfcOpenShell" version = "0.0.0" dependencies = [ "black==26.1.0", - "ruff==0.15.0", + "ruff==0.15.1", "poethepoet", "gersemi==0.25.4", ] From 8cfb162851888202d03b3812552d4f306cedc3cb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Feb 2026 17:57:03 +1100 Subject: [PATCH 017/131] Fix #7656. Regression in text editing where leaders were accidentally removed. Added tests. --- src/bonsai/bonsai/tool/drawing.py | 6 ++++-- src/bonsai/test/tool/test_drawing.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index a7238c75dc..864568c835 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -857,9 +857,11 @@ class Drawing(bonsai.core.tool.Drawing): def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None: assert (element := tool.Ifc.get_entity(obj)) assert (rep := cls.get_annotation_representation(element)) - for literal in cls.get_text_literal(obj, return_list=True): + to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")] + new_literals = [cls.add_literal(**a) for a in literal_attributes] + rep.Items = [i for i in rep.Items if not i.is_a("IfcTextLiteral")] + new_literals + for literal in to_remove: ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), literal) - rep.Items = [cls.add_literal(**a) for a in literal_attributes] @classmethod def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance: diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index f7beb94358..532186d7ff 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -21,6 +21,7 @@ import xml.etree.ElementTree as ET from pathlib import Path import bpy +import pytest import ifcopenshell import ifcopenshell.api.drawing import ifcopenshell.api.group @@ -31,6 +32,7 @@ import ifcopenshell.util.element import mathutils import numpy as np from mathutils import Vector +from ifcopenshell.util.shape_builder import ShapeBuilder import bonsai.core.tool import bonsai.tool as tool @@ -150,6 +152,34 @@ class TestDisableEditingSheets(NewFile): assert props.is_editing_sheets == False +class TestEditTextLiterals(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + obj = bpy.data.objects.new("Object", None) + element = ifc.createIfcAnnotation() + element.Representation = ifc.createIfcProductDefinitionShape() + context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation") + item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left") + builder = ShapeBuilder(tool.Ifc.get()) + polyline = builder.polyline([(0.,0.,0.), (1.,0.,0.)]) + representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item, polyline]) + element.Representation.Representations = [representation] + tool.Ifc.link(element, obj) + literal_attributes = [ + { + "Literal": "Foo", + "Path": "RIGHT", + "BoxAlignment": "bottom-left", + } + ] + subject.edit_text_literals(obj, literal_attributes) + assert len(ifc.by_type("IfcTextLiteralWithExtent")) == 1 + literal = ifc.by_type("IfcTextLiteralWithExtent")[0] + assert literal in representation.Items + assert literal.Literal == "Foo" + + class TestDisableEditingText(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) From 8c0bed0c614800d3bf118ec35ae24a76b3fd3a92 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Feb 2026 18:24:37 +1100 Subject: [PATCH 018/131] Stub open command so running tests doesn't keep on launching apps --- src/bonsai/bonsai/bim/module/drawing/operator.py | 1 + src/bonsai/test/bim/bootstrap.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index db389427e6..2f858a9d9e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3165,6 +3165,7 @@ class EditTextPopup(bpy.types.Operator): bpy.ops.bim.disable_editing_text() def execute(self, context): + # TODO: check for possible subtle undo bug here # can't use invoke() because this operator # will be run indirectly by hotkey # so we use execute() and track whether it's the first run of the operator diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index c799c74c00..587b8f5a2d 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -34,6 +34,7 @@ from bonsai.bim.ifc import IfcStore # Monkey-patch webbrowser opening since we want to test headlessly webbrowser.open = lambda x: True +tool.Drawing.open_with_user_command = lambda x, y: True variables = {"cwd": os.getcwd(), "ifc": "IfcStore.get_file()"} From e20e286168092ce52d1ea8da417a60f66c7f50d0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Feb 2026 18:25:54 +1100 Subject: [PATCH 019/131] Revert "feat(drawing): support multiple file selection in Add Reference" This reverts commit cf5ffad9af7dc911d1a3c9a90767fb8c950f7dbc. --- .../bonsai/bim/module/drawing/operator.py | 15 +--- src/bonsai/test/tool/test_drawing.py | 68 ------------------- 2 files changed, 2 insertions(+), 81 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 2f858a9d9e..abc6bbfb5c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3094,20 +3094,9 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) filename_ext = ".svg" - files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement) - directory: bpy.props.StringProperty(subtype="DIR_PATH") - def _execute(self, context): - # Handle both single and multiple file selection - if self.files: - for file_elem in self.files: - filepath = os.path.join(self.directory, file_elem.name) - uri = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri) - else: - # Fallback for single file (backward compatibility) - filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) + filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) + core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) class RemoveReference(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 532186d7ff..8501b5b9ea 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -958,71 +958,3 @@ class TestAddReferenceImage(NewFile): uv_node = material_nodes["Texture Coordinate"] assert len(uv_node.outputs["Generated"].links[:]) == 1 - - -class TestAddReference(NewFile): - def test_add_single_reference(self): - """Test adding a single reference file (backward compatibility)""" - bpy.ops.bim.create_project() - ifc_path = Path("test/files/temp/test.ifc").absolute() - bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) - - # Create a temporary SVG file - svg_path = Path("test/files/temp/reference.svg").absolute() - svg_path.parent.mkdir(parents=True, exist_ok=True) - with open(svg_path, "w") as f: - f.write('') - - try: - # Add single reference - bpy.ops.bim.add_reference(filepath=str(svg_path)) - - # Verify reference was added - ifc = tool.Ifc.get() - references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"] - assert len(references) == 1 - assert references[0].Name == "reference" - finally: - # Cleanup - if svg_path.exists(): - svg_path.unlink() - - def test_add_multiple_references(self): - """Test adding multiple reference files at once""" - bpy.ops.bim.create_project() - ifc_path = Path("test/files/temp/test.ifc").absolute() - bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) - - # Create temporary SVG files - temp_dir = Path("test/files/temp").absolute() - temp_dir.mkdir(parents=True, exist_ok=True) - - svg_files = [] - for i in range(3): - svg_path = temp_dir / f"reference_{i}.svg" - with open(svg_path, "w") as f: - f.write('') - svg_files.append(svg_path) - - try: - # Test by directly calling core.add_document multiple times - # (simulating what the operator does with multiple files) - ifc = tool.Ifc.get() - for svg_file in svg_files: - uri = tool.Ifc.get_uri(str(svg_file), use_relative_path=True) - from bonsai.bim import core - - core.drawing.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri) - - # Verify all references were added - references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"] - assert len(references) == 3 - - reference_names = {ref.Name for ref in references} - expected_names = {f"reference_{i}" for i in range(3)} - assert reference_names == expected_names - finally: - # Cleanup - for svg_file in svg_files: - if svg_file.exists(): - svg_file.unlink() From 37fe0ad9934a3bd427efa7027f79d5c47f582702 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 17 Feb 2026 11:18:24 +1100 Subject: [PATCH 020/131] Reimplement adding multiple references / schedules cf5ffad9af7dc911d1a3c9a90767fb8c950f7dbc Previously it was implemented inline. This now implements it as a tool.Blender function with tests. Also the previous tests didn't actually run and weren't actually testing any tools despite being in a tool tests. --- .../bonsai/bim/module/drawing/operator.py | 16 ++++++--- src/bonsai/bonsai/tool/blender.py | 10 ++++++ src/bonsai/test/tool/test_blender.py | 34 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index abc6bbfb5c..f92f7e0085 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -2896,12 +2896,16 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_options = {"REGISTER", "UNDO"} bl_description = "Add an .ods, .xls or .xlsx file as a schedule" + files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) + directory: bpy.props.StringProperty(subtype="DIR_PATH") filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) def _execute(self, context): - filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath) + for filepath in tool.Blender.get_selected_files( + self.directory, self.files, use_relative_path=self.use_relative_path + ): + core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath) class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator): @@ -3090,13 +3094,17 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_description = "Import a .svg file to the project as a reference" bl_options = {"REGISTER", "UNDO"} + files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) + directory: bpy.props.StringProperty(subtype="DIR_PATH") filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) filename_ext = ".svg" def _execute(self, context): - filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) + for filepath in tool.Blender.get_selected_files( + self.directory, self.files, use_relative_path=self.use_relative_path + ): + core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) class RemoveReference(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index e499f35106..a2a0f09ed8 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -2165,3 +2165,13 @@ class Blender(bonsai.core.tool.Blender): if cls.BLENDER_5: return np.array(mathutils_type) return np.array(mathutils_type, dtype=np.float32) + + @classmethod + def get_selected_files( + cls, directory: str, files: bpy.types.OperatorFileListElement, use_relative_path=False + ) -> list[Path]: + return [ + tool.Ifc.get_uri(Path(directory) / f.name, use_relative_path=use_relative_path) + for f in files + if (Path(directory) / f.name).is_file() + ] diff --git a/src/bonsai/test/tool/test_blender.py b/src/bonsai/test/tool/test_blender.py index 43d04c7c19..97e68b9d08 100644 --- a/src/bonsai/test/tool/test_blender.py +++ b/src/bonsai/test/tool/test_blender.py @@ -24,8 +24,10 @@ import pytest import bonsai.core.tool import bonsai.tool as tool +import tempfile from bonsai.tool.blender import Blender as subject from test.bim.bootstrap import NewFile +from pathlib import Path if TYPE_CHECKING: import bpy.stub_internal.rna_enums as rna_enums @@ -110,3 +112,35 @@ class TestBlenderErrorMessageExtraction(NewFile): assert error_reports == [] bpy.utils.unregister_class(OBJECT_OT_test_fail_operator) + + +class TestGetSelectedFiles(NewFile): + def test_get_a_single_file(self) -> None: + with tempfile.NamedTemporaryFile() as f: + file = type("", (object,), {"name": f.name})() + assert subject.get_selected_files(Path(f.name).parent, [file]) == [f.name] + + def test_get_multiple_files(self) -> None: + with tempfile.NamedTemporaryFile() as f: + with tempfile.NamedTemporaryFile() as g: + file = type("", (object,), {"name": f.name})() + file2 = type("", (object,), {"name": g.name})() + assert subject.get_selected_files(Path(f.name).parent, [file, file2]) == [f.name, g.name] + + def test_exclude_directories(self) -> None: + with tempfile.NamedTemporaryFile() as f: + with tempfile.TemporaryDirectory() as d: + file = type("", (object,), {"name": f.name})() + directory = type("", (object,), {"name": d})() + assert subject.get_selected_files(Path(f.name).parent, [file, directory]) == [f.name] + + def test_get_relative_paths(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + base_path = Path(tmp_dir) + with tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".ifc") as f: + tool.Ifc.set_path(str(f.name)) + with tempfile.NamedTemporaryFile(dir=tmp_dir) as g: + file = type("", (object,), {"name": g.name}) + assert subject.get_selected_files(Path(g.name).parent, [file], use_relative_path=True) == [ + Path(g.name).name + ] From c6b14d14747088be9b350c76d19cf6130a4c7036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Aliste?= Date: Mon, 16 Feb 2026 23:33:48 -0300 Subject: [PATCH 021/131] Fixes snap angle. In my previous commit, I mistakenly believed that there was an API change from snap_angle_increment to snap_angle_increment_3d But since the feature was introduced in blender 4.2 the setting is called snap_angle_increment_3d. --- src/bonsai/bonsai/tool/snap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index bc35505826..0dc11e86b0 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -118,14 +118,10 @@ class Snap(bonsai.core.tool.Snap): def get_angle_snap_value(cls, context: bpy.types.Context) -> float: """Get the angle snap increment from Blender's tool settings. - Uses snap_angle_increment_3d (Blender 5.0+) or snap_angle_increment (Blender 4.x). - :param context: Blender context :return: Angle snap increment in degrees """ - if bpy.app.version >= (5, 0, 0): - return math.degrees(context.scene.tool_settings.snap_angle_increment_3d) - return math.degrees(context.scene.tool_settings.snap_angle_increment) + return math.degrees(context.scene.tool_settings.snap_angle_increment_3d) @classmethod def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): From fcc80ad14a66468d3a353124bd4fd3a30e9875a2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 17 Feb 2026 18:11:13 +1100 Subject: [PATCH 022/131] Simplify add reference image size implementation and fix segfaulting tests Previously, there was a dance between invoke, execute, and draw. This can probably be resolved, but is a high-risk for undo bugs. This simplifies the logic flow to just a traditional _invoke -> _execute. I add a new feature test to at least make sure it does something, and this also fixes the segfault in tool tests as it no longer requires the launching of the file browser. --- .../bonsai/bim/module/drawing/operator.py | 101 +++--------------- src/bonsai/test/bim/feature/drawing.feature | 13 +-- src/bonsai/test/tool/test_drawing.py | 2 +- 3 files changed, 23 insertions(+), 93 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index f92f7e0085..144a687af6 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3815,91 +3815,14 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): description="Existing object name to add a style with reference image to. If not provided will create a new object.", options={"SKIP_SAVE"}, ) - - x_length: bpy.props.FloatProperty( - name="X Length", - description="Width of the reference image in project units", - default=1.0, - min=0.001, - soft_min=0.01, - precision=3, - ) - y_length: bpy.props.FloatProperty( - name="Y Length", - description="Height of the reference image in project units", - default=1.0, - min=0.001, - soft_min=0.01, - precision=3, - ) - - show_dimensions_dialog: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) + size: bpy.props.FloatProperty(name="Size", description="Size of the reference image", default=1.0, unit="LENGTH") def draw(self, context): - layout = self.layout - - if getattr(self, "show_dimensions_dialog", False): - if tool.Ifc.get(): - length_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT") - if length_unit: - unit_name = ifcopenshell.util.unit.get_full_unit_name(length_unit).lower() - else: - unit_name = "project units" - layout.label(text=f"Set Reference Image Dimensions (in {unit_name}):") - else: - layout.label(text="Set Reference Image Dimensions (in project units):") - layout.separator() - layout.prop(self, "x_length") - layout.prop(self, "y_length") - else: - if Path(tool.Ifc.get_path()).is_file(): - layout.prop(self, "use_relative_path") - else: - self.use_relative_path = False - layout.label(text="Save the .ifc file first ") - layout.label(text="to use relative paths.") - layout.prop(self, "override_existing_image") - layout.prop(self, "use_existing_object_by_name") - - def invoke(self, context, event): - if not getattr(self, "show_dimensions_dialog", False): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - else: - return context.window_manager.invoke_props_dialog(self) - - def execute(self, context): - if not getattr(self, "show_dimensions_dialog", False): - abs_path = Path(self.filepath).absolute().resolve() - if self.override_existing_image: - params = {"check_existing": True, "force_reload": True} - else: - params = {"check_existing": False} - - try: - image = load_image(abs_path.name, str(abs_path.parent), **params) - - image_width_px = image.size[0] - image_height_px = image.size[1] - aspect_ratio = image_width_px / image_height_px - - if aspect_ratio >= 1.0: - self.x_length = 1.0 - self.y_length = 1.0 / aspect_ratio - else: - self.x_length = aspect_ratio - self.y_length = 1.0 - - bpy.data.images.remove(image) - - except Exception as e: - self.report({"ERROR"}, f"Failed to load image: {str(e)}") - return {"CANCELLED"} - - self.show_dimensions_dialog = True - return context.window_manager.invoke_props_dialog(self) - - return self._execute(context) + if Path(tool.Ifc.get_path()).is_file(): + self.layout.prop(self, "use_relative_path") + self.layout.prop(self, "override_existing_image") + self.layout.prop(self, "use_existing_object_by_name") + self.layout.prop(self, "size") def _execute(self, context): space = tool.Blender.get_view3d_space() @@ -3920,11 +3843,19 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): params = {"check_existing": False} image = load_image(abs_path.name, str(abs_path.parent), **params) + aspect_ratio = image.size[0] / image.size[1] + if aspect_ratio >= 1.0: # Landscape + x_length = self.size + y_length = self.size / aspect_ratio + else: + x_length = self.size / aspect_ratio + y_length = self.size + def bm_add_image_plane(mesh): bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - plane_scale = Vector((self.x_length * unit_scale / 2.0, self.y_length * unit_scale / 2.0, 1.0)) + plane_scale = Vector((x_length / 2.0, y_length / 2.0, 1.0)) matrix = Matrix.LocRotScale(None, None, plane_scale) bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False) @@ -4028,8 +3959,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): tool.Style.reload_material_from_ifc(material) tool.Geometry.record_object_materials(obj) - return {"FINISHED"} - class ConvertSVGToDXF(bpy.types.Operator): bl_idname = "bim.convert_svg_to_dxf" diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index 6a24168e4b..b7b78e748d 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -3,12 +3,6 @@ Feature: Drawing Scenario: Duplicate drawing Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I look at the "Class" panel - And I set the "Products" property to "IfcElement" - And I set the "Class" property to "IfcWall" - And I click "Assign IFC Class" And I save IFC project And I look at the "Drawings" panel And I click "IMPORT" @@ -315,3 +309,10 @@ Scenario: Create sheet - with a drawing added to it And I click "IMAGE_PLANE" When I click "OUTPUT" Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall" + +Scenario: Add reference image + Given an empty IFC project + And I save IFC project + When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg')" + Then the object "IfcAnnotation/image" exists + And the object "IfcAnnotation/image" dimensions are "1.0,0.565,0." diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 8501b5b9ea..b0d2756198 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -938,7 +938,7 @@ class TestAddReferenceImage(NewFile): obj = bpy.data.objects["IfcAnnotation/image"] assert obj is not None - assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0))) + assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((1.0, 0.565, 0.0))) material = obj.active_material assert material From 8023a992da750b44fd738fec6218e3c440e820fb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 17 Feb 2026 18:16:20 +1100 Subject: [PATCH 023/131] Fix tests where panel name and tab panel name is identical For now probably just easier to skip tabs. They are just containers and not worth testing. Famous last words :) --- src/bonsai/test/bim/test_feature.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index a6d93c0006..262dce65ed 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -280,6 +280,8 @@ def create_ui_name_cache(): try: panel_type = getattr(bpy.types, bl_idname) if panel_type.bl_rna.base.name == "Panel": + if "_tab_" in panel_type.bl_idname: + continue # Tab panels are just groups and not relevant in testing ui_name_cache[panel_type.bl_label] = panel_type.bl_idname elif panel_type.bl_rna.base.name == "Operator": ui_name_cache[panel_type.bl_label] = bl_idname From ed500d58ba3cb093bc56beacef62eed5514b4f2f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 17 Feb 2026 18:16:33 +1100 Subject: [PATCH 024/131] For consistency, maxfail=1 for module tool tests --- src/bonsai/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 6b4704b7b3..38fbc61355 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -371,7 +371,7 @@ test-tool: ifndef MODULE pytest test/tool else - pytest test/tool/test_$(MODULE).py + pytest test/tool/test_$(MODULE).py --maxfail=1 endif # Reregistering test is not added to the standard test suite because during unregister From 291e815770a9d6d3aa0d3c1bde13f1d14f3b3a7f Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 13 Feb 2026 18:24:46 +0000 Subject: [PATCH 025/131] Add AGENTS.md contributor guide Guidelines for external contributors using AI coding tools, covering licensing, AI disclosure requirements, PR scope, commit style, code formatting, and testing expectations. Generated with the assistance of an AI coding tool. --- AGENTS.md | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..fd0e305245 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,153 @@ + + +# AGENTS.md + +Guidelines for AI coding agents contributing to IfcOpenShell. This file is +intended to be read by all AI agents regardless of platform (Claude Code, +Copilot, Cursor, etc.) in addition to any tool-specific configuration files. + +Human contributors using AI tools should also read this document carefully, +as they are responsible for ensuring their contributions comply with these +guidelines. + +## Project Overview + +IfcOpenShell is an open source library for working with Industry Foundation +Classes (IFC). It provides C++ and Python APIs, geometry processing, and an +ecosystem of tools including IfcConvert and the Bonsai Blender add-on. + +## Licensing + +All contributions must be compatible with the project's licensing: + +- **Library code** (everything except Bonsai): **LGPL-3.0-or-later** +- **Bonsai** (`src/bonsai/`): **GPL-3.0-or-later** + +There is no Contributor License Agreement (CLA). By submitting a pull request, +you agree that your contribution is licensed under the applicable license above. + +## Indicating AI-Generated Code + +Contributors must clearly indicate when code has been generated or +substantially written by an AI tool. + +### Commits + +Commits that modify existing code must include a note in the **body** of the +commit message (not the subject line) indicating that the change was +AI-generated. For example: + +``` +Fix off-by-one error in element iteration + +The loop termination condition was incorrect when processing +IfcRelAggregates relationships. + +Generated with the assistance of an AI coding tool. +``` + +### New Files + +New files that are AI-generated must include a comment near the top of the +file indicating this. Use the appropriate comment syntax for the language: + +```python +# This file was generated with the assistance of an AI coding tool. +``` + +```cpp +// This file was generated with the assistance of an AI coding tool. +``` + +### Pull Requests + +Pull requests containing AI-generated code must indicate in the PR description +which parts of the contribution are AI-generated. If the entire PR is +AI-generated, state that clearly. If only specific commits or files are +AI-generated, identify them. + +## Pull Request Guidelines + +### Scope and Size + +- Each pull request should address a **single issue or feature**. +- Do not mix unrelated changes (e.g., bug fixes with refactoring or style + changes) in the same PR. +- Large pull requests should be broken down into **multiple small, standalone + commits** that are each easy to review independently. Rewrite commit history + for this purpose if necessary. +- PRs that are minimal, focused solutions to a specific problem are much more + likely to be accepted. + +### What to Avoid + +- **Over-engineering**: Do not add features, abstractions, or configurability + beyond what is needed to solve the immediate problem. +- **Scope creep**: Do not make changes to files or code that are not directly + related to the task at hand. +- **Unnecessary additions**: Do not add docstrings, comments, type annotations, + or error handling to code you did not otherwise need to change. +- **Cosmetic changes**: Do not reformat, rename, or reorganize code that is + unrelated to your change. + +## Commit Messages + +- The **subject line** must be **50 characters or less**. +- Use the **imperative mood** (e.g., "Fix crash in geometry kernel", not + "Fixed crash" or "Fixes crash"). +- A commit message can be a single line if the purpose is obvious from the + subject alone. +- Otherwise, add a blank line after the subject followed by a short explanation + of a few lines in the body. + +## Code Style + +### Python + +- **Line length**: 120 characters +- **Formatter**: black +- **Linter**: ruff +- Configuration is in `pyproject.toml` + +### C++ + +- **Standard**: C++17 minimum +- **Formatter**: clang-format (configuration in `.clang-format`) +- **Linter**: clang-tidy (configuration in `.clang-tidy`) + +Run linters and formatters **before submitting** your pull request. Do not rely +on CI to catch formatting issues. + +## Testing + +- Pull requests with test coverage are **much more likely to be merged**. +- If tests are appropriate and feasible for your change, they should be + included. +- Tests are not required for every change (e.g., documentation-only changes), + but the expectation is that testable code changes come with tests. +- Python tests use **pytest** and are located in `test/` or `tests/` directories + within each package under `src/`. +- Run the existing test suite for the package you modified before submitting. + +## Architecture Quick Reference + +### Directory Structure + +- `src/ifcparse/` — C++ IFC file parsing +- `src/ifcgeom/` — C++ geometry processing (OpenCASCADE and CGAL kernels) +- `src/serializers/` — Output format serializers (glTF, Collada, SVG, etc.) +- `src/ifcwrap/` — SWIG Python bindings +- `src/ifcconvert/` — CLI conversion tool +- `src/ifcopenshell-python/` — Python API (`ifcopenshell` package) +- `src/bonsai/` — Blender add-on (GPL-3.0-or-later) +- `src/ifctester/` — IDS model auditing +- `src/ifcpatch/` — IFC file manipulation scripts +- `src/ifcdiff/` — IFC model comparison +- `src/ifcclash/` — Clash detection +- `src/ifccsv/` — Schedule import/export + +### IFC Schema Versions + +The library supports IFC2x3 TC1, IFC4 Add2 TC1, IFC4x1, IFC4x2, and +IFC4x3 Add2. Schema-specific code is compiled conditionally. Be aware of +which schema versions your change affects. From a5461c074849f81568f215a49a1e460f97a2e95e Mon Sep 17 00:00:00 2001 From: Sebastian Schilling Date: Thu, 12 Feb 2026 12:04:36 +0100 Subject: [PATCH 026/131] buildingSMART Data Dictionary module: added textfield to change data dictionary url --- src/bonsai/bonsai/bim/module/bsdd/__init__.py | 1 + src/bonsai/bonsai/bim/module/bsdd/prop.py | 19 +++++++++++- src/bonsai/bonsai/bim/module/bsdd/ui.py | 29 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/bsdd/__init__.py b/src/bonsai/bonsai/bim/module/bsdd/__init__.py index 25d6f501f0..eecc5880fa 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/__init__.py +++ b/src/bonsai/bonsai/bim/module/bsdd/__init__.py @@ -37,6 +37,7 @@ classes = ( ui.BIM_UL_bsdd_classes, ui.BIM_UL_bsdd_properties, ui.BIM_PT_bsdd, + ui.BIM_OT_bsdd_reset_baseurl, ) diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index 347cdab1b2..b7f1e57569 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Literal, Union import bpy +import bsdd from bpy.props import ( BoolProperty, CollectionProperty, @@ -73,6 +74,14 @@ def update_active_class_index(self: "BIMBSDDProperties", context: bpy.types.Cont BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() +def update_bsdd_baseurl(self: "BIMBSDDProperties", context: bpy.types.Context) -> None: + try: + tool.Bsdd.client = bsdd.Client() + if hasattr(tool.Bsdd.client, "baseurl"): + tool.Bsdd.client.baseurl = self.bsdd_baseurl + except Exception: + pass + class BSDDDictionary(PropertyGroup): uri: StringProperty(name="URI") default_language_code: StringProperty(name="Language") @@ -164,6 +173,13 @@ class BIMBSDDProperties(PropertyGroup): ) classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) + bsdd_baseurl: StringProperty( + name="Other URL:", + description="URL from another Dictionary with bSDD API", + default="", + update=update_bsdd_baseurl, + ) + if TYPE_CHECKING: active_dictionary: str active_dictionary: str @@ -182,7 +198,8 @@ class BIMBSDDProperties(PropertyGroup): should_filter_ifc_class: bool use_only_ifc_properties: bool classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset] - + bsdd_baseurl: str + @property def active_class(self) -> Union[BSDDClassification, None]: return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index) diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index c6966c01d8..a44be23664 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -24,6 +24,7 @@ import bpy from bpy.types import Panel, UIList import bonsai.tool as tool +import bsdd from bonsai.bim.module.bsdd.data import BSDDData if TYPE_CHECKING: @@ -50,6 +51,17 @@ class BIM_PT_bsdd(Panel): props = tool.Bsdd.get_bsdd_props() assert self.layout layout = self.layout + try: + if not props.bsdd_baseurl: + try: + props.bsdd_baseurl = tool.Bsdd.client.baseurl + except Exception: + props.bsdd_baseurl = bsdd.Client().baseurl + except Exception: + pass + row = layout.row(align=True) + row.prop(props, "bsdd_baseurl", text="Other URL", emboss=True) + row.operator("bim.bsdd_reset_baseurl", icon="LOOP_BACK", text="") if len(props.dictionaries): row = self.layout.row() row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH") @@ -83,6 +95,23 @@ class BIM_PT_bsdd(Panel): row = self.layout.row() row.operator("bim.load_bsdd_dictionaries") +class BIM_OT_bsdd_reset_baseurl(bpy.types.Operator): + bl_idname = "bim.bsdd_reset_baseurl" + bl_label = "Reset bSDD Base URL" + bl_description = "Resets the bSDD base URL to the default value" + + def execute(self, context): + import bsdd + props = tool.Bsdd.get_bsdd_props() + props.bsdd_baseurl = "" + try: + tool.Bsdd.client = bsdd.Client() + if hasattr(tool.Bsdd.client, "baseurl"): + tool.Bsdd.client.baseurl = default_url + except Exception: + pass + self.report({'INFO'}, "bSDD base URL reset.") + return {'FINISHED'} class BIM_UL_bsdd_dictionaries(UIList): def draw_item( From 418d410b5c0e0a5d775249f12d0be611bdb2a4df Mon Sep 17 00:00:00 2001 From: Sebastian Schilling Date: Tue, 17 Feb 2026 15:10:13 +0100 Subject: [PATCH 027/131] moved change of bsdd baseurl change to addon settings --- src/bonsai/bonsai/bim/module/bsdd/__init__.py | 1 - src/bonsai/bonsai/bim/module/bsdd/prop.py | 17 ----------- src/bonsai/bonsai/bim/module/bsdd/ui.py | 29 ------------------- src/bonsai/bonsai/bim/ui.py | 5 ++++ src/bonsai/bonsai/tool/bsdd.py | 4 +++ 5 files changed, 9 insertions(+), 47 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/bsdd/__init__.py b/src/bonsai/bonsai/bim/module/bsdd/__init__.py index eecc5880fa..25d6f501f0 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/__init__.py +++ b/src/bonsai/bonsai/bim/module/bsdd/__init__.py @@ -37,7 +37,6 @@ classes = ( ui.BIM_UL_bsdd_classes, ui.BIM_UL_bsdd_properties, ui.BIM_PT_bsdd, - ui.BIM_OT_bsdd_reset_baseurl, ) diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index b7f1e57569..efd8db46db 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Literal, Union import bpy -import bsdd from bpy.props import ( BoolProperty, CollectionProperty, @@ -74,14 +73,6 @@ def update_active_class_index(self: "BIMBSDDProperties", context: bpy.types.Cont BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() -def update_bsdd_baseurl(self: "BIMBSDDProperties", context: bpy.types.Context) -> None: - try: - tool.Bsdd.client = bsdd.Client() - if hasattr(tool.Bsdd.client, "baseurl"): - tool.Bsdd.client.baseurl = self.bsdd_baseurl - except Exception: - pass - class BSDDDictionary(PropertyGroup): uri: StringProperty(name="URI") default_language_code: StringProperty(name="Language") @@ -172,13 +163,6 @@ class BIMBSDDProperties(PropertyGroup): default=False, ) classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) - - bsdd_baseurl: StringProperty( - name="Other URL:", - description="URL from another Dictionary with bSDD API", - default="", - update=update_bsdd_baseurl, - ) if TYPE_CHECKING: active_dictionary: str @@ -198,7 +182,6 @@ class BIMBSDDProperties(PropertyGroup): should_filter_ifc_class: bool use_only_ifc_properties: bool classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset] - bsdd_baseurl: str @property def active_class(self) -> Union[BSDDClassification, None]: diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index a44be23664..f239eb41cf 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -51,17 +51,6 @@ class BIM_PT_bsdd(Panel): props = tool.Bsdd.get_bsdd_props() assert self.layout layout = self.layout - try: - if not props.bsdd_baseurl: - try: - props.bsdd_baseurl = tool.Bsdd.client.baseurl - except Exception: - props.bsdd_baseurl = bsdd.Client().baseurl - except Exception: - pass - row = layout.row(align=True) - row.prop(props, "bsdd_baseurl", text="Other URL", emboss=True) - row.operator("bim.bsdd_reset_baseurl", icon="LOOP_BACK", text="") if len(props.dictionaries): row = self.layout.row() row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH") @@ -95,24 +84,6 @@ class BIM_PT_bsdd(Panel): row = self.layout.row() row.operator("bim.load_bsdd_dictionaries") -class BIM_OT_bsdd_reset_baseurl(bpy.types.Operator): - bl_idname = "bim.bsdd_reset_baseurl" - bl_label = "Reset bSDD Base URL" - bl_description = "Resets the bSDD base URL to the default value" - - def execute(self, context): - import bsdd - props = tool.Bsdd.get_bsdd_props() - props.bsdd_baseurl = "" - try: - tool.Bsdd.client = bsdd.Client() - if hasattr(tool.Bsdd.client, "baseurl"): - tool.Bsdd.client.baseurl = default_url - except Exception: - pass - self.report({'INFO'}, "bSDD base URL reset.") - return {'FINISHED'} - class BIM_UL_bsdd_dictionaries(UIList): def draw_item( self, diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 32240a3fee..30ce93f076 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -660,6 +660,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bsdd_load_test_dictionaries: BoolProperty( name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False ) + bsdd_baseurl: StringProperty( + name="bSDD API Base URL", description="Base URL for data dictionary API requests, e.g. https://api.bsdd.buildingsmart.org/api/", + default="https://api.bsdd.buildingsmart.org/api/", + ) should_disable_undo_on_save: BoolProperty( name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False ) @@ -972,6 +976,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(self, "bsdd_load_preview_dictionaries") layout.prop(self, "bsdd_load_inactive_dictionaries") layout.prop(self, "bsdd_load_test_dictionaries") + layout.prop(self, "bsdd_baseurl") def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") diff --git a/src/bonsai/bonsai/tool/bsdd.py b/src/bonsai/bonsai/tool/bsdd.py index dc42708867..47c948abcc 100644 --- a/src/bonsai/bonsai/tool/bsdd.py +++ b/src/bonsai/bonsai/tool/bsdd.py @@ -123,6 +123,10 @@ class Bsdd(bonsai.core.tool.Bsdd): @classmethod def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]: prefs = tool.Blender.get_addon_preferences() + baseurl = getattr(prefs, "bsdd_baseurl", "https://api.bsdd.buildingsmart.org/api/") + cls.client = bsdd.Client() + if hasattr(cls.client, "baseurl"): + cls.client.baseurl = baseurl response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries) dicts = response.get("dictionaries") or [] statuses = ["Active"] From 7141f2cf90b1f70f7cec2037a6fb3c714239c44c Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 18 Feb 2026 19:12:56 +0100 Subject: [PATCH 028/131] Fixes to PR7607 (Linked IFC Projects): Wireframe toggle. More permisive to get has_transformation = False. Show enable_editing_link if link is loaded --- .../bonsai/bim/module/project/operator.py | 19 +++++++++++-------- src/bonsai/bonsai/bim/module/project/ui.py | 10 +++++----- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 1d0e52976b..edef161559 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1664,12 +1664,16 @@ class ToggleLinkVisibility(bpy.types.Operator): return {"FINISHED"} def toggle_wireframe(self, link: "Link") -> None: + linked_collections = self.get_linked_collections() + link.is_wireframe = not link.is_wireframe display_type = "WIRE" if link.is_wireframe else "TEXTURED" - for collection in self.get_linked_collections(): + for collection in linked_collections: objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects) for obj in objs: obj.display_type = display_type + if handle := tool.Project.get_link_empty_handle(link): + handle.display_type = display_type def toggle_visibility(self, link: "Link") -> None: linked_collections = self.get_linked_collections() @@ -1746,15 +1750,14 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator): # obj_matrix is typically calculated as: # obj_matrix = np.linalg.inv(local_matrix) @ transformation @ global_matrix - # So let's calculate the transformation - - transformed_global_matrix = local_matrix @ np.array(new_obj_matrix) - transformation = transformed_global_matrix @ np.linalg.inv(global_matrix) - if np.allclose(transformation, np.eye(4)): - link.has_transformation = True + identity_blender_matrix = np.linalg.inv(local_matrix) @ global_matrix + if np.allclose(np.array(new_obj_matrix), identity_blender_matrix, atol=1e-5): + link.has_transformation = False transformation = ",".join(map(str, np.eye(4).reshape(-1))) else: - link.has_transformation = False + transformed_global_matrix = local_matrix @ np.array(new_obj_matrix) + transformation = transformed_global_matrix @ np.linalg.inv(global_matrix) + link.has_transformation = True transformation = ",".join(map(str, transformation.reshape(-1))) if tool.Ifc.get(): diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 3d02e6ca05..1836500c54 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -487,12 +487,12 @@ class BIM_PT_links(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" index = self.props.active_link_index - if self.props.active_link.is_editing: - row.operator("bim.edit_link", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_link", text="", icon="CANCEL") - else: - row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL") if self.props.active_link.is_loaded: + if self.props.active_link.is_editing: + row.operator("bim.edit_link", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_link", text="", icon="CANCEL") + else: + row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL") row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index From d4388ec76dc869473169f267a26fe56bbd881211 Mon Sep 17 00:00:00 2001 From: falken10vdl <33285113+falken10vdl@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:56:10 +0100 Subject: [PATCH 029/131] AddReferenceImage: fix regression with IFC2X3 support, refactor to no longer depend on add_representation or update_representation, remove legacy style updating functionality * Enhance AddReferenceImage operator to use file browser instead of independent popup dialogue * Fix dimensions assertion in TestAddReferenceImage * Remove error in return in _execute (it is not execute) * Add IFC2X3 support to AddReferenceImage * Adde unit="LENGTH" to the x/y properties (every length dimension everywhere in the UI is in project length units. No need to say it explicitly) * Manually create the texture always, not just for IFC2X3 * Add poll method to AddReferenceImage operator to check for loaded IFC project * Refactor AddReferenceImage to add representation manually following pattern in root/operator.py's bim.add_element * Improve File explorer options between new and select from existing project Ifc Reference Images * Refactor get_existing_reference_images to use selector for filtering image annotations * No extra args needed after should_add_representation is False * Doing clean=True deletes everything * Don't manually add geometry and materials, don't call bpy.ops. Only create IFC data, then use preexisting loading functions to create geometry. * Black formatting, also now we can start to remove this operator as it becomes obsolete * Consolidate duplicate UV generation into Loader.load_generated_uv_map Replace 3 identical XY-UV baking blocks (create_object IMAGE, bm_add_image_plane, ImageScalingTool) with a single reusable classmethod in tool.Loader. * Fix IFC4 texture display in Solid viewport Texture mode IFC4 IfcTextureCoordinateGenerator Mode=COORD is used, load_texture_maps falls back to load_generated_uv_map to bake XY-UV data onto the mesh. * Fix IFC2X3 texture display * This looks wrong * Remove legacy override image feature, because we now have a proper styles and texture manager * Remove legacy override existing image element, because we now have a dedicated styles texture manager * Remove unnecessary roundtrip to bmesh and mesh --------- Co-authored-by: Dion Moult --- src/bonsai/bonsai/bim/import_ifc.py | 49 +--- .../bonsai/bim/module/drawing/operator.py | 231 ++++++++---------- .../bonsai/bim/module/project/operator.py | 22 +- .../bonsai/bim/module/style/operator.py | 1 + src/bonsai/bonsai/tool/loader.py | 38 ++- src/bonsai/test/tool/test_drawing.py | 6 +- 6 files changed, 153 insertions(+), 194 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 6e04800dd4..9f846d8ccc 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -85,7 +85,7 @@ class MaterialCreator: if element.is_a("IfcTypeProduct"): self.parse_element_type_material_styles(element) self.parsed_meshes.add(self.mesh.name) - if not self.ifc_import_settings.load_indexed_maps: + if self.ifc_import_settings.load_indexed_maps: self.load_texture_maps(shape_has_openings) self.assign_material_slots_to_faces() tool.Geometry.record_object_materials(obj) @@ -117,7 +117,6 @@ class MaterialCreator: for texture in texture_style.Textures or []: if coords := getattr(texture, "IsMappedBy", None): coords = coords[0] - # IfcTextureCoordinateGenerator handled in the style shader graph if coords.is_a("IfcIndexedTextureMap"): return coords # TODO: support IfcTextureMap @@ -135,6 +134,10 @@ class MaterialCreator: if shape_has_openings and coords.is_a("IfcIndexedTextureMap"): continue tool.Loader.load_indexed_map(coords, self.mesh) + elif tool.Style.get_texture_style(material): + # No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy, + # and IFC4 COORD uses generated UVs). Bake XY→UV as fallback. + tool.Loader.load_generated_uv_map(self.mesh) def assign_material_slots_to_faces(self) -> None: if not self.mesh["ios_materials"]: @@ -892,48 +895,6 @@ class IfcImporter: obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)) ) - if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE": - image = None - if obj.data and obj.data.materials and obj.data.materials[0]: - material = obj.data.materials[0] - if material.use_nodes and material.node_tree: - for node in material.node_tree.nodes: - if node.type == "TEX_IMAGE" and node.image: - image = node.image - break - if image: - import bmesh - - bm = bmesh.new() - bm.from_mesh(obj.data) - if not bm.loops.layers.uv: - uv_layer = bm.loops.layers.uv.new() - else: - uv_layer = bm.loops.layers.uv.active - - if bm.verts: - min_x = min(v.co.x for v in bm.verts) - max_x = max(v.co.x for v in bm.verts) - min_y = min(v.co.y for v in bm.verts) - max_y = max(v.co.y for v in bm.verts) - - width = max_x - min_x - height = max_y - min_y - - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - u = (vert.co.x - min_x) / width if width > 0 else 0.5 - v = (vert.co.y - min_y) / height if height > 0 else 0.5 - - u = max(0.0, min(1.0, u)) - v = max(0.0, min(1.0, v)) - - loop[uv_layer].uv = (u, v) - - bm.to_mesh(obj.data) - bm.free() - obj.data.update() return obj def load_existing_meshes(self) -> None: diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 144a687af6..ad90e24693 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -40,6 +40,7 @@ from typing import ( import bmesh import bpy +import logging import ifcopenshell import ifcopenshell.api import ifcopenshell.api.document @@ -50,6 +51,7 @@ import ifcopenshell.ifcopenshell_wrapper import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.selector +import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import numpy as np import shapely @@ -59,6 +61,7 @@ from bpy_extras.io_utils import ImportHelper from lxml import etree from mathutils import Color, Matrix, Vector +import bonsai.bim.import_ifc import bonsai.bim.export_ifc import bonsai.bim.handler import bonsai.bim.helper @@ -138,7 +141,7 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator): element.ApplicableOccurrence = f"IfcAnnotation/{object_type}" if props.create_representation_for_type and object_type == "IMAGE": - bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name) + bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name) class EnableAddAnnotationType(bpy.types.Operator): @@ -1759,7 +1762,7 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator): enable_editing=True, ) if props.object_type == "IMAGE": - bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name) + bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name) class AddSheet(bpy.types.Operator, tool.Ifc.Operator): @@ -3802,27 +3805,70 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) + x_length: bpy.props.FloatProperty( + name="X Length", + description="Width of the reference image", + default=1.0, + min=0.001, + soft_min=0.01, + precision=3, + unit="LENGTH", + ) + y_length: bpy.props.FloatProperty( + name="Y Length", + description="Height of the reference image", + default=1.0, + min=0.001, + soft_min=0.01, + precision=3, + unit="LENGTH", + ) - override_existing_image: bpy.props.BoolProperty( - name="Override Existing Image", - default=True, - description=( - "Override image if it was previously loaded to Blender. If disabled, will always create a new image" - ), - ) - use_existing_object_by_name: bpy.props.StringProperty( - name="Use Existing Object By Name", - description="Existing object name to add a style with reference image to. If not provided will create a new object.", - options={"SKIP_SAVE"}, - ) - size: bpy.props.FloatProperty(name="Size", description="Size of the reference image", default=1.0, unit="LENGTH") + @classmethod + def poll(cls, context): + if not tool.Ifc.get(): + cls.poll_message_set("No IFC project is loaded.") + return False + return True + + def invoke(self, context, event): + self._last_filepath = "" + return super().invoke(context, event) + + def check(self, context): + if not hasattr(self, "_last_filepath"): + self._last_filepath = "" + + if self.filepath and self.filepath != self._last_filepath: + self._last_filepath = self.filepath + + abs_path = Path(self.filepath).absolute().resolve() + if abs_path.exists() and abs_path.is_file(): + image = load_image(abs_path.name, str(abs_path.parent), check_existing=False) + image_width_px = image.size[0] + image_height_px = image.size[1] + aspect_ratio = image_width_px / image_height_px + + if aspect_ratio >= 1.0: + self.x_length = 1.0 + self.y_length = 1.0 / aspect_ratio + else: + self.x_length = aspect_ratio + self.y_length = 1.0 + + bpy.data.images.remove(image) + return True + + return False def draw(self, context): + layout = self.layout if Path(tool.Ifc.get_path()).is_file(): - self.layout.prop(self, "use_relative_path") - self.layout.prop(self, "override_existing_image") - self.layout.prop(self, "use_existing_object_by_name") - self.layout.prop(self, "size") + layout.prop(self, "use_relative_path") + else: + self.use_relative_path = False + layout.prop(self, "x_length") + layout.prop(self, "y_length") def _execute(self, context): space = tool.Blender.get_view3d_space() @@ -3837,127 +3883,66 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) ifc_file = tool.Ifc.get() - if self.override_existing_image: - params = {"check_existing": True, "force_reload": True} - else: - params = {"check_existing": False} + params = {"check_existing": False} image = load_image(abs_path.name, str(abs_path.parent), **params) - aspect_ratio = image.size[0] / image.size[1] - if aspect_ratio >= 1.0: # Landscape - x_length = self.size - y_length = self.size / aspect_ratio - else: - x_length = self.size / aspect_ratio - y_length = self.size + mesh = bpy.data.meshes.new(image_filepath.stem) + obj = bpy.data.objects.new(image_filepath.stem, mesh) + element = tool.Drawing.run_root_assign_class( + obj=obj, ifc_class="IfcAnnotation", predefined_type="IMAGE", should_add_representation=False + ) - def bm_add_image_plane(mesh): - bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) + builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + hx = self.x_length * 0.5 / unit_scale + hy = self.y_length * 0.5 / unit_scale + verts = [(-hx, -hy, 0.0), ( hx, -hy, 0.0), ( hx, hy, 0.0), (-hx, hy, 0.0)] + item = builder.mesh(verts, [[0, 1, 2, 3]]) - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - plane_scale = Vector((x_length / 2.0, y_length / 2.0, 1.0)) - matrix = Matrix.LocRotScale(None, None, plane_scale) - bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False) + ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + representation = builder.get_representation(ifc_context, [item]) + ifcopenshell.api.geometry.assign_representation(ifc_file, element, representation) - if not bm.loops.layers.uv: - uv_layer = bm.loops.layers.uv.new() - else: - uv_layer = bm.loops.layers.uv.active - - min_x = min(v.co.x for v in bm.verts) - max_x = max(v.co.x for v in bm.verts) - min_y = min(v.co.y for v in bm.verts) - max_y = max(v.co.y for v in bm.verts) - - width = max_x - min_x - height = max_y - min_y - - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - u = (vert.co.x - min_x) / width if width > 0 else 0.5 - v = (vert.co.y - min_y) / height if height > 0 else 0.5 - - u = max(0.0, min(1.0, u)) - v = max(0.0, min(1.0, v)) - loop[uv_layer].uv = (u, v) - - tool.Blender.apply_bmesh(mesh, bm) - - if self.use_existing_object_by_name: - obj = bpy.data.objects[self.use_existing_object_by_name] - bm_add_image_plane(obj.data) - bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="") - else: - temp_mesh = bpy.data.meshes.new("temp_mesh") - bm_add_image_plane(temp_mesh) - obj = bpy.data.objects.new(image_filepath.stem, temp_mesh) - tool.Drawing.run_root_assign_class( - obj=obj, - ifc_class="IfcAnnotation", - predefined_type="IMAGE", - should_add_representation=True, - context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"), - ifc_representation_class=None, - ) - tool.Blender.remove_data_block(temp_mesh) - - element = tool.Ifc.get_entity(obj) - if element and isinstance(obj.data, bpy.types.Mesh): - representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - if representation and representation.Items: - item_id = representation.Items[0].id() - num_faces = len(obj.data.polygons) - obj.data["ios_item_ids"] = [item_id] * num_faces - tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces) - - for item in representation.Items: - if item.is_a("IfcPolygonalFaceSet") and item.Coordinates: - new_coords = [] - for vertex in obj.data.vertices: - co = obj.matrix_world @ vertex.co - new_coords.append([co.x, co.y, co.z]) - item.Coordinates.CoordList = new_coords - - tool.Blender.set_active_object(obj) - - material = bpy.data.materials.new(name=image_filepath.stem) - obj.data.materials.append(None) # new slot - obj.material_slots[0].material = material - bpy.ops.bim.add_style() - - style = tool.Ifc.get_entity(material) - assert style - tool.Style.assign_style_to_object(style, obj) + style = ifcopenshell.api.style.add_style(tool.Ifc.get(), name=image_filepath.stem) + ifcopenshell.api.style.assign_representation_styles( + ifc_file, shape_representation=representation, styles=[style] + ) # TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because # we don't support IfcSurfaceStyleWithTextures without Rendering yet shading_attributes = { - "SurfaceColour": { - "Red": 1.0, - "Green": 1.0, - "Blue": 1.0, - }, + "SurfaceColour": {"Red": 1.0, "Green": 1.0, "Blue": 1.0}, "Transparency": 0.0, "ReflectanceMethod": "NOTDEFINED", } ifcopenshell.api.style.add_surface_style( - tool.Ifc.get(), - style=style, - ifc_class="IfcSurfaceStyleRendering", - attributes=shading_attributes, + tool.Ifc.get(), style=style, ifc_class="IfcSurfaceStyleRendering", attributes=shading_attributes ) - texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix()) + + if tool.Ifc.get_schema() == "IFC2X3": + texture = ifc_file.create_entity( + "IfcImageTexture", + RepeatS=True, + RepeatT=True, + TextureType="TEXTURE", + UrlReference=image_filepath.as_posix(), + ) + else: + texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix()) + ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD") + textures = [texture] - ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map ifcopenshell.api.style.add_surface_style( - ifc_file, - style=style, - ifc_class="IfcSurfaceStyleWithTextures", - attributes={"Textures": textures}, + ifc_file, style=style, ifc_class="IfcSurfaceStyleWithTextures", attributes={"Textures": textures} ) - tool.Style.reload_material_from_ifc(material) - tool.Geometry.record_object_materials(obj) + + logger = logging.getLogger("ImportIFC") + ifc_import_settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger) + ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = tool.Ifc.get() + ifc_importer.create_style(style) + + bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) class ConvertSVGToDXF(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index edef161559..43588584bd 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -3248,29 +3248,9 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts) - if bm.loops.layers.uv: - uv_layer = bm.loops.layers.uv.active - - min_x = min(v.co.x for v in bm.verts) - max_x = max(v.co.x for v in bm.verts) - min_y = min(v.co.y for v in bm.verts) - max_y = max(v.co.y for v in bm.verts) - - width = max_x - min_x - height = max_y - min_y - - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - u = (vert.co.x - min_x) / width if width > 0 else 0.5 - v = (vert.co.y - min_y) / height if height > 0 else 0.5 - - u = max(0.0, min(1.0, u)) - v = max(0.0, min(1.0, v)) - loop[uv_layer].uv = (u, v) - bm.to_mesh(mesh) bm.free() + tool.Loader.load_generated_uv_map(mesh) mesh.update() element = tool.Ifc.get_entity(self.target_object) diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index a26cce67f1..e7d1d05e0a 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -87,6 +87,7 @@ class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator): core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True) +# TODO: remove completely class AddStyle(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_style" bl_label = "Add Style" diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index d81fec5fd2..94b15e73b7 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -179,7 +179,8 @@ class Loader(bonsai.core.tool.Loader): def surface_texture_to_dict(cls, surface_texture): if isinstance(surface_texture, dict): return surface_texture - mappings = surface_texture.IsMappedBy or [] + # IsMappedBy is an IFC4+ inverse attribute, not available in IFC2X3. + mappings = getattr(surface_texture, "IsMappedBy", None) or [] surface_texture = surface_texture.get_info() uv_mode = None if mappings: @@ -188,7 +189,7 @@ class Loader(bonsai.core.tool.Loader): uv_mode = "Generated" elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE": uv_mode = "Camera" - surface_texture["uv_mode"] = uv_mode or "UV" + surface_texture["uv_mode"] = uv_mode or "Generated" return surface_texture @classmethod @@ -286,6 +287,9 @@ class Loader(bonsai.core.tool.Loader): for texture in textures: mode = texture.get("Mode", None) + # IFC2X3 IfcImageTexture has no Mode attribute; default to DIFFUSE. + if mode is None and texture["type"] == "IfcImageTexture": + mode = "DIFFUSE" node = None image_url = None @@ -293,7 +297,8 @@ class Loader(bonsai.core.tool.Loader): def get_image() -> Union[bpy.types.Image, None]: # TODO: orphaned textures after shader recreated? if texture["type"] == "IfcImageTexture": - original_image_url = texture["URLReference"] + # IFC2X3 uses UrlReference, IFC4+ uses URLReference. + original_image_url = texture.get("URLReference") or texture.get("UrlReference", "") is_relative = not os.path.isabs(original_image_url) nonlocal image_url image_url = Path(original_image_url) @@ -539,6 +544,33 @@ class Loader(bonsai.core.tool.Loader): for colour in colours: cls.load_indexed_map(colour, mesh) + @classmethod + def load_generated_uv_map(cls, mesh: bpy.types.Mesh) -> None: + bm = bmesh.new() + bm.from_mesh(mesh) + uv_layer = bm.loops.layers.uv.active or bm.loops.layers.uv.new("UVMap") + + all_verts = [v.co for v in bm.verts] + if not all_verts: + bm.free() + return + + min_x = min(v.x for v in all_verts) + max_x = max(v.x for v in all_verts) + min_y = min(v.y for v in all_verts) + max_y = max(v.y for v in all_verts) + width = max_x - min_x + height = max_y - min_y + + for face in bm.faces: + for loop in face.loops: + u = (loop.vert.co.x - min_x) / width if width > 0 else 0.5 + v = (loop.vert.co.y - min_y) / height if height > 0 else 0.5 + loop[uv_layer].uv = (max(0.0, min(1.0, u)), max(0.0, min(1.0, v))) + + bm.to_mesh(mesh) + bm.free() + @classmethod def load_indexed_map(cls, index_map: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None: """Add data from index map as blender mesh attribute. diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index b0d2756198..69d7fbde92 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -934,11 +934,11 @@ class TestAddReferenceImage(NewFile): bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) filepath = Path("test/files/image.jpg").absolute() - bpy.ops.bim.add_reference_image(filepath=str(filepath)) + bpy.ops.bim.add_reference_image(filepath=str(filepath), x_length=3.53982, y_length=2.0) obj = bpy.data.objects["IfcAnnotation/image"] assert obj is not None - assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((1.0, 0.565, 0.0))) + assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0))) material = obj.active_material assert material @@ -957,4 +957,4 @@ class TestAddReferenceImage(NewFile): assert texture_filepath == filepath uv_node = material_nodes["Texture Coordinate"] - assert len(uv_node.outputs["Generated"].links[:]) == 1 + assert len(uv_node.outputs["UV"].links[:]) == 1 From 1751c36c67d9ff43eeae00327656ef4f5e01d87c Mon Sep 17 00:00:00 2001 From: falken10vdl <33285113+falken10vdl@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:02:57 +0100 Subject: [PATCH 030/131] AddReferenceImage: implement option to show texture in solid mode (#7689) --- src/bonsai/bonsai/bim/module/drawing/operator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index ad90e24693..2cddf46f9e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3823,6 +3823,11 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): precision=3, unit="LENGTH", ) + show_texture_solid_mode: bpy.props.BoolProperty( + name="Show Texture in Solid mode (slow)", + description="Show Texture in Solid mode (slow)", + default=False, + ) @classmethod def poll(cls, context): @@ -3867,10 +3872,14 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): layout.prop(self, "use_relative_path") else: self.use_relative_path = False + layout.prop(self, "show_texture_solid_mode") layout.prop(self, "x_length") layout.prop(self, "y_length") + def _execute(self, context): + project_props = tool.Project.get_project_props() + project_props.load_indexed_maps = self.show_texture_solid_mode space = tool.Blender.get_view3d_space() if space.shading.color_type != "TEXTURE": space.shading.color_type = "TEXTURE" From 8afe05601ea302f5a1f473fe8deb87fc082f512a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:41:51 +0000 Subject: [PATCH 031/131] Bump tar from 7.5.7 to 7.5.9 in /src/ifctester/webapp Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.9. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.9) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- src/ifctester/webapp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index 647b9d534e..e1527f59f3 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -2851,9 +2851,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { From 9ff4a7f0e0c23b85491c3478f1fa90d66088822d Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 22 Feb 2026 15:10:29 -0600 Subject: [PATCH 032/131] Table was not rendering correctly. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix Sphinx docs: replace csv-table with list-table for formatting functions The documentation table of formatting/query functions was not rendering because `.. csv-table::` requires strict RFC4180 CSV escaping. The table contains nested quotes, inch marks (e.g. `3' - 0"`), backticks, and code examples, which cause the CSV parser in docutils to treat rows as malformed and drop the entire directive. Replaced the directive with `.. list-table::`, which parses reStructuredText instead of CSV and safely supports inline code, quotes, and multi-line cells. Also moved the examples text outside the directive block and ensured a blank line after the table so Sphinx does not interpret following paragraphs as table rows. No content changes — documentation now renders correctly. Generated with the assistance of an AI coding tool. --- .../ifcopenshell-python/selector_syntax.rst | 92 ++++++++++++++++--- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index 2d0cc9e071..4f994daced 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -240,22 +240,84 @@ in spreadsheets. For example ``upper("foo")`` will produce ``FOO``. You may nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce ``Foobar``. Strings must be double quoted. -.. csv-table:: - :header: "Function", "Example", "Result", "Description" +.. list-table:: + :header-rows: 1 + :widths: 28 28 16 28 + + * - Function + - Example + - Result + - Description + + * - ``upper({{value}})`` + - ``upper("Foo")`` + - ``FOO`` + - Uppercases a string. + + * - ``lower({{value}})`` + - ``lower("Foo")`` + - ``foo`` + - Lowercases a string. + + * - ``title({{value}})`` + - ``title("foo")`` + - ``Foo`` + - Titlecases a string. + + * - ``concat({{value}}[, {{value2}}]*)`` + - ``concat("foo", "bar")`` + - ``foobar`` + - Concatenates two or more strings. + + * - ``round({{value}}, {{precision}})`` + - ``round(3.123, 0.1)`` + - ``3.1`` + - Rounds ``{{value}}`` to the nearest ``{{precision}}``. + + * - ``int({{value}})`` + - ``int(3.123)`` + - ``3`` + - Truncates the decimal part of the ``{{value}}``. + + * - ``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])`` + - ``number(1234.56, ",", ".")`` + - ``1.234,56`` + - Formats ``{{value}}`` with an optional custom ``{{decimal_separator}}`` and ``{{thousands_separator}}``. The default separators are ``.`` and ``,``. + + * - ``metric_length({{value}}, {{precision}}, {{decimals}})`` + - ``metric_length(3.123, 0.1, 2)`` + - ``3.10`` + - Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places. + + * - ``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})`` + - ``imperial_length(3.0, 4, "foot", "foot", true)`` + OR + ``imperial_length(3.0, 4, "foot", "foot", false)`` + - ``3'`` + OR + ``3' - 0"`` + - The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is ``foot``, or just inches if ``{{output_unit}}`` is ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches omit the inch portion (e.g., ``3'`` instead of ``3' - 0"``). + + * - ``sort({{values}})`` + - ``sort({{mats.Name}})`` + - ``Name1, Name2`` + - Sorts a list of items. + + * - ``reverse({{values}})`` + - ``reverse({{mats.Name}})`` + - ``Name2, Name1`` + - Reverses a list of items. + + * - ``join({{separator}}, {{values}})`` + - ``join("-", {{mats.Name}})`` + - ``Name1-Name2`` + - Joins a list of items with a custom separator. By default, lists are rendered as comma separated. + + * - ``{{value1}}[+-*/]{{value2}}`` + - ``{{z}}+3`` + - ``5`` + - Does arithmetic. Operators such as ``+``, ``-``, ``*``, and ``/`` are allowed and can be mixed with variables and formatting functions. - "``upper({{value}})``", "``upper(""Foo"")``", "``FOO``", "Uppercases a string." - "``lower({{value}})``", "``lower(""Foo"")``", "``foo``", "Lowercases a string." - "``title({{value}})``", "``title(""foo"")``", "``Foo``", "Titlecases a string." - "``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings." - "``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``." - "``int({{value}})``", "``int(3.123)``", "``3``", "Truncates the decimal part of the ``{{value}}``." - "``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "``1.234,56``", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``." - "``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places." - "``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)." - "``sort({{values}})``", "``sort({{mats.Name}})``", "``Name1, Name2``", "Sorts a list of items." - "``reverse({{values}})``", "``reverse({{mats.Name}})``", "``Name2, Name1``", "Reverses a list of items." - "``join({{separator}}, {{values}})``", "``join("-", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated." - "``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions." When using queries in an IfcAnnotation tag surround with backticks. Examples: From 514cbb49cc9d6d21d36c58ef545ca5e1f82dfb03 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 22 Feb 2026 17:46:10 -0600 Subject: [PATCH 033/131] Fix #7646: Fix layer thumbnail orientation for IFC types Use EPset_Parametric.LayerSetDirection exclusively to determine horizontal vs vertical layer rendering in type thumbnails, rather than hardcoding IfcSlabType checks. Also fix line drawing to use the is_horizontal flag consistently. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/tool/model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 148f4a6b75..c9cca803ec 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1245,9 +1245,6 @@ class Model(bonsai.core.tool.Model): height = 100 is_horizontal = False - if element.is_a("IfcSlabType"): - is_horizontal = True - parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") if parametric: layer_set_direction = parametric.get("LayerSetDirection", None) @@ -1266,7 +1263,7 @@ class Model(bonsai.core.tool.Model): del thicknesses[-1] for thickness in thicknesses: current_thickness += thickness - if element.is_a("IfcSlabType"): + if is_horizontal: y = (current_thickness / total_thickness) * height line = [x_offset, y_offset + y, x_offset + width, y_offset + y] else: From 7d8c7a2c3d1b18666e1483cdf2372604e2b86e95 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Mon, 23 Feb 2026 07:09:18 -0600 Subject: [PATCH 034/131] Fix #7681: Fix isolate_objects ignoring hide_select/hide_viewport (#7710) Objects with hide_select=True could not be selected during isolation, causing hide_view_set to incorrectly hide them. Objects with hide_viewport=True had their H-key hide state modified as a side effect of hide_view_clear/hide_view_set. Both are now left unaffected by bim.activate_drawing. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/tool/blender.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index a2a0f09ed8..d05f3356ec 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1908,14 +1908,29 @@ class Blender(bonsai.core.tool.Blender): previously_active = bpy.context.view_layer.objects.active override = cls.get_viewport_context() + + # Save H-key hide state for globally-restricted objects so we don't change it. + # hide_viewport=True means the object is globally hidden via the outliner restriction; + # hide_view_clear/hide_view_set should not add or remove an additional H-key hide on them. + viewport_restricted = {obj: obj.hide_get() for obj in bpy.context.view_layer.objects if obj.hide_viewport} + with bpy.context.temp_override(**override): bpy.ops.object.hide_view_clear(select=False) bpy.ops.object.select_all(action="DESELECT") + hide_select_objs = [] for obj in objs: + if obj.hide_select: + hide_select_objs.append(obj) + obj.hide_select = False obj.select_set(True) with bpy.context.temp_override(**override): bpy.ops.object.hide_view_set(unselected=True) + for obj in hide_select_objs: + obj.hide_select = True + + for obj, was_hidden in viewport_restricted.items(): + obj.hide_set(was_hidden) bpy.ops.object.select_all(action="DESELECT") for name in previously_selected: From 2f5c71588ede9bbf5a892433a1b8a378e69f3969 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 24 Feb 2026 09:53:24 +1100 Subject: [PATCH 035/131] Revert "Table was not rendering correctly." This reverts commit 9ff4a7f0e0c23b85491c3478f1fa90d66088822d. --- .../ifcopenshell-python/selector_syntax.rst | 92 +++---------------- 1 file changed, 15 insertions(+), 77 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index 4f994daced..2d0cc9e071 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -240,84 +240,22 @@ in spreadsheets. For example ``upper("foo")`` will produce ``FOO``. You may nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce ``Foobar``. Strings must be double quoted. -.. list-table:: - :header-rows: 1 - :widths: 28 28 16 28 - - * - Function - - Example - - Result - - Description - - * - ``upper({{value}})`` - - ``upper("Foo")`` - - ``FOO`` - - Uppercases a string. - - * - ``lower({{value}})`` - - ``lower("Foo")`` - - ``foo`` - - Lowercases a string. - - * - ``title({{value}})`` - - ``title("foo")`` - - ``Foo`` - - Titlecases a string. - - * - ``concat({{value}}[, {{value2}}]*)`` - - ``concat("foo", "bar")`` - - ``foobar`` - - Concatenates two or more strings. - - * - ``round({{value}}, {{precision}})`` - - ``round(3.123, 0.1)`` - - ``3.1`` - - Rounds ``{{value}}`` to the nearest ``{{precision}}``. - - * - ``int({{value}})`` - - ``int(3.123)`` - - ``3`` - - Truncates the decimal part of the ``{{value}}``. - - * - ``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])`` - - ``number(1234.56, ",", ".")`` - - ``1.234,56`` - - Formats ``{{value}}`` with an optional custom ``{{decimal_separator}}`` and ``{{thousands_separator}}``. The default separators are ``.`` and ``,``. - - * - ``metric_length({{value}}, {{precision}}, {{decimals}})`` - - ``metric_length(3.123, 0.1, 2)`` - - ``3.10`` - - Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places. - - * - ``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})`` - - ``imperial_length(3.0, 4, "foot", "foot", true)`` - OR - ``imperial_length(3.0, 4, "foot", "foot", false)`` - - ``3'`` - OR - ``3' - 0"`` - - The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is ``foot``, or just inches if ``{{output_unit}}`` is ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches omit the inch portion (e.g., ``3'`` instead of ``3' - 0"``). - - * - ``sort({{values}})`` - - ``sort({{mats.Name}})`` - - ``Name1, Name2`` - - Sorts a list of items. - - * - ``reverse({{values}})`` - - ``reverse({{mats.Name}})`` - - ``Name2, Name1`` - - Reverses a list of items. - - * - ``join({{separator}}, {{values}})`` - - ``join("-", {{mats.Name}})`` - - ``Name1-Name2`` - - Joins a list of items with a custom separator. By default, lists are rendered as comma separated. - - * - ``{{value1}}[+-*/]{{value2}}`` - - ``{{z}}+3`` - - ``5`` - - Does arithmetic. Operators such as ``+``, ``-``, ``*``, and ``/`` are allowed and can be mixed with variables and formatting functions. +.. csv-table:: + :header: "Function", "Example", "Result", "Description" + "``upper({{value}})``", "``upper(""Foo"")``", "``FOO``", "Uppercases a string." + "``lower({{value}})``", "``lower(""Foo"")``", "``foo``", "Lowercases a string." + "``title({{value}})``", "``title(""foo"")``", "``Foo``", "Titlecases a string." + "``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings." + "``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``." + "``int({{value}})``", "``int(3.123)``", "``3``", "Truncates the decimal part of the ``{{value}}``." + "``number({{value}}[, {{decimal_separator}}[, {{thousands_separator}}]])``", "``number(1234.56, "","", ""."")``", "``1.234,56``", "Formats {{value}} with an optional custom {{decimal_separator}} and {{thousands_separator}}. The default separators are ``.`` and ``,``." + "``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places." + "``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)." + "``sort({{values}})``", "``sort({{mats.Name}})``", "``Name1, Name2``", "Sorts a list of items." + "``reverse({{values}})``", "``reverse({{mats.Name}})``", "``Name2, Name1``", "Reverses a list of items." + "``join({{separator}}, {{values}})``", "``join("-", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated." + "``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions." When using queries in an IfcAnnotation tag surround with backticks. Examples: From f69ea8278976ec9c4c025ef67960f67a0a87b2a0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 24 Feb 2026 09:53:27 +1100 Subject: [PATCH 036/131] Revert "Fix #7646: Fix layer thumbnail orientation for IFC types" This reverts commit 514cbb49cc9d6d21d36c58ef545ca5e1f82dfb03. --- src/bonsai/bonsai/tool/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index c9cca803ec..148f4a6b75 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1245,6 +1245,9 @@ class Model(bonsai.core.tool.Model): height = 100 is_horizontal = False + if element.is_a("IfcSlabType"): + is_horizontal = True + parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") if parametric: layer_set_direction = parametric.get("LayerSetDirection", None) @@ -1263,7 +1266,7 @@ class Model(bonsai.core.tool.Model): del thicknesses[-1] for thickness in thicknesses: current_thickness += thickness - if is_horizontal: + if element.is_a("IfcSlabType"): y = (current_thickness / total_thickness) * height line = [x_offset, y_offset + y, x_offset + width, y_offset + y] else: From 41afaaec0d49aa06a840ee34e14ac59c57bb61e1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 24 Feb 2026 09:53:28 +1100 Subject: [PATCH 037/131] Revert "Fix #7681: Fix isolate_objects ignoring hide_select/hide_viewport (#7710)" This reverts commit 7d8c7a2c3d1b18666e1483cdf2372604e2b86e95. --- src/bonsai/bonsai/tool/blender.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index d05f3356ec..a2a0f09ed8 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1908,29 +1908,14 @@ class Blender(bonsai.core.tool.Blender): previously_active = bpy.context.view_layer.objects.active override = cls.get_viewport_context() - - # Save H-key hide state for globally-restricted objects so we don't change it. - # hide_viewport=True means the object is globally hidden via the outliner restriction; - # hide_view_clear/hide_view_set should not add or remove an additional H-key hide on them. - viewport_restricted = {obj: obj.hide_get() for obj in bpy.context.view_layer.objects if obj.hide_viewport} - with bpy.context.temp_override(**override): bpy.ops.object.hide_view_clear(select=False) bpy.ops.object.select_all(action="DESELECT") - hide_select_objs = [] for obj in objs: - if obj.hide_select: - hide_select_objs.append(obj) - obj.hide_select = False obj.select_set(True) with bpy.context.temp_override(**override): bpy.ops.object.hide_view_set(unselected=True) - for obj in hide_select_objs: - obj.hide_select = True - - for obj, was_hidden in viewport_restricted.items(): - obj.hide_set(was_hidden) bpy.ops.object.select_all(action="DESELECT") for name in previously_selected: From 0d382119dddded963348012fb5c802edd89c8421 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 24 Feb 2026 09:53:57 +1100 Subject: [PATCH 038/131] Fix docs table for selector syntax --- .../docs/ifcopenshell-python/selector_syntax.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index 2d0cc9e071..4e28e83ed4 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -254,7 +254,7 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce "``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)." "``sort({{values}})``", "``sort({{mats.Name}})``", "``Name1, Name2``", "Sorts a list of items." "``reverse({{values}})``", "``reverse({{mats.Name}})``", "``Name2, Name1``", "Reverses a list of items." - "``join({{separator}}, {{values}})``", "``join("-", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated." + "``join({{separator}}, {{values}})``", "``join(""-"", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated." "``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions." When using queries in an IfcAnnotation tag surround with backticks. From dcc25038f909b1827cd091c2899b52d55ab6f1eb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 24 Feb 2026 10:03:33 +1100 Subject: [PATCH 039/131] Fix #7646. Bug with layer thumbnail orientation. --- src/bonsai/bonsai/tool/model.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 148f4a6b75..0f3f4e45ad 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1244,18 +1244,7 @@ class Model(bonsai.core.tool.Model): height = 100 - is_horizontal = False - if element.is_a("IfcSlabType"): - is_horizontal = True - - parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") - if parametric: - layer_set_direction = parametric.get("LayerSetDirection", None) - if layer_set_direction == "AXIS2": - is_horizontal = False - elif layer_set_direction == "AXIS3": - is_horizontal = True - + is_horizontal = cls.get_usage_type(element) == "LAYER3" if is_horizontal: width, height = height, width @@ -1266,7 +1255,7 @@ class Model(bonsai.core.tool.Model): del thicknesses[-1] for thickness in thicknesses: current_thickness += thickness - if element.is_a("IfcSlabType"): + if is_horizontal: y = (current_thickness / total_thickness) * height line = [x_offset, y_offset + y, x_offset + width, y_offset + y] else: From 2d7a556dd5c24f1ba2d7ab0beb58078ebfbf5669 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 24 Feb 2026 11:26:18 +0100 Subject: [PATCH 040/131] Fix sectioned solid cap #7674 --- src/ifcgeom/kernels/opencascade/loft.cpp | 31 +++++++++--------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp index 74c9c187df..b055b7743b 100644 --- a/src/ifcgeom/kernels/opencascade/loft.cpp +++ b/src/ifcgeom/kernels/opencascade/loft.cpp @@ -165,13 +165,6 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re std::vector shps(loft->children.size()); std::vector>> all_tags; - - std::ostringstream oss; - loft->children[0]->print(oss); - loft->children[1]->print(oss); - auto s = oss.str(); - std::wcout << s.c_str() << std::endl; - // First convert all taxonomy items to TopoDS_Wire/Face for (auto it = loft->children.begin(); it < loft->children.end(); ++it) { auto i = std::distance(loft->children.begin(), it); @@ -267,6 +260,18 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re return true; */ + if (shps.size() < 2) { + Logger::Error("Not enough sections to loft"); + return false; + } + + if (shps[0].ShapeType() == TopAbs_FACE) { + // When processing a sectioned *surface* there are no + // begin and end caps that need to be added. + BB.Add(comp, shps.front().Reversed()); + BB.Add(comp, shps.back()); + } + // @todo this approach is // potentially incorrect as there is no guarantee that the wires for // subsequently placed profiles are traversed from an equivalent start vertex. @@ -292,18 +297,6 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re ws[0][i] = TopoDS::Wire(*fa[i]); } } - if (it->ShapeType() == TopAbs_FACE) { - // When processing a sectioned *surface* there are no - // begin and end caps that need to be added. - if (it == shps.begin()) { - // faces.Append(shps[0]); - BB.Add(comp, shps[0]); - } - if (jt == shps.end() - 1) { - // faces.Append(shps[1]); - BB.Add(comp, shps[1]); - } - } if (!all_tags.empty()) { // only open profiles have tags for now, so there is only one wire, no inner wires From ece7d6b97f0446d69a5782aac2cef3874f5409a7 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:54:26 -0800 Subject: [PATCH 041/131] Fixes example in documentation --- .../ifcopenshell/api/spatial/reference_structure.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py index 4446971f0d..f19be2360b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -63,14 +63,15 @@ def reference_structure( storey1 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuildingStorey") storey2 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuildingStorey") storey3 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuildingStorey") + space = ifcopenshell.api.root.create_entity(model, ifc_class="IfcSpace") # The project contains a site (note that project aggregation is a special case in IFC) ifcopenshell.api.aggregate.assign_object(model, products=[site], relating_object=project) # The site has a building, the building has a storey, and the storey has a space ifcopenshell.api.aggregate.assign_object(model, products=[building], relating_object=site) - ifcopenshell.api.aggregate.assign_object(model, products=[storey], relating_object=building) - ifcopenshell.api.aggregate.assign_object(model, products=[space], relating_object=storey) + ifcopenshell.api.aggregate.assign_object(model, products=[storey1,storey2,storey3], relating_object=building) + ifcopenshell.api.aggregate.assign_object(model, products=[space], relating_object=storey1) # Create a column, this column spans 3 storeys column = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") @@ -80,7 +81,11 @@ def reference_structure( # And referenced in the others ifcopenshell.api.spatial.reference_structure( - model, products=[column], relating_structure=[storey2, storey3] + model, products=[column], relating_structure=storey2 + ) + + ifcopenshell.api.spatial.reference_structure( + model, products=[column], relating_structure=storey3 ) """ From 8df4b2cd562439b9499e50371c2903d6603c28a4 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 14 Jan 2026 01:06:51 +0100 Subject: [PATCH 042/131] Scale font size in PolylineDecorator and BoundingBoxDecorator based on Blender's UI preferences --- .../bonsai/bim/module/model/decorator.py | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 328804d4c6..a6338a05e7 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -29,6 +29,7 @@ import bpy import gpu import ifcopenshell import mathutils +import sys from bpy.types import SpaceView3D from bpy_extras import view3d_utils from bpy_extras.view3d_utils import location_3d_to_region_2d @@ -466,13 +467,19 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 0 - font_size = tool.Blender.scale_font_size(12) + ui_style = context.preferences.ui_styles[0] + widget_font_points = ui_style.widget.points + ui_scale = context.preferences.view.ui_scale + platform_scale = 2 if sys.platform == 'darwin' else 1 + + font_size = widget_font_points * ui_scale * platform_scale + offset = widget_font_points * ui_scale * (1.5 * platform_scale) + line_height = widget_font_points * ui_scale * (1.25 * platform_scale) blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) color = self.addon_prefs.decorations_colour color_highlight = self.addon_prefs.decorator_color_special - offset = 20 new_line = 0 for i, (key, field_name) in enumerate(texts.items()): formatted_value = None @@ -480,7 +487,7 @@ class PolylineDecorator: # Controls which options are displayed in the UI if key not in self.input_ui.input_options: continue - new_line += 20 + new_line += line_height if self.tool_state and key != self.tool_state.input_type: formatted_value = self.input_ui.get_formatted_value(key) else: @@ -515,7 +522,12 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 1 self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - font_size = tool.Blender.scale_font_size(12) + ui_style = context.preferences.ui_styles[0] + widget_font_points = ui_style.widget.points + ui_scale = context.preferences.view.ui_scale + platform_scale = 2 if sys.platform == 'darwin' else 1 + + font_size = widget_font_points * ui_scale * platform_scale blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -1936,7 +1948,13 @@ class BoundingBoxDecorator: addon_prefs = tool.Blender.get_addon_preferences() font_id = 0 - font_size = tool.Blender.scale_font_size(12) + # Get Blender's default UI widget font size from preferences + ui_style = context.preferences.ui_styles[0] + widget_font_points = ui_style.widget.points + ui_scale = context.preferences.view.ui_scale + platform_scale = 2 if sys.platform == 'darwin' else 1 + + font_size = widget_font_points * ui_scale * platform_scale blf.size(font_id, font_size) blf.enable(font_id, blf.SHADOW) blf.shadow(font_id, 6, 0, 0, 0, 1) From dde6e2d62b6bbbbeebec8faceb89088cc807615d Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 14 Jan 2026 01:13:28 +0100 Subject: [PATCH 043/131] black --- src/bonsai/bonsai/bim/module/model/decorator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index a6338a05e7..6fd5a9020f 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -470,8 +470,8 @@ class PolylineDecorator: ui_style = context.preferences.ui_styles[0] widget_font_points = ui_style.widget.points ui_scale = context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == 'darwin' else 1 - + platform_scale = 2 if sys.platform == "darwin" else 1 + font_size = widget_font_points * ui_scale * platform_scale offset = widget_font_points * ui_scale * (1.5 * platform_scale) line_height = widget_font_points * ui_scale * (1.25 * platform_scale) @@ -525,8 +525,8 @@ class PolylineDecorator: ui_style = context.preferences.ui_styles[0] widget_font_points = ui_style.widget.points ui_scale = context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == 'darwin' else 1 - + platform_scale = 2 if sys.platform == "darwin" else 1 + font_size = widget_font_points * ui_scale * platform_scale blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) @@ -1952,8 +1952,8 @@ class BoundingBoxDecorator: ui_style = context.preferences.ui_styles[0] widget_font_points = ui_style.widget.points ui_scale = context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == 'darwin' else 1 - + platform_scale = 2 if sys.platform == "darwin" else 1 + font_size = widget_font_points * ui_scale * platform_scale blf.size(font_id, font_size) blf.enable(font_id, blf.SHADOW) From ecaea5f7766303540fb0c836ec78253395eefd2c Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 22 Jan 2026 09:19:57 +0100 Subject: [PATCH 044/131] refactor scale_font_size as per developers feedback --- .../bonsai/bim/module/model/decorator.py | 27 ++++--------------- src/bonsai/bonsai/tool/blender.py | 12 ++++----- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 6fd5a9020f..bad611b1d7 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -29,7 +29,6 @@ import bpy import gpu import ifcopenshell import mathutils -import sys from bpy.types import SpaceView3D from bpy_extras import view3d_utils from bpy_extras.view3d_utils import location_3d_to_region_2d @@ -467,14 +466,9 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 0 - ui_style = context.preferences.ui_styles[0] - widget_font_points = ui_style.widget.points - ui_scale = context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == "darwin" else 1 - - font_size = widget_font_points * ui_scale * platform_scale - offset = widget_font_points * ui_scale * (1.5 * platform_scale) - line_height = widget_font_points * ui_scale * (1.25 * platform_scale) + font_size = tool.Blender.scale_font_size(None) + offset = tool.Blender.scale_font_size(None) * 1.5 + line_height = tool.Blender.scale_font_size(None) * 1.25 blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -522,12 +516,7 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 1 self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - ui_style = context.preferences.ui_styles[0] - widget_font_points = ui_style.widget.points - ui_scale = context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == "darwin" else 1 - - font_size = widget_font_points * ui_scale * platform_scale + font_size = tool.Blender.scale_font_size(None) blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -1948,13 +1937,7 @@ class BoundingBoxDecorator: addon_prefs = tool.Blender.get_addon_preferences() font_id = 0 - # Get Blender's default UI widget font size from preferences - ui_style = context.preferences.ui_styles[0] - widget_font_points = ui_style.widget.points - ui_scale = context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == "darwin" else 1 - - font_size = widget_font_points * ui_scale * platform_scale + font_size = tool.Blender.scale_font_size(None) blf.size(font_id, font_size) blf.enable(font_id, blf.SHADOW) blf.shadow(font_id, 6, 0, 0, 0, 1) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index a2a0f09ed8..b10f88b3f2 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1572,12 +1572,12 @@ class Blender(bonsai.core.tool.Blender): @classmethod def scale_font_size(cls, size): - default_dpi = 72 - default_pixel_size = 1.0 - default_scale = default_dpi * default_pixel_size - system = bpy.context.preferences.system - system_scale = system.dpi * system.pixel_size - return (system_scale / default_scale) * size + ui_style = bpy.context.preferences.ui_styles[0] + base_size = ui_style.widget.points if size is None else size + ui_scale = bpy.context.preferences.view.ui_scale + platform_scale = 2 if sys.platform == "darwin" else 1 + + return base_size * ui_scale * platform_scale @classmethod def apply_transform_as_local(cls, obj: bpy.types.Object) -> bool: From c0857c715aa09fcfe0c47013db8be9365c6fef56 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 28 Jan 2026 08:58:35 +0100 Subject: [PATCH 045/131] Refactor scale_font_size to improve DPI and pixel size handling for better font scaling --- src/bonsai/bonsai/tool/blender.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index b10f88b3f2..4aa67685a7 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1572,12 +1572,17 @@ class Blender(bonsai.core.tool.Blender): @classmethod def scale_font_size(cls, size): + default_dpi = 72 + default_pixel_size = 1.0 ui_style = bpy.context.preferences.ui_styles[0] base_size = ui_style.widget.points if size is None else size - ui_scale = bpy.context.preferences.view.ui_scale - platform_scale = 2 if sys.platform == "darwin" else 1 - - return base_size * ui_scale * platform_scale + platform_scale = 0.5 if sys.platform == "darwin" else 1 + + default_scale = default_dpi * default_pixel_size + system = bpy.context.preferences.system + system_scale = system.dpi * system.pixel_size + return (system_scale / default_scale) * base_size *platform_scale + @classmethod def apply_transform_as_local(cls, obj: bpy.types.Object) -> bool: From 1b784e22af5e8851835f000fbb61db49c6588917 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 28 Jan 2026 09:11:08 +0100 Subject: [PATCH 046/131] Add decorator font scale property addon setting --- src/bonsai/bonsai/bim/ui.py | 8 ++++++++ src/bonsai/bonsai/tool/blender.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 30ce93f076..26c06c5b8d 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -747,6 +747,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default=".ifc.metadata.blend", ) + decorator_font_scale: bpy.props.FloatProperty( + name="Decorator Font Scale", + description="Scale factor for decorator font size.", + default=1.0, + ) + if TYPE_CHECKING: svg2pdf_command: str svg2dxf_command: str @@ -786,6 +792,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): mass_time_units_in_wizard: bool chain_filter_with_set_operations: bool save_metadata_blend_file: bool + decorator_font_scale: float def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -1009,6 +1016,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): else: row = layout.row() row.operator("bim.manage_tab_visibility", icon="PREFERENCES") + layout.prop(self, "decorator_font_scale") # Scene panel groups diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 4aa67685a7..d2f9b420a6 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1581,7 +1581,7 @@ class Blender(bonsai.core.tool.Blender): default_scale = default_dpi * default_pixel_size system = bpy.context.preferences.system system_scale = system.dpi * system.pixel_size - return (system_scale / default_scale) * base_size *platform_scale + return (system_scale / default_scale) * base_size *platform_scale * tool.Blender.get_addon_preferences().decorator_font_scale @classmethod From 0be348707c84dad9a387c78627c707cb7e65c20e Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 25 Feb 2026 09:29:39 +0100 Subject: [PATCH 047/131] Update scale_font_size method to accept a None parameter so it is cleaner the calls from the rest of the code base --- src/bonsai/bonsai/bim/module/model/decorator.py | 10 +++++----- src/bonsai/bonsai/tool/blender.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index bad611b1d7..f5a1e534da 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -466,9 +466,9 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 0 - font_size = tool.Blender.scale_font_size(None) - offset = tool.Blender.scale_font_size(None) * 1.5 - line_height = tool.Blender.scale_font_size(None) * 1.25 + font_size = tool.Blender.scale_font_size() + offset = tool.Blender.scale_font_size() * 1.5 + line_height = tool.Blender.scale_font_size() * 1.25 blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -516,7 +516,7 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 1 self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - font_size = tool.Blender.scale_font_size(None) + font_size = tool.Blender.scale_font_size() blf.size(self.font_id, font_size) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -1937,7 +1937,7 @@ class BoundingBoxDecorator: addon_prefs = tool.Blender.get_addon_preferences() font_id = 0 - font_size = tool.Blender.scale_font_size(None) + font_size = tool.Blender.scale_font_size() blf.size(font_id, font_size) blf.enable(font_id, blf.SHADOW) blf.shadow(font_id, 6, 0, 0, 0, 1) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index d2f9b420a6..9471cad28c 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1571,7 +1571,7 @@ class Blender(bonsai.core.tool.Blender): return getattr(scene, "sun_pos_properties", None) @classmethod - def scale_font_size(cls, size): + def scale_font_size(cls, size=None): default_dpi = 72 default_pixel_size = 1.0 ui_style = bpy.context.preferences.ui_styles[0] From 18527a78e1325b19cc0a7aff5c0e95faca869e57 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 26 Feb 2026 12:35:40 +0100 Subject: [PATCH 048/131] rule_executor.py don't log RecursionError as error --- .../ifcopenshell/express/rule_executor.py | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py b/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py index 9201e480be..68240aeddd 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py +++ b/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py @@ -9,24 +9,28 @@ from codegen import indent def reverse_compile(s): - return re.sub(r'\bself\b', 'SELF', re.sub( - r"\s*\-\s*EXPRESS_ONE_BASED_INDEXING", - "", + return re.sub( + r"\bself\b", + "SELF", re.sub( - ", )?+.(, INDETERMINATE)\\"[::-1], - "]\\1[", + r"\s*\-\s*EXPRESS_ONE_BASED_INDEXING", + "", re.sub( - r", '(\w+)', INDETERMINATE\)", - ".\\1", - s.strip() - .replace("len(", "SIZEOF(") - .replace("assert ", "") - .replace(" is not False", "") - .replace("express_getattr(", "") - .replace("express_getitem(", ""), + ", )?+.(, INDETERMINATE)\\"[::-1], + "]\\1[", + re.sub( + r", '(\w+)', INDETERMINATE\)", + ".\\1", + s.strip() + .replace("len(", "SIZEOF(") + .replace("assert ", "") + .replace(" is not False", "") + .replace("express_getattr(", "") + .replace("express_getitem(", ""), + )[::-1], )[::-1], - )[::-1], - )) + ), + ) @dataclass @@ -68,9 +72,13 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: if hasattr(logger, "set_instance"): # when using the json logger, we notify it of the relevant instance - pre_annotate_instance = lambda instance: logger.set_state('instance', instance) if hasattr(logger, 'set_state') else None + pre_annotate_instance = lambda instance: ( + logger.set_state("instance", instance) if hasattr(logger, "set_state") else None + ) post_annotate_instance = lambda instance: instance - pre_annotate_attribute = lambda attribute: logger.set_state('attribute', attribute) if hasattr(logger, 'set_state') else None + pre_annotate_attribute = lambda attribute: ( + logger.set_state("attribute", attribute) if hasattr(logger, "set_state") else None + ) post_annotate_attribute = lambda attribute: None else: # when using the normal text logger the instance is appended to the method @@ -90,13 +98,13 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: import time import subprocess - current_dir_files = {fn.lower(): fn for fn in os.listdir('.')} - schema_name = str(f.schema_identifier).split(' ')[-1].lower() - schema_path = current_dir_files.get(schema_name + '.exp') - fn = schema_path[:-4] + '.py' + current_dir_files = {fn.lower(): fn for fn in os.listdir(".")} + schema_name = str(f.schema_identifier).split(" ")[-1].lower() + schema_path = current_dir_files.get(schema_name + ".exp") + fn = schema_path[:-4] + ".py" if not os.path.exists(fn): subprocess.run([sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True) - time.sleep(1.) + time.sleep(1.0) source = open(fn, "r").read() a = ast.parse(source) @@ -108,12 +116,14 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: rules = list(filter(lambda x: hasattr(x, "SCOPE"), scope.values())) - if hasattr(logger, 'set_state'): - logger.set_state('type', 'global_rule') + if hasattr(logger, "set_state"): + logger.set_state("type", "global_rule") for R in [r for r in rules if r.SCOPE == "file"]: try: R()(f) + except RecursionError as e: + logger.info(str(e)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno pre_annotate_attribute(R.__name__) @@ -127,17 +137,15 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: ) ) - if hasattr(logger, 'set_state'): - logger.set_state('type', 'simpletype_rule') + if hasattr(logger, "set_state"): + logger.set_state("type", "simpletype_rule") types = {} subtypes = collections.defaultdict(list) for d in S.declarations(): if isinstance(d, ifcopenshell.ifcopenshell_wrapper.type_declaration): types[d.name()] = d - if isinstance( - d.declared_type(), ifcopenshell.ifcopenshell_wrapper.named_type - ): + if isinstance(d.declared_type(), ifcopenshell.ifcopenshell_wrapper.named_type): subtypes[d.declared_type().declared_type().name()].append(d.name()) D = collections.defaultdict(list) @@ -170,6 +178,8 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: for R in D[type_name(type)]: try: R()(fix_type(value)) + except RecursionError as e: + logger.info(str(e)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno pre_annotate_instance(instance) @@ -220,6 +230,8 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: for inst in f: try: values = list(inst) + except RecursionError as e: + logger.info(str(e)) except Exception as e: if hasattr(logger, "set_state"): logger.error(str(e)) @@ -228,22 +240,22 @@ def run(f: ifcopenshell.file, logger: Logger) -> None: continue entity = S.declaration_by_name(inst.is_a()) attrs = entity.all_attributes() - for i, (attr, val, is_derived) in enumerate( - zip(attrs, values, entity.derived()) - ): + for i, (attr, val, is_derived) in enumerate(zip(attrs, values, entity.derived())): if is_derived: # @todo pass else: check(val, attr.type_of_attribute(), instance=inst) - if hasattr(logger, 'set_state'): - logger.set_state('type', 'entity_rule') + if hasattr(logger, "set_state"): + logger.set_state("type", "entity_rule") for R in [r for r in rules if r.SCOPE == "entity"]: for inst in f.by_type(R.TYPE_NAME): try: R()(inst) + except RecursionError as e: + logger.info(str(e)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno pre_annotate_instance(inst) From 077a0c375504b4c0ea46c1bfa9f05274fb8785f3 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 26 Feb 2026 12:36:01 +0100 Subject: [PATCH 049/131] Run black on express/ --- .../ifcopenshell/express/bootstrap.py | 20 ++- .../ifcopenshell/express/cat.py | 11 +- .../ifcopenshell/express/codegen.py | 2 +- .../ifcopenshell/express/header.py | 1 + .../ifcopenshell/express/implementation.py | 46 ++++--- .../ifcopenshell/express/mapping.py | 1 + .../ifcopenshell/express/nodes.py | 100 ++++++++------ .../ifcopenshell/express/rule_compiler.py | 97 +++++--------- .../ifcopenshell/express/schema.py | 21 ++- .../ifcopenshell/express/schema_class.py | 126 ++++++++++-------- .../ifcopenshell/express/templates.py | 36 ++--- 11 files changed, 239 insertions(+), 222 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py index 9ff87d5ad0..ef3c3c3ef0 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py +++ b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py @@ -163,7 +163,18 @@ statements = [] terminals = reduce(lambda x, y: x | y, (find_bytype(e, Terminal) for id, e in express)) keywords = list(filter(operator.attrgetter("is_keyword"), terminals)) negated_keywords = map(lambda s: "~%s" % s, keywords) -no_action = {"letter", "digit", "digits", "real_literal", "integer_literal", "string_literal", "simple_string_literal", "letter", "not_quote", "not_paren_star_quote_special"} +no_action = { + "letter", + "digit", + "digits", + "real_literal", + "integer_literal", + "string_literal", + "simple_string_literal", + "letter", + "not_quote", + "not_paren_star_quote_special", +} while True: emitted_in_loop = set() @@ -194,7 +205,9 @@ for id in to_emit: if id in to_combine: stmt = "Suppress%s" % stmt if id not in no_action and not isinstance(expr.contents, Keyword): - children = list(map(operator.attrgetter('contents'), reduce(lambda x, y: x | y, (find_bytype(e, Keyword) for e in [expr])))) + children = list( + map(operator.attrgetter("contents"), reduce(lambda x, y: x | y, (find_bytype(e, Keyword) for e in [expr]))) + ) has_duplicates = len(children) > len(set(children)) node_type = "ListNode" if ("ZeroOrMore" in stmt or has_duplicates) else "Node" action = ".setParseAction(%s)" % ( @@ -243,5 +256,4 @@ if __name__ == "__main__": mdl = importlib.import_module(output) mdl.Generator(m).emit() sys.stdout.write(m.schema.name) -""" % ("\n ".join(statements)) - ) +""" % ("\n ".join(statements))) diff --git a/src/ifcopenshell-python/ifcopenshell/express/cat.py b/src/ifcopenshell-python/ifcopenshell/express/cat.py index d6e7b2c880..373afeb1d5 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/cat.py +++ b/src/ifcopenshell-python/ifcopenshell/express/cat.py @@ -1,15 +1,16 @@ import sys, fileinput -if sys.platform == "win32" and not hasattr(sys.stdout, 'buffer'): +if sys.platform == "win32" and not hasattr(sys.stdout, "buffer"): import os, msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) files = sys.argv[1:] -if files[0] == '-o': - b = open(files[1], 'wb') +if files[0] == "-o": + b = open(files[1], "wb") files = files[2:] else: - b = getattr(sys.stdout, 'buffer', sys.stdout) + b = getattr(sys.stdout, "buffer", sys.stdout) -for line in fileinput.input(files=files, mode='rb'): +for line in fileinput.input(files=files, mode="rb"): b.write(line) diff --git a/src/ifcopenshell-python/ifcopenshell/express/codegen.py b/src/ifcopenshell-python/ifcopenshell/express/codegen.py index fe997091b1..dfcf5083c8 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/codegen.py +++ b/src/ifcopenshell-python/ifcopenshell/express/codegen.py @@ -26,7 +26,7 @@ def indent(n, s): else: strs = s splitted = itertools.chain.from_iterable(map(functools.partial(str.split, sep="\n"), map(str, strs))) - return "\n".join(" "*n + l for l in splitted) + return "\n".join(" " * n + l for l in splitted) class Base: diff --git a/src/ifcopenshell-python/ifcopenshell/express/header.py b/src/ifcopenshell-python/ifcopenshell/express/header.py index 4cc8c8ea84..6059354925 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/header.py +++ b/src/ifcopenshell-python/ifcopenshell/express/header.py @@ -28,6 +28,7 @@ from collections import defaultdict USE_VIRTUAL_INHERITANCE = True + class Header(codegen.Base): def __init__(self, mapping): declarations = [] diff --git a/src/ifcopenshell-python/ifcopenshell/express/implementation.py b/src/ifcopenshell-python/ifcopenshell/express/implementation.py index 248bbd6e86..bc4b080ad6 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/implementation.py +++ b/src/ifcopenshell-python/ifcopenshell/express/implementation.py @@ -69,7 +69,7 @@ class Implementation(codegen.Base): templates.enum_from_string_stmt % dict(context, **locals()) for value in enum.values ), ) - + if USE_VIRTUAL_INHERITANCE: for name, enum in mapping.schema.selects.items(): write( @@ -118,10 +118,7 @@ class Implementation(codegen.Base): null_check = "" if arg["is_optional"]: - attr_check = ( - "if(get_attribute_value(%d).isNull()) { return %%s; }" - % (arg["index"] - 1,) - ) + attr_check = "if(get_attribute_value(%d).isNull()) { return %%s; }" % (arg["index"] - 1,) if "boost::optional" in arg["full_type"]: null_check = attr_check % "boost::none" else: @@ -157,7 +154,7 @@ class Implementation(codegen.Base): return templates.set_attr_stmt_enum elif arg["is_templated_list"] and not (select or simple or express): return templates.set_attr_stmt_array - elif arg["full_type"].endswith('*'): + elif arg["full_type"].endswith("*"): return templates.set_attr_instance else: return templates.set_attr_stmt @@ -178,7 +175,9 @@ class Implementation(codegen.Base): "non_optional_type": arg["non_optional_type"].replace("::Value", ""), "star_if_optional": "*" if "boost::optional" in arg["full_type"] else "", "check_optional_set_begin": "if (v) {" if "boost::optional" in arg["full_type"] else "", - "check_optional_set_else": "} else {" if "boost::optional" in arg["full_type"] else "if constexpr (false)", + "check_optional_set_else": ( + "} else {" if "boost::optional" in arg["full_type"] else "if constexpr (false)" + ), "check_optional_set_end": "}" if "boost::optional" in arg["full_type"] else "", }, ) @@ -193,11 +192,15 @@ class Implementation(codegen.Base): tmpl = ( templates.constructor_stmt_array if arg["is_templated_list"] - else templates.constructor_stmt_enum - if arg["is_enum"] - else templates.constructor_stmt_instance - if arg["full_type"].endswith('*') - else templates.constructor_stmt + else ( + templates.constructor_stmt_enum + if arg["is_enum"] + else ( + templates.constructor_stmt_instance + if arg["full_type"].endswith("*") + else templates.constructor_stmt + ) + ) ) impl = tmpl % { "name": deref_name, @@ -321,7 +324,7 @@ class Implementation(codegen.Base): else templates.simpletype_impl_is_without_supertype ) - constructor = templates.constructor_single_initlist# if superclass else templates.constructor + constructor = templates.constructor_single_initlist # if superclass else templates.constructor simpletype_impl_cast = ( templates.simpletype_impl_cast_templated @@ -374,8 +377,21 @@ class Implementation(codegen.Base): ("IfcEntityInstanceData&& e",), "", ), - ("", "", constructor, "", ("%s v" % type_str,), ("set_attribute_value(0, v%s);" % ("->generalize()" if mapping.is_templated_list(type) else ""))) if mapping.simple_type_parent(class_name) is None else \ - ("v", "", constructor, "", ("%s v" % type_str,), ""), + ( + ( + "", + "", + constructor, + "", + ("%s v" % type_str,), + ( + "set_attribute_value(0, v%s);" + % ("->generalize()" if mapping.is_templated_list(type) else "") + ), + ) + if mapping.simple_type_parent(class_name) is None + else ("v", "", constructor, "", ("%s v" % type_str,), "") + ), ("", "", templates.cast_function, type_str, (), simpletype_impl_cast), ), ), diff --git a/src/ifcopenshell-python/ifcopenshell/express/mapping.py b/src/ifcopenshell-python/ifcopenshell/express/mapping.py index 7ebac2478b..609f346c2c 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/mapping.py +++ b/src/ifcopenshell-python/ifcopenshell/express/mapping.py @@ -25,6 +25,7 @@ import schema from header import USE_VIRTUAL_INHERITANCE + class Mapping: express_to_cpp_typemapping = { diff --git a/src/ifcopenshell-python/ifcopenshell/express/nodes.py b/src/ifcopenshell-python/ifcopenshell/express/nodes.py index 34d3ef747b..2b8b872c83 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/nodes.py +++ b/src/ifcopenshell-python/ifcopenshell/express/nodes.py @@ -23,6 +23,7 @@ import operator import collections import bootstrap + class Node: def __init__(self, s, loc, tokens, rule=None): self.rule = rule or (type(self).__name__) @@ -58,15 +59,15 @@ class ListNode: rules_as_list = set() for t in self.tokens: - r = getattr(t, 'rule', None) + r = getattr(t, "rule", None) if r: rules_as_list.add(r) self.dict_tokens[r].append(t) - + for r, t in tokens.asDict().items(): if r not in rules_as_list: self.dict_tokens[r].append(t) - + self.flat = sum([getattr(t, "flat", [t]) for t in self.tokens], []) def __repr__(self): @@ -74,7 +75,7 @@ class ListNode: def __iter__(self): return iter(self.tokens) - + # Somehow indexing messes up the pyparsing results, so instead of x[0] use list(x)[0] # def __getitem__(self, i): # return self.tokens[i] @@ -110,7 +111,7 @@ def format_clause(exp): return "".join(whitespace(term) for term in exp.flat) -class TypeDeclaration(Node): +class TypeDeclaration(Node): name = property(lambda self: self.type_id[0]) utype = property(lambda self: self.underlying_type.any().any()) type = property(lambda self: self.utype[0] if isinstance(self.utype, list) else self.utype) @@ -245,7 +246,8 @@ class NamedType(Node): def do_try(fn): try: return fn() - except: pass + except: + pass def get_rule_id(x): @@ -255,8 +257,14 @@ def get_rule_id(x): if matches: return matches[0] + rule_dependencies = { - k: list(map(operator.attrgetter('contents'), bootstrap.reduce(lambda x, y: x | y, (bootstrap.find_bytype(e, bootstrap.Keyword) for e in [v])))) \ + k: list( + map( + operator.attrgetter("contents"), + bootstrap.reduce(lambda x, y: x | y, (bootstrap.find_bytype(e, bootstrap.Keyword) for e in [v])), + ) + ) for k, v in bootstrap.express } @@ -264,16 +272,17 @@ all_rules = [k for k, e in bootstrap.express] rule_definitions = {k: v for k, v in bootstrap.express} + def to_tree(x, key=None): - + def prune(di): # translate class names back to grammar rules if nested actions are encountered di = {get_rule_id(k) or k: v for k, v in di.items()} - + def replace_synonyms(x): for y in x: yield y - if False: # y in di: + if False: # y in di: # production element from grammar is found in parsed data, # return that. @@ -292,19 +301,21 @@ def to_tree(x, key=None): yield S # Do this recursively yield from replace_synonyms([S]) - + # is this a concatenation with zero or more synonyms? then also processs that # @todo catches: # - simple_expression = term { add_like_op term } . # but should probably also work on # - a = b { b } # in which case the second Concat would be eliminated - elif isinstance(rule, bootstrap.Concat) and \ - len(rule.contents) == 2 and \ - is_synonym(rule.contents[0]) and \ - isinstance(rule.contents[1].contents, bootstrap.Repeated) and \ - isinstance(rule.contents[1].contents.contents[0], bootstrap.Concat) and \ - str(rule.contents[1].contents.contents[0].contents[1]) == str(rule.contents[0]): + elif ( + isinstance(rule, bootstrap.Concat) + and len(rule.contents) == 2 + and is_synonym(rule.contents[0]) + and isinstance(rule.contents[1].contents, bootstrap.Repeated) + and isinstance(rule.contents[1].contents.contents[0], bootstrap.Concat) + and str(rule.contents[1].contents.contents[0].contents[1]) == str(rule.contents[0]) + ): S = is_synonym(rule.contents[0]) yield S # Do this recursively @@ -315,13 +326,13 @@ def to_tree(x, key=None): if key == "aggregation_types": # hack hack hack apparently the parser can't distinguish these subrules += list(replace_synonyms(rule_dependencies["general_aggregation_types"])) - + if rule_dependencies[key] and not subrules: # sometimes an intermediate production rule is missing # from the pyparsing output, e.g from parameter to simple_expression # directly. Recover from this. subrules = sum(map(rule_dependencies.__getitem__, rule_dependencies[key]), []) - + if not isinstance(rule_definitions[key], bootstrap.Union): # Filter out terminals when not a union. E.g no # reason to retain TYPE, END_TYPE, but operators @@ -331,7 +342,7 @@ def to_tree(x, key=None): vs = list(di.values()) return {k: v for k, v in di.items() if k in subrules or (k == key and len(vs) == 1 and vs[0] not in all_rules)} - + def simplify(di): if isinstance(di, list): if set(map(type, di)) == {str} and set(map(len, di)) == {1}: @@ -343,11 +354,11 @@ def to_tree(x, key=None): return {k: simplify(v) for k, v in di.items()} else: return di - + if isinstance(x, ListNode): d = to_tree(x.dict_tokens, key=get_rule_id(x) or key) - if key == 'if_stmt': + if key == "if_stmt": # The definition of if statement if (roughy): # 'if' expr 'then' stmt+ 'else' stmt+ # this causes stmt to be joined under the same @@ -355,39 +366,41 @@ def to_tree(x, key=None): # `else_stmt` that collects the second group # of stmts. - statements = x.dict_tokens['stmt'] - + statements = x.dict_tokens["stmt"] + else_index = None if_nesting = 0 - for i, tk in enumerate(x.flat): - if tk == 'if': if_nesting += 1 - if tk == 'end_if': if_nesting -= 1 - if tk == 'else' and if_nesting == 1: + for i, tk in enumerate(x.flat): + if tk == "if": + if_nesting += 1 + if tk == "end_if": + if_nesting -= 1 + if tk == "else" and if_nesting == 1: else_index = i if else_index: indices = [] for s in statements: for i in range(max(indices, default=0), len(x.flat)): - if x.flat[i:i+len(s.flat)] == s.flat: + if x.flat[i : i + len(s.flat)] == s.flat: indices.append(i) break assert len(indices) == len(statements) before_else = [i < else_index for i in indices] - else_stmt = [st for b, st in zip(before_else, d['stmt']) if not b] - d['stmt'] = [st for b, st in zip(before_else, d['stmt']) if b] + else_stmt = [st for b, st in zip(before_else, d["stmt"]) if not b] + d["stmt"] = [st for b, st in zip(before_else, d["stmt"]) if b] if else_stmt: - d['else_stmt'] = else_stmt - - if key == 'formal_parameter': + d["else_stmt"] = else_stmt + + if key == "formal_parameter": # Not so pretty hack to fix the overwriting of simple_id-like # ast nodes. The full solution would probably to register parse # actions. And directly reassign. - pid = d['parameter_id'][0][0] - d['parameter_id'][0] = x.flat[:x.flat.index(pid)+1:2] + pid = d["parameter_id"][0][0] + d["parameter_id"][0] = x.flat[: x.flat.index(pid) + 1 : 2] if key is None: return {get_rule_id(x): d} @@ -400,7 +413,10 @@ def to_tree(x, key=None): elif isinstance(x, dict): # d = {k: to_tree(v, key=k) for k, v in x.items()} # not fully understood, but when finding specific node Types and production rules, prioritize the former - d = {get_rule_id(k) or k: to_tree(v, key=k) for k, v in sorted(x.items(), key=lambda p: get_rule_id(p[0]) is not None)} + d = { + get_rule_id(k) or k: to_tree(v, key=k) + for k, v in sorted(x.items(), key=lambda p: get_rule_id(p[0]) is not None) + } return simplify(prune(d)) elif isinstance(x, list): return [to_tree(v, key=key) for v in x] @@ -459,7 +475,8 @@ class SuperTypeExpression(Node): else: constraint = self.supertype_rule[0] return [ - list(list(s)[0])[0].simple_id for s in list(list(list(constraint.subtype_constraint[0].supertype_expression[0])[0])[0].one_of[0])[2::2] + list(list(s)[0])[0].simple_id + for s in list(list(list(constraint.subtype_constraint[0].supertype_expression[0])[0])[0].one_of[0])[2::2] ] sub_types = property(get_sub_types) @@ -576,10 +593,11 @@ class ProcedureDeclaration(ListNode): @property def name(self): return self.flat[1] - - + + class FunctionDeclaration(ProcedureDeclaration): pass - + + class RuleDeclaration(ProcedureDeclaration): pass diff --git a/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py b/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py index 41fc6764c1..a77936d853 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py +++ b/src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py @@ -48,11 +48,7 @@ def to_graph(tree): # bootstrap.py that result in an intermediate list index node in to_tree() # Start with the intermediate nodes and filter out root (needs to have predecessors) - intermediate = [ - n - for n in g.nodes - if g.nodes[n].get("label") is None and list(g.predecessors(n)) - ] + intermediate = [n for n in g.nodes if g.nodes[n].get("label") is None and list(g.predecessors(n))] for n in intermediate: pr = list(g.predecessors(n)) @@ -82,8 +78,7 @@ def to_graph(tree): for n in g.nodes: if ( len(list(g.successors(n))) == 0 - and g.nodes[n].get("label") - not in ifcopenshell.express.express_parser.all_rules + and g.nodes[n].get("label") not in ifcopenshell.express.express_parser.all_rules ): g.nodes[n]["is_terminal"] = True @@ -105,8 +100,7 @@ def write_dot(fn, g): def format(di): Q = '"' inner = ",".join( - f"{k}={'' if v.startswith('<') else Q}{v}{'' if v.startswith('<') else Q}" - for k, v in di.items() + f"{k}={'' if v.startswith('<') else Q}{v}{'' if v.startswith('<') else Q}" for k, v in di.items() ) if inner: inner = f"[{inner}]" @@ -179,9 +173,7 @@ class context: def has_inverse(self, a): for r in self.rules: - if a in map( - lambda n: self.graph.nodes[n].get("label"), self.graph.predecessors(r) - ): + if a in map(lambda n: self.graph.nodes[n].get("label"), self.graph.predecessors(r)): return True return False @@ -190,10 +182,7 @@ class context: yield context(self.graph, [r]) def descendants(self): - return [ - b.rules[0][len(self.rules[0]) + 1 :] - for b in self.branches(allow_multiple=True) - ] + return [b.rules[0][len(self.rules[0]) + 1 :] for b in self.branches(allow_multiple=True)] def __repr__(self): try: @@ -206,14 +195,11 @@ class context: assert len(self.rules) == 1 nodes = itertools.chain( self.rules, - itertools.chain.from_iterable( - dict(nx.bfs_successors(self.graph, self.rules[0])).values() - ), + itertools.chain.from_iterable(dict(nx.bfs_successors(self.graph, self.rules[0])).values()), ) terminals_or_values = list( filter( - lambda n: self.graph.nodes[n].get("is_terminal") - or self.graph.nodes[n].get("value"), + lambda n: self.graph.nodes[n].get("is_terminal") or self.graph.nodes[n].get("value"), nodes, ) ) @@ -375,9 +361,7 @@ def calc_{class_name}_{str(derived_attr.attribute_decl.redeclared_attribute.qual """ if context.entity_body.derive_clause: - statements.extend( - map(format_derived, context.entity_body.derive_clause.branches()) - ) + statements.extend(map(format_derived, context.entity_body.derive_clause.branches())) return "\n\n".join(statements) @@ -420,10 +404,12 @@ def process_expression(context): exclude=[context.rel_op_extended], ) else: - if len(context.simple_expression.branches()) == 2 and str(context.rel_op_extended) == 'in': + if len(context.simple_expression.branches()) == 2 and str(context.rel_op_extended) == "in": # IfcBlobTexture try: - is_literal_str_list = set(map(type, ast.literal_eval(str(context.simple_expression.branches()[1])))) == {str} + is_literal_str_list = set( + map(type, ast.literal_eval(str(context.simple_expression.branches()[1]))) + ) == {str} except: is_literal_str_list = False if is_literal_str_list: @@ -567,9 +553,7 @@ def process_function_decl(context): str.lower, map( str, - context.function_head.formal_parameter.parameter_id.branches( - allow_multiple=True - ), + context.function_head.formal_parameter.parameter_id.branches(allow_multiple=True), ), ) return f"def {context.function_head.function_id}({', '.join(arguments)}):\n{indent(4, context.algorithm_head.local_decl)}\n{indent(4, context.stmt.branches())}" @@ -582,9 +566,7 @@ def process_query(context): def process_local_variable(context): if context.expression: expr = str(context.expression) - if ( - context.parameter_type.generalized_types.general_aggregation_types.general_set_type - ): + if context.parameter_type.generalized_types.general_aggregation_types.general_set_type: expr = re.sub(r"(\[[^\]]*\])", "express_set(\\1)", expr) return "%s = %s" % (str(context.variable_id).lower(), expr) @@ -623,9 +605,7 @@ def process_assignment(context): if m := re.match(r"^([^\[]+)\[([^\[]+)\]$", lhs): # @todo ugly regex hack aggr, index = m.groups() - return ( - f"temp = list({aggr})\ntemp[{index}] = {context.expression}\n{aggr} = temp" - ) + return f"temp = list({aggr})\ntemp[{index}] = {context.expression}\n{aggr} = temp" else: return "%s = %s" % (lhs, context.expression) @@ -643,11 +623,7 @@ def process_case_action(context): def process_case_statement(context): branches = context.branches( - exclude=[ - getattr(context, v) - for v in context.descendants() - if not v.startswith("case_action") - ] + exclude=[getattr(context, v) for v in context.descendants() if not v.startswith("case_action")] ) if context.stmt and context.stmt.branches(): branches += [f"else:\n{indent(4, context.stmt)}"] @@ -658,9 +634,7 @@ def process_aggregate_initializer(context): if context.element.repetition: return "([%s] * %s)" % (context.element.expression, context.element.repetition) else: - return "[%s]" % ",".join( - map(str, context.element.branches() if context.element else ()) - ) + return "[%s]" % ",".join(map(str, context.element.branches() if context.element else ())) def process_index(context): @@ -676,9 +650,7 @@ def process_index(context): codegen_rule("function_call", process_function_call) codegen_rule( "actual_parameter_list", - lambda context: ",".join( - map(str, context.expression.branches() if context.expression else []) - ), + lambda context: ",".join(map(str, context.expression.branches() if context.expression else [])), ) codegen_rule("entity_decl", functools.partial(process_type_decl, "entity")) codegen_rule("rule_decl", process_rule_decl) @@ -696,9 +668,7 @@ codegen_rule("simple_factor", simple_concat) codegen_rule("primary", simple_concat) codegen_rule("qualifier", simple_concat) codegen_rule("return_stmt", lambda context: "return %s" % context) -codegen_rule( - "compound_stmt", lambda context: "\n".join(map(str, context.stmt.branches())) -) +codegen_rule("compound_stmt", lambda context: "\n".join(map(str, context.stmt.branches()))) codegen_rule("if_stmt", process_if_stmt) codegen_rule("repeat_stmt", process_repeat_stmt) # codegen_rule("index", lambda context: '**express_index(%s)' % context) @@ -707,19 +677,14 @@ codegen_rule("index_qualifier", process_index) codegen_rule("group_qualifier", lambda context: empty()) codegen_rule("attribute_qualifier", lambda context: ".%s" % context) codegen_rule("rel_op", process_rel_op) -codegen_rule( - "built_in_constant", lambda context: "None" if str(context) == "?" else str(context) -) +codegen_rule("built_in_constant", lambda context: "None" if str(context) == "?" else str(context)) codegen_rule("assignment_stmt", process_assignment) codegen_rule("local_variable", process_local_variable) codegen_rule("local_decl", lambda context: "\n".join(map(str, context.branches()))) codegen_rule("general_ref/parameter_ref", make_lowercase) codegen_rule( "qualifiable_factor/attribute_ref", - make_lowercase_if( - lambda context: str(context) - not in set(map(str, schema.all_declarations.keys())) - ), + make_lowercase_if(lambda context: str(context) not in set(map(str, schema.all_declarations.keys()))), ) codegen_rule("case_action", process_case_action) codegen_rule("case_stmt", process_case_statement) @@ -824,17 +789,17 @@ if __name__ == "__main__": import subprocess schema = ifcopenshell.express.express_parser.parse(sys.argv[1]).schema - + try: ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema.name) except: # @nb note the difference here between: - # + # # - ifcopenshell.express.express_parser.parse # - ifcopenshell.express.parse.parse - # + # # First generates a pyparsing AST - # + # # Second populates a latebound schema # that can be registered in C++. builder = ifcopenshell.express.parse(sys.argv[1]) @@ -1056,13 +1021,13 @@ INDETERMINATE = indeterminate_type() if isinstance(v, str): nl = "\n" es = "\\n" - n[ - "label" - ] = f'<
{n.get("label")}
{v.replace("<", "<").replace(">", ">").replace(nl, "
")}
>' + n["label"] = ( + f'<
{n.get("label")}
{v.replace("<", "<").replace(">", ">").replace(nl, "
")}
>' + ) elif isinstance(v, empty): - n[ - "label" - ] = f'<
{n.get("label")}
---
>' + n["label"] = ( + f'<
{n.get("label")}
---
>' + ) fn = f"{nm}.dot" write_dot(fn, G) diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema.py b/src/ifcopenshell-python/ifcopenshell/express/schema.py index ebb050d1ae..315db817b6 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema.py @@ -27,6 +27,7 @@ if tuple(map(int, platform.python_version_tuple())) < (2, 7): collections.OrderedDict = ordereddict.OrderedDict + # According to ISO 10303-11 7.1.2: Letters: "... The case of # letters is significant only within explicit string literals." class OrderedCaseInsensitiveDict_KeyObject(str): @@ -92,23 +93,21 @@ class Schema: sort = lambda d: OrderedCaseInsensitiveDict(sorted(d)) - declarations = [ - d.any()[0] - for d in schema_declarations - if d.rule == "declaration" - ] + [ - d - for d in schema_declarations - if d.rule == "RuleDeclaration" + declarations = [d.any()[0] for d in schema_declarations if d.rule == "declaration"] + [ + d for d in schema_declarations if d.rule == "RuleDeclaration" ] - + self.types = sort([(t.name, t) for t in declarations if isinstance(t, nodes.TypeDeclaration)]) self.entities = sort([(t.name, t) for t in declarations if isinstance(t, nodes.EntityDeclaration)]) self.rules = sort([(t.name, t) for t in declarations if isinstance(t, nodes.RuleDeclaration)]) self.functions = sort([(t.name, t) for t in declarations if isinstance(t, nodes.FunctionDeclaration)]) - self.keys = list(self.types.keys()) + list(self.entities.keys()) + list(self.rules.keys()) + list(self.functions.keys()) - self.all_declarations = {k: v for d in (self.types, self.entities, self.rules, self.functions) for k, v in d.items()} + self.keys = ( + list(self.types.keys()) + list(self.entities.keys()) + list(self.rules.keys()) + list(self.functions.keys()) + ) + self.all_declarations = { + k: v for d in (self.types, self.entities, self.rules, self.functions) for k, v in d.items() + } of_type = lambda *types: sort( [(a, b.type) for a, b in self.types.items() if any(isinstance(b.type, ty) for ty in types)] diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index 50a4e50c5d..d7595ad6cb 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -123,6 +123,7 @@ class string_pool: def __init__(self, fn): self.di = {} self.fn = fn + def append(self, v): def _(): if i := self.di.get(v): @@ -131,7 +132,9 @@ class string_pool: i = len(self.di) self.di[v] = i return i + return self.fn(_()) + def __iter__(self): return iter(self.di.keys()) @@ -146,9 +149,9 @@ class EarlyBoundCodeWriter: "", '#include "../ifcparse/IfcSchema.h"', '#include "../ifcparse/%(schema_name_title)s.h"' % self.__dict__, - '#include ', + "#include ", "", - 'using namespace std::string_literals;', + "using namespace std::string_literals;", "using namespace IfcParse;", "", ] @@ -180,18 +183,18 @@ class EarlyBoundCodeWriter: self.statements.append("{factory_placeholder}") -# self.statements.append( -# """ -# #if defined(__clang__) -# __attribute__((optnone)) -# #elif defined(__GNUC__) || defined(__GNUG__) -# #pragma GCC push_options -# #pragma GCC optimize ("O0") -# #elif defined(_MSC_VER) -# #pragma optimize("", off) -# #endif -# """ -# ) + # self.statements.append( + # """ + # #if defined(__clang__) + # __attribute__((optnone)) + # #elif defined(__GNUC__) || defined(__GNUG__) + # #pragma GCC push_options + # #pragma GCC optimize ("O0") + # #elif defined(_MSC_VER) + # #pragma optimize("", off) + # #endif + # """ + # ) self.statements.append("IfcParse::schema_definition* %s_populate_schema() {" % self.schema_name.upper()) self.statements.append("{string_pool_placeholder}") @@ -200,7 +203,7 @@ class EarlyBoundCodeWriter: index_in_schema = self.names.index(name) ref = self.strings.append(name) self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new type_declaration(%(ref)s, %(index_in_schema)d, %(declared_type)s);' + " %(schema_name)s_types[%(index_in_schema)d] = new type_declaration(%(ref)s, %(index_in_schema)d, %(declared_type)s);" % locals() ) @@ -210,7 +213,7 @@ class EarlyBoundCodeWriter: ref = self.strings.append(name) items = ",".join(self.strings.append(v) for v in enum.values) self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new enumeration_type(%(ref)s, %(index_in_schema)d, {%(items)s});' + " %(schema_name)s_types[%(index_in_schema)d] = new enumeration_type(%(ref)s, %(index_in_schema)d, {%(items)s});" % locals() ) @@ -218,10 +221,14 @@ class EarlyBoundCodeWriter: schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) ref = self.strings.append(name) - supertype = "0" if len(type.supertypes) == 0 else "%s_types[%d]" % (self.schema_name, self.names.index(type.supertypes[0])) + supertype = ( + "0" + if len(type.supertypes) == 0 + else "%s_types[%d]" % (self.schema_name, self.names.index(type.supertypes[0])) + ) is_abstract = "true" if type.abstract else "false" self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new entity(%(ref)s, %(is_abstract)s, %(index_in_schema)d, (entity*) %(supertype)s);' + " %(schema_name)s_types[%(index_in_schema)d] = new entity(%(ref)s, %(is_abstract)s, %(index_in_schema)d, (entity*) %(supertype)s);" % locals() ) @@ -233,73 +240,89 @@ class EarlyBoundCodeWriter: map(lambda v: "%s_types[%d]" % (self.schema_name, self.names.index(v)), sorted(map(str, type.values))) ) self.statements.append( - ' %(schema_name)s_types[%(index_in_schema)d] = new select_type(%(ref)s, %(index_in_schema)d, {%(items)s});' + " %(schema_name)s_types[%(index_in_schema)d] = new select_type(%(ref)s, %(index_in_schema)d, {%(items)s});" % locals() ) def entity_attributes(self, name, attribute_definitions, is_derived): schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) + def _(): index_in_schema = self.names.index(name) schema_name = self.schema_name for attr_name, decl_type, optional in attribute_definitions: attr_name_ref = self.strings.append(attr_name) optional_cpp = str(optional).lower() - yield 'new attribute(%(attr_name_ref)s, %(decl_type)s, %(optional_cpp)s)' % locals() + yield "new attribute(%(attr_name_ref)s, %(decl_type)s, %(optional_cpp)s)" % locals() + attributes = ",".join(_()) derived = ",".join(map(lambda b: str(b).lower(), is_derived)) - self.statements.append(" ((entity*)%(schema_name)s_types[%(index_in_schema)d])->set_attributes({%(attributes)s}, {%(derived)s});" % locals()) + self.statements.append( + " ((entity*)%(schema_name)s_types[%(index_in_schema)d])->set_attributes({%(attributes)s}, {%(derived)s});" + % locals() + ) def inverse_attributes(self, name, inv_attrs): schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) + def _(): schema_name = self.schema_name index_in_schema = self.names.index(name) for attr_name, aggr_type, bound1, bound2, entity_ref, attribute_entity, attribute_entity_index in inv_attrs: attr_name_ref = self.strings.append(attr_name) opposite_index_in_schema = self.names.index(entity_ref) - opposite1 = '%(schema_name)s_types[%(opposite_index_in_schema)d]' % locals() + opposite1 = "%(schema_name)s_types[%(opposite_index_in_schema)d]" % locals() opposite_index_in_schema = self.names.index(attribute_entity) - opposite2 = '%(schema_name)s_types[%(opposite_index_in_schema)d]' % locals() - yield 'new inverse_attribute(%(attr_name_ref)s, inverse_attribute::%(aggr_type)s_type, %(bound1)d, %(bound2)d, ((entity*) %(opposite1)s), ((entity*) %(opposite2)s)->attributes()[%(attribute_entity_index)d])' % locals() + opposite2 = "%(schema_name)s_types[%(opposite_index_in_schema)d]" % locals() + yield "new inverse_attribute(%(attr_name_ref)s, inverse_attribute::%(aggr_type)s_type, %(bound1)d, %(bound2)d, ((entity*) %(opposite1)s), ((entity*) %(opposite2)s)->attributes()[%(attribute_entity_index)d])" % locals() + attributes = ",".join(_()) - self.statements.append(" ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_inverse_attributes({%(attributes)s});" % locals()) + self.statements.append( + " ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_inverse_attributes({%(attributes)s});" + % locals() + ) def entity_subtypes(self, name, tys): schema_name = self.schema_name.upper() index_in_schema = self.names.index(name) - subtypes = ",".join(map(lambda t: ("((entity*) %%(schema_name)s_types[%d])" % self.names.index(t)), tys)) % locals() - self.statements.append(" ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_subtypes({%(subtypes)s});" % locals()) + subtypes = ( + ",".join(map(lambda t: ("((entity*) %%(schema_name)s_types[%d])" % self.names.index(t)), tys)) % locals() + ) + self.statements.append( + " ((entity*) %(schema_name)s_types[%(index_in_schema)d])->set_subtypes({%(subtypes)s});" % locals() + ) def finalize(self, can_be_instantiated_set): schema_name = self.schema_name.upper() schema_name_title = self.schema_name.capitalize() + def _(): schema_name = self.schema_name.upper() schema_name_title = self.schema_name.capitalize() for type_name in self.names: index_in_schema = self.names.index(type_name) yield "%(schema_name)s_types[%(index_in_schema)d]" % locals() + declarations = ",".join(_()) schema_name_ref = self.strings.append(schema_name) self.statements.append( - ' return new schema_definition(%(schema_name_ref)s, {%(declarations)s}, new %(schema_name)s_instance_factory());' + " return new schema_definition(%(schema_name_ref)s, {%(declarations)s}, new %(schema_name)s_instance_factory());" % locals() ) - self.statements.append("}"); + self.statements.append("}") -# self.statements.append( -# """ -# #if defined(__clang__) -# #elif defined(__GNUC__) || defined(__GNUG__) -# #pragma GCC pop_options -# #elif defined(_MSC_VER) -# #pragma optimize("", on) -# #endif -# """ -# ) + # self.statements.append( + # """ + # #if defined(__clang__) + # #elif defined(__GNUC__) || defined(__GNUG__) + # #pragma GCC pop_options + # #elif defined(_MSC_VER) + # #pragma optimize("", on) + # #endif + # """ + # ) self.statements.extend( ( @@ -340,24 +363,18 @@ class EarlyBoundCodeWriter: ) ) - self.statements[self.statements.index("{factory_placeholder}")] = ( - """ + self.statements[self.statements.index("{factory_placeholder}")] = """ class %(schema_name)s_instance_factory : public IfcParse::instance_factory { virtual IfcUtil::IfcBaseClass* operator()(const IfcParse::declaration* decl, IfcEntityInstanceData&& data) const { %(instance_mapping)s } }; -""" - % locals() - ) +""" % locals() "" - self.statements[self.statements.index("{string_pool_placeholder}")] = ( - """ + self.statements[self.statements.index("{string_pool_placeholder}")] = """ const std::string strings[] = {%s}; -""" - % ",".join(map(lambda s: '"%s"s' % s, self.strings)) - ) +""" % ",".join(map(lambda s: '"%s"s' % s, self.strings)) def __str__(self): return "\n".join(self.statements) @@ -379,16 +396,19 @@ class SchemaClass(codegen.Base): def wrapper(*args, **kwargs): schema_name_upper = mapping.schema.name.upper() declared_type = fn(*args, **kwargs) - if 'simple_type' in declared_type: + if "simple_type" in declared_type: pass else: - match = re.search(r'\((\w+?_[\w+]+?_\w+?)\)', declared_type) + match = re.search(r"\((\w+?_[\w+]+?_\w+?)\)", declared_type) if match: old_decl = match.group(1) - name = old_decl.lower().replace(schema_name.lower() + '_', '').replace('_type', '') + name = old_decl.lower().replace(schema_name.lower() + "_", "").replace("_type", "") idx = [n.lower() for n in x.names].index(name) - declared_type = declared_type.replace(old_decl, '%(schema_name_upper)s_types[%(idx)d]' % locals()) + declared_type = declared_type.replace( + old_decl, "%(schema_name_upper)s_types[%(idx)d]" % locals() + ) return declared_type + return wrapper if code == EarlyBoundCodeWriter else fn @transform_to_indexed diff --git a/src/ifcopenshell-python/ifcopenshell/express/templates.py b/src/ifcopenshell-python/ifcopenshell/express/templates.py index e0398fd58d..564fe881a5 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/templates.py +++ b/src/ifcopenshell-python/ifcopenshell/express/templates.py @@ -217,7 +217,7 @@ const IfcParse::entity& %(schema_name)s::%(name)s::Class() { return *((IfcParse: %(schema_name)s::%(name)s::%(name)s(%(constructor_arguments)s) : %(superclass_num_attrs)s { %(constructor_implementation)s; populate_derived(); } """ -# data_ = e; +# data_ = e; # data_ = new IfcEntityInstanceData(%(schema_name_upper)s_types[%(index_in_schema)d]); optional_attribute_description = "/// Whether the optional attribute %s is defined for this %s" @@ -255,32 +255,16 @@ get_attr_stmt_nested_array = "%(null_check)s aggregate_of_aggregate_of_instance: get_inverse = "if (!file_) { return nullptr; } return file_->getInverse(id_, %(schema_name_upper)s_types[%(type_index)d], %(index)d)->as<%(type)s>();" -set_attr_stmt = ( - "%(check_optional_set_begin)sset_attribute_value(%(index)d, %(star_if_optional)sv);%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -) -set_attr_instance = ( - "%(check_optional_set_begin)sset_attribute_value(%(index)d, v->as());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -) -set_attr_stmt_enum = "%(check_optional_set_begin)sset_attribute_value(%(index)d, EnumerationReference(&%(non_optional_type)s::Class(), (size_t) %(star_if_optional)sv));%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -set_attr_stmt_array = ( - "%(check_optional_set_begin)sset_attribute_value(%(index)d, (%(star_if_optional)sv)->generalize());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -) +set_attr_stmt = "%(check_optional_set_begin)sset_attribute_value(%(index)d, %(star_if_optional)sv);%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" +set_attr_instance = "%(check_optional_set_begin)sset_attribute_value(%(index)d, v->as());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" +set_attr_stmt_enum = "%(check_optional_set_begin)sset_attribute_value(%(index)d, EnumerationReference(&%(non_optional_type)s::Class(), (size_t) %(star_if_optional)sv));%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" +set_attr_stmt_array = "%(check_optional_set_begin)sset_attribute_value(%(index)d, (%(star_if_optional)sv)->generalize());%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" -constructor_stmt = ( - "set_attribute_value(%(index)d, (%(name)s));" -) -constructor_stmt_enum = ( - "set_attribute_value(%(index)d, (EnumerationReference(&%(type)s::Class(),(size_t)%(name)s)));" -) -constructor_stmt_array = ( - "set_attribute_value(%(index)d, (%(name)s)->generalize());" -) -constructor_stmt_derived = ( - "" -) -constructor_stmt_instance = ( - "set_attribute_value(%(index)d, %(name)s ? %(name)s->as() : (IfcUtil::IfcBaseClass*) nullptr);" -) +constructor_stmt = "set_attribute_value(%(index)d, (%(name)s));" +constructor_stmt_enum = "set_attribute_value(%(index)d, (EnumerationReference(&%(type)s::Class(),(size_t)%(name)s)));" +constructor_stmt_array = "set_attribute_value(%(index)d, (%(name)s)->generalize());" +constructor_stmt_derived = "" +constructor_stmt_instance = "set_attribute_value(%(index)d, %(name)s ? %(name)s->as() : (IfcUtil::IfcBaseClass*) nullptr);" constructor_stmt_optional = " if (%(name)s) {%(stmt)s }" From 9ab9da2ca860886a40b0a4f7638f9b97456ed5d7 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 26 Feb 2026 12:36:17 +0100 Subject: [PATCH 050/131] Update black exclude dirs --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 95b61e2c39..febd543cf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ include = ''' |nix/.*.pyi?$ ''' extend-exclude = ''' - src/ifcopenshell-python/ifcopenshell/express/* + src/ifcopenshell-python/ifcopenshell/express/rules/* + |src/ifcopenshell-python/ifcopenshell/express/express_parser.py |src/ifcopenshell-python/ifcopenshell/mvd/* |src/ifcopenshell-python/ifcopenshell/simple_spf/* |src/ifc2ca/templates/* From e3464b395eca262fe529465cff1ca5b6655c4ff6 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 26 Feb 2026 12:38:36 +0100 Subject: [PATCH 051/131] --recursion-limit option in validate.py --- src/ifcopenshell-python/ifcopenshell/validate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index 13f148d520..e1270afb57 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -793,6 +793,12 @@ if __name__ == "__main__": parser.add_argument("files", nargs="+", help="The IFC file to validate.") parser.add_argument("--rules", action="store_true", help="Run express rules.") parser.add_argument("--json", action="store_true", help="Output in JSON format.") + parser.add_argument( + "--recursion-limit", + type=int, + default=-1, + help="Override sys.getrecursionlimit to process express rules on deeply nested structures (e.g 10000)", + ) parser.add_argument( "--fields", action="store_true", @@ -804,6 +810,9 @@ if __name__ == "__main__": filenames: list[str] = args.files some_file_is_invalid = False + if args.recursion_limit > 0: + sys.setrecursionlimit(args.recursion_limit) + for fn in filenames: handler = None if args.json: From 5aa7ba6be68d93e8a12590a95cf026098eed57d2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Feb 2026 17:58:30 +0500 Subject: [PATCH 052/131] IfcConvert - fix Windows builds stuck on `0.8.0` version --- win/build-all-win.py | 20 ++++++++++++++++++-- win/run-cmake.bat | 17 ++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/win/build-all-win.py b/win/build-all-win.py index 087f869ac4..9e8d597185 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -30,6 +30,22 @@ def run(command: list[str]) -> None: subprocess.check_call(command) # nosec B603 +def set_env(var_name: str, value: str) -> tuple[str, str | None]: + """ + :return: Tuple of ``(var_name, old_value)`` to be passed to ``restore_env``. + """ + old_value = os.getenv(var_name) + os.environ[var_name] = value + return var_name, old_value + + +def restore_env(var_name: str, old_value: str | None) -> None: + if old_value is None: + del os.environ[var_name] + else: + os.environ[var_name] = old_value + + def build() -> None: for python_version in PYTHON_VERSIONS: os.environ["PYTHON_VERSION"] = python_version @@ -40,16 +56,16 @@ def build() -> None: text=True, input="y\n", ) + OLD_ADD_COMMIT_SHA = set_env("ADD_COMMIT_SHA", "ON") run( [ str(REPO_WIN / "run-cmake.bat"), "vs2022-x64", "-DENABLE_BUILD_OPTIMIZATIONS=ON", "-DGLTF_SUPPORT=ON", - "-DADD_COMMIT_SHA=ON", - "-DVERSION_OVERRIDE=ON", ] ) + restore_env(*OLD_ADD_COMMIT_SHA) run([str(REPO_WIN / "install-ifcopenshell.bat"), "vs2022-x64", "Release"]) diff --git a/win/run-cmake.bat b/win/run-cmake.bat index 397887eb6c..db3f06593d 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -20,6 +20,9 @@ :: Example usage: :: run-cmake.bat vs2022-x64 :: run-cmake.bat vs2022-x64 -DGLTF_SUPPORT=ON -DHDF5_SUPPORT=OFF +:: +:: Used environment variables: +:: - `ADD_COMMIT_SHA` - if defined then `ADD_COMMIT_SHA` and `VERSION_OVERRIDE` cmake args will be set to `ON`. @if not defined ECHO_ON ( echo off ) @@ -105,7 +108,13 @@ set PYTHON_LIBRARY=%PYTHONHOME%\libs\python%PY_VER_MAJOR_MINOR%.lib :: we can remove it later. if not defined SWIG_INSTALL_DIR set SWIG_INSTALL_DIR=%INSTALL_DIR%\swigwin set JSON_INCLUDE_DIR=%INSTALL_DIR%\json -if not defined ADD_COMMIT_SHA set ADD_COMMIT_SHA=Off +if defined ADD_COMMIT_SHA ( + set ADD_COMMIT_SHA=ON + set VERSION_OVERRIDE=ON +) else ( + set ADD_COMMIT_SHA=OFF + set VERSION_OVERRIDE=OFF +) set CGAL_INSTALL_DIR=%INSTALL_DIR%\cgal set GMP_INSTALL_DIR=%INSTALL_DIR%\mpir @@ -184,13 +193,15 @@ IF NOT "%VS_TOOLSET_HOST%"=="" ( -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ - -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% %ARGUMENTS% + -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% -DVERSION_OVERRIDE=%VERSION_OVERRIDE% ^ + %ARGUMENTS% ) ELSE ( cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% ^ -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ - -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% %ARGUMENTS% + -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% -DVERSION_OVERRIDE=%VERSION_OVERRIDE% ^ + %ARGUMENTS% ) IF NOT %ERRORLEVEL%==0 GOTO :Error From 521c8eae0dc712c901a1d4548c867fe59ba5e545 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Feb 2026 17:58:41 +0500 Subject: [PATCH 053/131] run-cmake.bat - document `USE_NINJA` env var --- win/run-cmake.bat | 1 + 1 file changed, 1 insertion(+) diff --git a/win/run-cmake.bat b/win/run-cmake.bat index db3f06593d..bc6955f619 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -23,6 +23,7 @@ :: :: Used environment variables: :: - `ADD_COMMIT_SHA` - if defined then `ADD_COMMIT_SHA` and `VERSION_OVERRIDE` cmake args will be set to `ON`. +:: - `USE_NINJA` - if defined then the Ninja generator will be used instead of the Visual Studio. @if not defined ECHO_ON ( echo off ) From 40d43732cac5321e535786b12cdf15cb374c580f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Feb 2026 19:18:13 +0500 Subject: [PATCH 054/131] build-deps - bump proj version to avoid errors in cmake 4+ --- win/build-deps.cmd | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/win/build-deps.cmd b/win/build-deps.cmd index c81541104f..59f60412ee 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -249,8 +249,9 @@ IF NOT %ERRORLEVEL%==0 GOTO :Error :proj -IF EXIST "%INSTALL_DIR%\proj-9.2.1" ( - echo Found existing "%INSTALL_DIR%\proj-9.2.1", skipping +set PROJ_VERSION=9.4.1 +IF EXIST "%INSTALL_DIR%\proj-%PROJ_VERSION%" ( + echo Found existing "%INSTALL_DIR%\proj-%PROJ_VERSION%", skipping goto :mpir ) @@ -269,13 +270,13 @@ copy sqlite3.h %INSTALL_DIR%\sqlite3\include popd set DEPENDENCY_NAME=proj -set DEPENDENCY_DIR=%DEPS_DIR%\proj-9.2.1 -call :DownloadFile https://download.osgeo.org/proj/proj-9.2.1.zip "%DEPS_DIR%" proj-9.2.1.zip +set DEPENDENCY_DIR=%DEPS_DIR%\proj-%PROJ_VERSION% +call :DownloadFile https://download.osgeo.org/proj/proj-%PROJ_VERSION%.zip "%DEPS_DIR%" proj-%PROJ_VERSION%.zip IF NOT %ERRORLEVEL%==0 GOTO :Error -call :ExtractArchive proj-9.2.1.zip "%DEPS_DIR%" "%DEPS_DIR%\proj-9.2.1" +call :ExtractArchive proj-%PROJ_VERSION%.zip "%DEPS_DIR%" "%DEPS_DIR%\proj-%PROJ_VERSION%" IF NOT %ERRORLEVEL%==0 GOTO :Error pushd "%DEPENDENCY_DIR%" -call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\proj-9.2.1" ^ +call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\proj-%PROJ_VERSION%" ^ -DSQLITE3_INCLUDE_DIR=%INSTALL_DIR%\sqlite3\include ^ -DSQLITE3_LIBRARY=%INSTALL_DIR%\sqlite3\lib\sqlite3.lib ^ -DENABLE_TIFF=Off -DENABLE_CURL=Off -DBUILD_PROJSYNC=Off ^ From 83fed6257e20675c7c38b412a25d363d0d88be81 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Feb 2026 11:57:41 +0500 Subject: [PATCH 055/131] run-cmake.bat - deduplicate cmake args code --- win/run-cmake.bat | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/win/run-cmake.bat b/win/run-cmake.bat index bc6955f619..2a49a65042 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -190,21 +190,16 @@ if defined USE_NINJA ( ) IF NOT "%VS_TOOLSET_HOST%"=="" ( - cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% -T %VS_TOOLSET_HOST% ^ - -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ - -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ - -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ - -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% -DVERSION_OVERRIDE=%VERSION_OVERRIDE% ^ - %ARGUMENTS% -) ELSE ( - cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% ^ - -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ - -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ - -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ - -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% -DVERSION_OVERRIDE=%VERSION_OVERRIDE% ^ - %ARGUMENTS% + set VS_TOOLSET_OPTION=-T %VS_TOOLSET_HOST% ) +cmake.exe %CMAKELISTS_DIR% -G %GENERATOR% %ARCH_OPTION% %VS_TOOLSET_OPTION% ^ + -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" ^ + -DWITH_ROCKSDB=On -DWITH_ZSTD=On ^ + -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" ^ + -DADD_COMMIT_SHA=%ADD_COMMIT_SHA% -DVERSION_OVERRIDE=%VERSION_OVERRIDE% ^ + %ARGUMENTS% + IF NOT %ERRORLEVEL%==0 GOTO :Error echo. From 38381f44b99b531e1cef42da663739efed54dbce Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Feb 2026 12:05:47 +0500 Subject: [PATCH 056/131] ci-bonsai-daily - generate timestamp once for all builds To avoid running in a situation when some builds are using one tag and some are using another and then unstable repo script fails to find builds for some platforms. --- .github/workflows/ci-bonsai-daily.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-bonsai-daily.yml b/.github/workflows/ci-bonsai-daily.yml index 3b52198f3a..ddfa64a47a 100644 --- a/.github/workflows/ci-bonsai-daily.yml +++ b/.github/workflows/ci-bonsai-daily.yml @@ -24,9 +24,15 @@ jobs: runs-on: ubuntu-latest if: | github.repository == 'IfcOpenShell/IfcOpenShell' + outputs: + timestamp: ${{ steps.timestamp.outputs.timestamp }} steps: - - name: Set env - run: echo ok go + - name: Get current timestamp + id: timestamp + # Include hours and minutes to release tag + # to avoid possibility of unstable repo's index.json + # pointing to the new file when index.json itself wasn't yet updated. + run: echo "timestamp=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT build: needs: activate @@ -67,12 +73,6 @@ jobs: - name: Get current version id: version run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - - name: Get current date - id: date - # Include hours and minutes to release tag - # to avoid possibility of unstable repo's index.json - # pointing to the new file when index.json itself wasn't yet updated. - run: echo "date=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT - name: Compile run: | cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} @@ -88,8 +88,8 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ steps.find_zip.outputs.filepath }} asset_name: ${{ steps.find_zip.outputs.filename }} - release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}} (unstable)" - tag: "bonsai-${{steps.version.outputs.version}}-alpha${{steps.date.outputs.date}}" + release_name: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }} (unstable)" + tag: "bonsai-${{steps.version.outputs.version}}-alpha${{ needs.activate.outputs.timestamp }}" overwrite: true body: "See README in https://github.com/IfcOpenShell/bonsai_unstable_repo/ on how to setup autoupdates for daily Bonsai builds." From 0234809d0b2bfe216b07c3c89064151bc29f686d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Feb 2026 12:13:02 +0500 Subject: [PATCH 057/131] Prefer direct api calls over `tool.Ifc.run` --- .../bonsai/bim/module/system/operator.py | 8 ++++--- src/bonsai/bonsai/tool/project.py | 24 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 089bd9f24b..d365964968 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING import bpy import ifcopenshell.api +import ifcopenshell.api.attribute import ifcopenshell.api.system import ifcopenshell.util.system @@ -445,6 +446,7 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): return "Cycle through flow directions: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED → SOURCE..." def _execute(self, context): + ifc_file = tool.Ifc.get() port = tool.Ifc.get().by_id(self.port_id) if not port or not port.is_a("IfcDistributionPort"): return {"CANCELLED"} @@ -459,7 +461,7 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): } next_direction = flow_cycle_map.get(current_direction, "SOURCE") - tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction}) + ifcopenshell.api.attribute.edit_attributes(ifc_file, product=port, attributes={"FlowDirection": next_direction}) connected_port = tool.System.get_connected_port(port) if connected_port: @@ -470,8 +472,8 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): "NOTDEFINED": "NOTDEFINED", } connected_direction = connected_direction_map.get(next_direction, "NOTDEFINED") - tool.Ifc.run( - "attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction} + ifcopenshell.api.attribute.edit_attributes( + ifc_file, product=connected_port, attributes={"FlowDirection": connected_direction} ) PortData.is_loaded = False diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index da613c3fa9..fc1e4d5e77 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -542,11 +542,11 @@ class Project(bonsai.core.tool.Project): if not ifc_file: raise Exception("No IFC file loaded") - doc = tool.Ifc.run("document.add_information", parent=None) + doc = ifcopenshell.api.document.add_information(ifc_file, parent=None) if ifc_file.schema == "IFC2X3": - tool.Ifc.run( - "document.edit_information", + ifcopenshell.api.document.edit_information( + ifc_file, information=doc, attributes={ "DocumentId": "BLEND_METADATA", @@ -557,8 +557,8 @@ class Project(bonsai.core.tool.Project): }, ) else: - tool.Ifc.run( - "document.edit_information", + ifcopenshell.api.document.edit_information( + ifc_file, information=doc, attributes={ "Identification": "BLEND_METADATA", @@ -578,13 +578,15 @@ class Project(bonsai.core.tool.Project): return ifc_file = tool.Ifc.get() - if not ifc_file: - return - - tool.Ifc.run("document.edit_information", information=doc, attributes={"Location": metadata_filename}) + ifcopenshell.api.document.edit_information( + ifc_file, information=doc, attributes={"Location": metadata_filename} + ) @classmethod def remove_metadata_document_information(cls) -> None: doc = cls.get_metadata_document_information() - if doc: - tool.Ifc.run("document.remove_information", information=doc) + if not doc: + return + + ifc_file = tool.Ifc.get() + ifcopenshell.api.document.remove_information(ifc_file, information=doc) From 7322082a0d0f014ea330f19e6d584b1ac2703070 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Feb 2026 12:18:17 +0500 Subject: [PATCH 058/131] typing --- .../bonsai/bim/module/model/decorator.py | 4 ++-- .../bonsai/bim/module/search/operator.py | 10 ++++---- src/bonsai/bonsai/tool/model.py | 4 ++-- src/bonsai/bonsai/tool/search.py | 24 +++++++++++-------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index f5a1e534da..1a580b9fc7 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -1077,7 +1077,7 @@ class ProductDecorator: data["verts"] = [] # Verts - polyline_vertices = [] + polyline_vertices: list[Vector] = [] polyline_props = tool.Model.get_polyline_props() polyline_data = polyline_props.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] @@ -1197,7 +1197,7 @@ class ProductDecorator: data = {} data["verts"] = [] # Verts - polyline_vertices = [] + polyline_vertices: list[Vector] = [] polyline_props = tool.Model.get_polyline_props() polyline_data = polyline_props.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 83f401a122..d9645fe67a 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -19,7 +19,7 @@ import bisect import json import traceback -from typing import TYPE_CHECKING, Literal, assert_never, get_args +from typing import TYPE_CHECKING, Any, Literal, assert_never, get_args import bpy import ifcopenshell @@ -695,9 +695,9 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): filter_groups = tool.Search.get_filter_groups(module) if tool.Blender.get_addon_preferences().chain_filter_with_set_operations: - filter_structure = [] + filter_structure: list[list[dict[str, Any]]] = [] for filter_group in filter_groups: - group_data = [] + group_data: list[dict[str, Any]] = [] for ifc_filter in filter_group.filters: filter_data = { "type": ifc_filter.type, @@ -852,9 +852,9 @@ class SaveSearch(Operator, tool.Ifc.Operator): query = tool.Search.export_filter_query(filter_groups) results = tool.Search.execute_filter_groups(filter_groups) - filter_structure = [] + filter_structure: list[list[dict[str, Any]]] = [] for filter_group in filter_groups: - group_data = [] + group_data: list[dict[str, Any]] = [] for ifc_filter in filter_group.filters: filter_data = { "type": ifc_filter.type, diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 0f3f4e45ad..48b106fd74 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -2626,7 +2626,7 @@ class Model(bonsai.core.tool.Model): reference_obj: bpy.types.Object, objs: Iterable[bpy.types.Object], align_type: Literal["CENTER", "POSITIVE", "NEGATIVE"], - ): + ) -> None: if align_type == "CENTER": point = reference_obj.matrix_world @ (Vector(reference_obj.bound_box[0]) + (reference_obj.dimensions / 2)) elif align_type == "POSITIVE": @@ -2771,7 +2771,7 @@ class Model(bonsai.core.tool.Model): SvIfcStore.use_bonsai_file = False @classmethod - def create_bmesh_from_vertices(cls, vertices, is_closed=False): + def create_bmesh_from_vertices(cls, vertices: list[Vector], is_closed: bool = False) -> bmesh.types.BMesh: bm = bmesh.new() new_verts = [bm.verts.new(v) for v in vertices] diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index ffd3a1aef3..356addee3a 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -20,7 +20,7 @@ from __future__ import annotations import json from itertools import cycle -from typing import TYPE_CHECKING, Literal, Union +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell.guid @@ -51,7 +51,9 @@ class Search(bonsai.core.tool.Search): @classmethod def import_filter_structure( - cls, filter_structure: list, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + cls, + filter_structure: list[list[dict[str, Any]]], + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup], ) -> None: filter_groups.clear() @@ -188,7 +190,9 @@ class Search(bonsai.core.tool.Search): return "" @classmethod - def execute_filter_groups(cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]) -> set: + def execute_filter_groups( + cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + ) -> set[ifcopenshell.entity_instance]: """ Execute filter groups with simplified chaining support. Within a single group chain, all filters chain sequentially with ADD/SUBTRACT/FILTER modes. @@ -196,10 +200,10 @@ class Search(bonsai.core.tool.Search): """ preferences = tool.Blender.get_addon_preferences() - all_group_results = [] + all_group_results: list[set[ifcopenshell.entity_instance]] = [] - for group_idx, filter_group in enumerate(filter_groups): - group_results = set() + for filter_group in filter_groups: + group_results: set[ifcopenshell.entity_instance] = set() for filter_index, ifc_filter in enumerate(filter_group.filters): if not ifc_filter.value: @@ -245,7 +249,7 @@ class Search(bonsai.core.tool.Search): if group_results: all_group_results.append(group_results) - final_results = set() + final_results: set[ifcopenshell.entity_instance] = set() for group_results in all_group_results: final_results.update(group_results) @@ -262,9 +266,9 @@ class Search(bonsai.core.tool.Search): """ filter_structure = data.get("filter_structure", []) - all_group_results = [] + all_group_results: list[set[ifcopenshell.entity_instance]] = [] for group_data in filter_structure: - group_results = set() + group_results: set[ifcopenshell.entity_instance] = set() for filter_data in group_data: filter_mode = filter_data.get("filter_mode", "ADD") @@ -332,7 +336,7 @@ class Search(bonsai.core.tool.Search): if group_results: all_group_results.append(group_results) - final_results = set() + final_results: set[ifcopenshell.entity_instance] = set() for group_results in all_group_results: final_results.update(group_results) From ac23c7a74bffb0a983899fc080c157ba18f2d22e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:06:32 +0000 Subject: [PATCH 059/131] vs-cfg.cmd - more readable error on supported versions of VS --- win/vs-cfg.cmd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/win/vs-cfg.cmd b/win/vs-cfg.cmd index 794291792b..a5720c48f0 100644 --- a/win/vs-cfg.cmd +++ b/win/vs-cfg.cmd @@ -104,6 +104,9 @@ IF "!GENERATOR!"=="" IF NOT "%VisualStudioVersion%"=="" ( GOTO :GeneratorValid ) ) + call utils\cecho.cmd 0 12 ^ + "Generator is not provided and VisualStudioVersion='%VisualStudioVersion%' is not supported - cannot proceed." + exit /b 1 ) :: Check that the used CMake version supports the chosen generator From 0e90cd81b31baafe039508a5d7b24aec281887ab Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:05:17 +0000 Subject: [PATCH 060/131] vs-cfg.cmd - add support for Visual Studio 18 2026 --- win/vs-cfg.cmd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/win/vs-cfg.cmd b/win/vs-cfg.cmd index a5720c48f0..8df7c41b27 100644 --- a/win/vs-cfg.cmd +++ b/win/vs-cfg.cmd @@ -46,7 +46,8 @@ set GENERATORS[2]="Visual Studio 14 2015" set GENERATORS[3]="Visual Studio 15 2017" set GENERATORS[4]="Visual Studio 16 2019" set GENERATORS[5]="Visual Studio 17 2022" -set LAST_GENERATOR_IDX=5 +set GENERATORS[6]="Visual Studio 18 2026" +set LAST_GENERATOR_IDX=6 :: Is generator shorthand used? set GEN_SHORTHAND=!GENERATOR:vs=! @@ -160,6 +161,7 @@ IF %VS_VER%==2015 ( set "VC_VER=14.0" ) IF %VS_VER%==2017 ( set "VC_VER=14.1" ) IF %VS_VER%==2019 ( set "VC_VER=14.2" ) IF %VS_VER%==2022 ( set "VC_VER=14.3" ) +IF %VS_VER%==2026 ( set "VC_VER=14.5" ) :: determine the argument for Boost bootstrap set BOOST_BOOTSTRAP_VER=vc%VC_VER% From 1fb6227d1303cdfc307ffdd8ecb1e728e97d7542 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:02:03 +0000 Subject: [PATCH 061/131] build-deps - use other mpir fork to support VS 2026 --- win/build-deps.cmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/win/build-deps.cmd b/win/build-deps.cmd index 59f60412ee..407a55dccb 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -298,8 +298,9 @@ IF EXIST "%INSTALL_DIR%\mpir" ( ) set DEPENDENCY_NAME=mpir +:: `mpfr` depends on relative path `..\mpir\config.h`, so dependency name should match exactly. set DEPENDENCY_DIR=%DEPS_DIR%\mpir -call :GitCloneAndCheckoutRevision https://github.com/BrianGladman/mpir.git "%DEPENDENCY_DIR%" +call :GitCloneAndCheckoutRevision https://github.com/Andrej730/mpir-vs2026.git "%DEPENDENCY_DIR%" IF NOT %ERRORLEVEL%==0 GOTO :Error pushd "%DEPENDENCY_DIR%" git reset --hard From d9e488d518eab8bd966f37874f7175da674f0194 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 19 Feb 2026 11:44:47 +0500 Subject: [PATCH 062/131] cmake format --- src/examples/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 79a2399821..a7c99043bd 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -36,11 +36,15 @@ else() endif() macro(build_example exe_name) - set (additional_targets ${ARGN}) + set(additional_targets ${ARGN}) add_executable(${exe_name} ${exe_name}.cpp) if(STANDALONE_PROJECT) - target_link_libraries(${exe_name} IfcOpenShell::IfcParse $<$:IfcOpenShell::${additional_targets}>) + target_link_libraries( + ${exe_name} + IfcOpenShell::IfcParse + $<$:IfcOpenShell::${additional_targets}> + ) else() target_include_directories(${exe_name} PRIVATE "${CMAKE_SOURCE_DIR}/../src") target_link_libraries(${exe_name} IfcParse ${additional_targets}) From d4ebf3f30875ac391bd262dc469933c64dae5ae1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:02:31 +0000 Subject: [PATCH 063/131] cmake - error if `svgpp` submodule is not initialized --- src/svgfill/CMakeLists.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/svgfill/CMakeLists.txt b/src/svgfill/CMakeLists.txt index f2f8c1a062..0d9764013a 100644 --- a/src/svgfill/CMakeLists.txt +++ b/src/svgfill/CMakeLists.txt @@ -36,7 +36,14 @@ message(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}") find_package(LibXml2 REQUIRED) find_package(CGAL REQUIRED) -include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/svgpp/include) +set(SVGPP_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/svgpp/include") +if(NOT EXISTS ${SVGPP_INCLUDE}) + message( + FATAL_ERROR + "Missing svgpp include path, probably you forgot to initialize submodules in git repo. Missing path - '${SVGPP_INCLUDE}'." + ) +endif() +include_directories(${Boost_INCLUDE_DIRS} ${SVGPP_INCLUDE}) file(GLOB LIB_H_FILES src/*.h) file(GLOB LIB_CPP_FILES src/svgfill.cpp src/arrange_polygons.cpp) From ba5ea08aee57caa5c9bb165d3592b132b91499db Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:02:45 +0000 Subject: [PATCH 064/131] Bump swig version to support cmake 4 --- nix/build-all.py | 2 +- win/build-deps.cmd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index 2a4318dc35..133f0c1d46 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -149,7 +149,7 @@ EIGEN_VERSION = "3.4.0" PCRE_VERSION = "8.41" PCRE2_VERSION = "10.32" LIBXML2_VERSION = "2.13.8" -SWIG_VERSION = "4.1.0" +SWIG_VERSION = "4.2.1" OPENCOLLADA_VERSION = "v1.6.68" HDF5_VERSION = "1.13.1" diff --git a/win/build-deps.cmd b/win/build-deps.cmd index 407a55dccb..d4a1a21637 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -574,7 +574,7 @@ IF NOT %ERRORLEVEL%==0 GOTO :Error :SWIG set DEPENDENCY_NAME=SWIG -set SWIG_VERSION=4.1.0 +set SWIG_VERSION=4.2.1 set DEPENDENCY_DIR=%DEPS_DIR%\swig-%SWIG_VERSION% set DEPENDENCY_INSTALL_DIR=%INSTALL_DIR%\swig-%SWIG_VERSION% echo SWIG_INSTALL_DIR=%DEPENDENCY_INSTALL_DIR%>>"%~dp0\%BUILD_DEPS_CACHE_PATH%" From aebcb676f23ca60ba8f2a580ad258c9034f975cb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:03:19 +0000 Subject: [PATCH 065/131] windows - add occt patch to support cmake 4 --- win/patches/V7_8_1.patch | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/win/patches/V7_8_1.patch b/win/patches/V7_8_1.patch index 842a52bad6..9f185521c2 100644 --- a/win/patches/V7_8_1.patch +++ b/win/patches/V7_8_1.patch @@ -84,3 +84,16 @@ index c9399159f1..0aa55392f9 100644 endif() if (BUILD_SHARED_LIBS AND NOT "${BUILD_SHARED_LIBRARY_NAME_POSTFIX}" STREQUAL "") +diff --git a/adm/cmake/cotire.cmake b/adm/cmake/cotire.cmake +index acdca71a9f..6c6e29b374 100644 +--- a/adm/cmake/cotire.cmake ++++ b/adm/cmake/cotire.cmake +@@ -37,7 +37,7 @@ set(__COTIRE_INCLUDED TRUE) + if (NOT CMAKE_SCRIPT_MODE_FILE) + cmake_policy(PUSH) + endif() +-cmake_minimum_required(VERSION 2.8.12) ++cmake_minimum_required(VERSION 3.5) + if (NOT CMAKE_SCRIPT_MODE_FILE) + cmake_policy(POP) + endif() From 3807479e42beb1b65c8974ac26579d69b2754e93 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:04:19 +0000 Subject: [PATCH 066/131] build-deps - update occt config to support cmake 4 And also to make it work in sync with `build-all.py`. --- win/build-deps.cmd | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/win/build-deps.cmd b/win/build-deps.cmd index d4a1a21637..846b36dd19 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -511,9 +511,18 @@ cd "%DEPENDENCY_DIR%" :: TODO: remove CMAKE_DEBUG_POSTFIX setting later. :: Temporarily explicitly set `CMAKE_DEBUG_POSTFIX` to empty to override it's perviously being set to `d`. :: OCCT don't need it, since it's layout is separating debug and release build by different folders. +:: +:: OCCT 7.8.1 we're using is becoming old and it was targeting cmake 3.1+. +::To make it buildable on cmake 4, we override policy version, but it may have some quirks in the future and we may consider version bump. call :RunCMake -DINSTALL_DIR="%DEPENDENCY_INSTALL_DIR%" -DBUILD_LIBRARY_TYPE="Static" -DCMAKE_DEBUG_POSTFIX="" ^ - -DBUILD_MODULE_Draw=0 -DUSE_FREETYPE=OFF ^ - -DBUILD_USE_PCH=ON + -DBUILD_MODULE_Draw=0 ^ + -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF ^ + -DUSE_XLIB=OFF ^ + -DUSE_FREETYPE=OFF ^ + -DUSE_OPENGL=OFF ^ + -DUSE_GLES2=OFF ^ + -DBUILD_USE_PCH=ON ^ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 if not %ERRORLEVEL%==0 goto :Error :: whole program optimization avoids Visual C++ hanging when compiling 32-bit release OCCT up to version 7.4.0 From 8bfceec1fde3f2d664aa7cae1e1b29afd3e54015 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:04:31 +0000 Subject: [PATCH 067/131] vs-cfg.cmd - document some output vars --- win/vs-cfg.cmd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/win/vs-cfg.cmd b/win/vs-cfg.cmd index 8df7c41b27..d89f3fa88b 100644 --- a/win/vs-cfg.cmd +++ b/win/vs-cfg.cmd @@ -35,6 +35,11 @@ :: "vs2019-x86-v141_xp" => cmake -G "Visual Studio 16 2019" -A Win32 -T v141_xp :: :: NOTE: The delayed environment variable expansion needs to be enabled before calling this. +:: +:: Output variables: +:: - VC_VER - e.g. "14.5" +:: - VS_VER - e.g. "2026" +:: - BOOST_BOOTSTRAP_VER - e.g. "vc145" @if not defined ECHO_ON ( echo off ) From 88a57172952478dcdd43e841c8e60de4b2528e0b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Feb 2026 15:04:45 +0000 Subject: [PATCH 068/131] build-deps.cmd - fix issue building opencollada in cmake 4 --- win/build-deps.cmd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/win/build-deps.cmd b/win/build-deps.cmd index 846b36dd19..f55b27043d 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -464,8 +464,10 @@ IF NOT %ERRORLEVEL%==0 git apply --reject --whitespace=fix "%~dp0patches\OpenCOL :: uncomment to following line in order to delete the CMakeCache.txt always if experiencing problems. REM IF EXIST "%DEPENDENCY_DIR%\%BUILD_DIR%\CMakeCache.txt". del "%DEPENDENCY_DIR%\%BUILD_DIR%\CMakeCache.txt" :: NOTE Enforce that the embedded LibXml2 and PCRE are used as there might be problems with arbitrary versions of the libraries. +:: OpenCOLLADA is ancient at this point and allows cmake 2.6+, which results in error in cmake 4, so we override minimum cmake version. call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\%DEPENDENCY_INSTALL_NAME%" -DUSE_STATIC_MSVC_RUNTIME=0 -DCMAKE_DEBUG_POSTFIX=d ^ - -DLIBXML2_LIBRARIES="" -DLIBXML2_INCLUDE_DIR="" -DPCRE_INCLUDE_DIR="" -DPCRE_LIBRARIES="" + -DLIBXML2_LIBRARIES="" -DLIBXML2_INCLUDE_DIR="" -DPCRE_INCLUDE_DIR="" -DPCRE_LIBRARIES="" ^ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 IF NOT %ERRORLEVEL%==0 GOTO :Error REM IF NOT EXIST "%DEPS_DIR%\OpenCOLLADA\%BUILD_DIR%\lib\%DEBUG_OR_RELEASE%\OpenCOLLADASaxFrameworkLoader.lib". call :BuildCMakeProject "%DEPENDENCY_DIR%\%BUILD_DIR%" %DEBUG_OR_RELEASE% From dcac336b984f8b759791f73c12c192ad864b7eab Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 9 Feb 2026 12:12:04 +0500 Subject: [PATCH 069/131] tool.ps1 - refer to cecho.cmd directly, use `return` instead of `exit 0` Which is useful when debugging and calling tools.ps1 directly - less thing to modify to make it work. Also replaced `exit 0` with `return`, so it would be possible to reuse functions inside `tools.ps1` --- win/utils/tools.ps1 | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/win/utils/tools.ps1 b/win/utils/tools.ps1 index c143581789..0e5cdadfc7 100644 --- a/win/utils/tools.ps1 +++ b/win/utils/tools.ps1 @@ -2,6 +2,8 @@ Set-PSDebug -Trace 0 Set-StrictMode -Version 3 $ErrorActionPreference = "Stop" +$cecho = "$PSScriptRoot\cecho.cmd" + # Create marker file to indicate whether Release or Debug build was installed. function mark { @@ -18,7 +20,7 @@ function mark { if (Test-Path -Path $marker_filepath) { return } - cecho.cmd 0 13 "Marking installation in '$installation_dir' with '$ENV:MARKER_FILE'." + . $cecho 0 13 "Marking installation in '$installation_dir' with '$ENV:MARKER_FILE'." New-Item -Path $marker_filepath -ItemType File | Out-Null } @@ -76,7 +78,7 @@ function mark_based_on_artifacts { if (Test-Path -Path $marker_filepath) { return } - cecho.cmd 0 13 "Found artifact '$artifact' for dependency '$dependency_name' $env:BUILD_CFG." + . $cecho 0 13 "Found artifact '$artifact' for dependency '$dependency_name' $env:BUILD_CFG." & mark $installation_dir } @@ -138,12 +140,11 @@ function extract_file { [string]$dir_after_extraction ) if (Test-Path -Path "$dir_after_extraction") { - cecho.cmd 0 13 "$dependency_name already extracted into '$dir_after_extraction'. Skipping." - exit 0 + . $cecho 0 13 "$dependency_name already extracted into '$dir_after_extraction'. Skipping." + return } - cecho.cmd 0 13 "Extracting $dependency_name into '$destination_dir' from '$filename'." + . $cecho 0 13 "Extracting $dependency_name into '$destination_dir' from '$filename'." 7za x "$filename" -o"$destination_dir" - exit 0 } @@ -161,13 +162,12 @@ function download_file { mkdir "$destination_dir" -Force | Out-Null pushd "$destination_dir" if (Test-Path -Path "$filename") { - cecho.cmd 0 13 "$dependency_name already downloaded. Skipping." - exit 0 + . $cecho 0 13 "$dependency_name already downloaded. Skipping." + return } - cecho.cmd 0 13 "Downloading $dependency_name into '$destination_dir'" + . $cecho 0 13 "Downloading $dependency_name into '$destination_dir'" Invoke-WebRequest $url -OutFile $filename - exit 0 } @@ -185,21 +185,20 @@ function git_clone_and_checkout_revision { [string]$revision ) if (Test-Path -Path "$dest_dir") { - cecho.cmd 0 13 "Cloning $dependency_name is already cloned." - exit 0 + . $cecho 0 13 "Cloning $dependency_name is already cloned." + return } - cecho.cmd 0 13 "Cloning $dependency_name into '$dest_dir'." + . $cecho 0 13 "Cloning $dependency_name into '$dest_dir'." pushd "$env:DEPS_DIR" git clone $git_url $dest_dir popd pushd "$dest_dir" git fetch - cecho.cmd 0 13 "Checking out $dependency_name revision $revision." + . $cecho 0 13 "Checking out $dependency_name revision $revision." git reset --hard git checkout $revision popd - exit 0 } function install_cmake_project { @@ -212,12 +211,11 @@ function install_cmake_project { [string]$configuration ) pushd "$build_dir" - cecho.cmd 0 13 "Installing $dependency_name ($configuration). Please be patient, this may take a while." + . $cecho 0 13 "Installing $dependency_name ($configuration). Please be patient, this may take a while." $command = "cmake --install . --config $configuration" - cecho.cmd 0 13 "$command" + . $cecho 0 13 "$command" Invoke-Expression $command popd - exit 0 } From 54a6fb651eda365ee25662ec2ce639c05e7a2c30 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 9 Feb 2026 12:12:37 +0500 Subject: [PATCH 070/131] tool.ps1 - support commands with 0 args No such commands atm though. --- win/utils/tools.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/win/utils/tools.ps1 b/win/utils/tools.ps1 index 0e5cdadfc7..322e306651 100644 --- a/win/utils/tools.ps1 +++ b/win/utils/tools.ps1 @@ -223,7 +223,12 @@ function main { & setup_build_cfg # Dispatch command. $command = $Args[0] - $command_args = $Args[1..($args.Count - 1)] + if ($args.Count -gt 1) { + $command_args = $Args[1..($args.Count - 1)] + } + else { + $command_args = @() + } & $command @command_args } From 3ec9d695560d2483b4fe92e899a197477dab7ef6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 9 Feb 2026 12:13:37 +0500 Subject: [PATCH 071/131] build-deps - support building Boost for VS2026 --- win/build-deps.cmd | 14 ++++++++++++++ win/utils/tools.ps1 | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/win/build-deps.cmd b/win/build-deps.cmd index f55b27043d..811f381f19 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -414,6 +414,11 @@ if exist "%DEPS_DIR%\boost-%BOOST_VERSION%". ( ren %DEPS_DIR%\boost-%BOOST_VERSION% boost_%BOOST_VER% ) +:: As boost 1.90.0 it still includes b2 that doesn't support vc145 (not to mention older boost versions). +:: So to support vc145 we download b2 separately (only if we do use vc145). +call :check_boost_vc145_compatibility "%VC_VER%" "%DEPS_DIR%" "%DEPENDENCY_DIR%" +if NOT %ERRORLEVEL%==0 GOTO :Error + :: Build Boost build script if not exist "%DEPENDENCY_DIR%\project-config.jam". ( cd "%DEPS_DIR%" @@ -930,6 +935,15 @@ exit /b 0 IF NOT %ERRORLEVEL%==0 GOTO :Error exit /b 0 +:: Params: +:: - %1 - VC_VER +:: - %2 - DEPS_DIR +:: - %3 - BOOST_ROOT +:check_boost_vc145_compatibility +%PWSH_TOOLS% check_boost_vc145_compatibility "%1" "%2" "%3" +IF NOT %ERRORLEVEL%==0 GOTO :Error +exit /b 0 + :: PrintUsage - Prints usage information :PrintUsage call "%~dp0\utils\cecho.cmd" 0 10 "Requirements for a successful execution:" diff --git a/win/utils/tools.ps1 b/win/utils/tools.ps1 index 322e306651..583e4cfb3b 100644 --- a/win/utils/tools.ps1 +++ b/win/utils/tools.ps1 @@ -219,6 +219,43 @@ function install_cmake_project { } +function check_boost_vc145_compatibility { + param( + [Parameter(Mandatory = $true)] + [string]$VC_VER, + [Parameter(Mandatory = $true)] + [string]$DEPS_DIR, + [Parameter(Mandatory = $true)] + [string]$BOOST_ROOT + ) + + $boost_build_path = "$BOOST_ROOT/tools/build" + + if ($VC_VER -ne "14.5") { + . $cecho 0 13 "VC_VER is not 14.5, no need to install updated b2." + return + } + + $res = Select-String -Path "$boost_build_path/src/engine/build.bat" -Pattern 'vc143, vc145' -Quiet; + if ($res) { + . $cecho 0 13 "vc145 already supported, no need to install updated b2." + return + } + + $b2_version = "5.4.2" + $b2_stem = "b2-$b2_version" + $b2_path = "$DEPS_DIR\$b2_stem" + $b2_filename = "$b2_stem.zip" + + & download_file "b2" "https://github.com/bfgroup/b2/releases/download/$b2_version/$b2_filename" "$DEPS_DIR" "$b2_filename" + & extract_file "b2" "$b2_filename" "$DEPS_DIR" "$b2_path" + + . $cecho 0 13 "Installing b2 with vc145 support..." + Remove-Item -Recurse -Path "$boost_build_path" + Copy-Item -Path "$b2_path" -Destination "$boost_build_path" -Recurse + . $cecho 0 13 "b2 with vc145 support installed." +} + function main { & setup_build_cfg # Dispatch command. From 5ebd4256a1bcdc1e77b8ff2c485372b3f6d2021e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Feb 2026 12:48:09 +0500 Subject: [PATCH 072/131] build-all-win.py - fix missing compression Resulting in larger zip files for builds, reported in 7404 --- win/build-all-win.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/win/build-all-win.py b/win/build-all-win.py index 9e8d597185..2c0533c253 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -6,6 +6,7 @@ but also archives them to '~/outputs'. import os import subprocess +import zipfile from pathlib import Path from zipfile import ZipFile @@ -77,7 +78,7 @@ def archive_executables() -> None: if file.suffix.lower() != ".exe": continue zip_name = ZIP_TEMPLATE.format(package_name=file.stem) - with ZipFile(OUTPUT_DIR / zip_name, "w") as zipf: + with ZipFile(OUTPUT_DIR / zip_name, "w", compression=zipfile.ZIP_DEFLATED) as zipf: zipf.write(file, arcname=file.name) print(f"{file} -> {zip_name}") From c649a4b5225256f1c9f264a2bfdf0c4dcedf2c57 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Feb 2026 17:12:54 +0500 Subject: [PATCH 073/131] build-all - don't fail silently on missing Python dependencies --- nix/build-all.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index 133f0c1d46..73ac36ac34 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -1101,23 +1101,15 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"]) for PYTHON_VERSION in PYTHON_VERSIONS: - try: - build_dependency( - f"python-{PYTHON_VERSION}", - "autoconf", - PYTHON_CONFIGURE_ARGS, - f"http://www.python.org/ftp/python/{PYTHON_VERSION}/", - f"Python-{PYTHON_VERSION}.tgz", - ) - except RuntimeError as e: - # Sometimes setting up modules such as pip/lzma can cause - # the python installer script to return a non zero exit - # code where actually the headers and dynamic libraries - # are installed correctly. This is all we need so we catch - # the exception and only reraise if a partially successful - # install is not detected. - if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")): - raise e + # Don't fail silently on missing Python dependencies (e.g. openssl or zlib), + # because later ifcopenshell-python build will fail too but in a more confusing way. + build_dependency( + f"python-{PYTHON_VERSION}", + "autoconf", + PYTHON_CONFIGURE_ARGS, + f"http://www.python.org/ftp/python/{PYTHON_VERSION}/", + f"Python-{PYTHON_VERSION}.tgz", + ) if MAC_CROSS_COMPILE_INTEL: assert original_path From 59c28b5ae6c2acb02d01c835f772810dccf8c4cb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 13 Feb 2026 11:45:54 +0500 Subject: [PATCH 074/131] build_rocky - use `dnf` instead of `yum` It's using `dnf` either way, but just to make it more explicit. --- .github/workflows/build_rocky.yml | 4 ++-- .github/workflows/build_rocky_arm.yml | 4 ++-- nix/build-all.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index 662aa04f26..e613f8a572 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -11,8 +11,8 @@ jobs: steps: - name: Install Dependencies run: | - yum update -y - yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ + dnf update -y + dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \ sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \ readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \ diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index 445f49658a..36d7b01975 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -11,8 +11,8 @@ jobs: steps: - name: Install Dependencies run: | - yum update -y - yum install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ + dnf update -y + dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \ sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \ readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \ diff --git a/nix/build-all.py b/nix/build-all.py index 73ac36ac34..7adbfbcfd8 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -93,7 +93,7 @@ Used environment variables: # $ brew install git bison autoconf automake libffi cmake # # # # on RHEL-related distros: # -# $ yum install git gcc gcc-c++ autoconf bison make cmake # +# $ dnf install git gcc gcc-c++ autoconf bison make cmake # # mesa-libGL-devel libffi-devel fontconfig-devel bzip2 # # automake patch byacc xz # From c8ca904333e3f1321fcc509df6fc117afc025b7c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 13 Feb 2026 12:09:57 +0500 Subject: [PATCH 075/131] build-all - distinct command and path in logs --- 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 7adbfbcfd8..e435ec8eba 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -498,7 +498,7 @@ def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = collector.append(line) pipe.close() - logger.debug(f"running command {' '.join(cmds)} in directory {cwd}") + logger.debug(f"running command `{' '.join(cmds)}` in directory '{cwd}'") stdout: list[str] = [] stderr: list[str] = [] From 5ea4290920ee496b2c6082552d93bb125900e75e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 13 Feb 2026 12:41:48 +0500 Subject: [PATCH 076/131] build-all - ensure `bison` is installed --- nix/build-all.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nix/build-all.py b/nix/build-all.py index e435ec8eba..df092d6048 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -433,13 +433,16 @@ print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t # Check that required tools are in PATH yacc = "yacc" # Used during swig building process, installed with `bison` on Debian / `byacc` on Red Hat. +bison = "bison" + missing_commands: "list[str]" = [] -required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz] +required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison] if "wasm" in flags: # Skip swig build for WASM. required_commands.append("swig") required_commands.append("pyodide") required_commands.remove(yacc) + required_commands.remove(bison) for cmd in required_commands: if shutil.which(cmd) is None: From d43b9ee3535af388cdd60c01cf22d66e30816104 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 13 Feb 2026 17:46:37 +0500 Subject: [PATCH 077/131] build-all - ensure Python was built with openssl --- nix/build-all.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nix/build-all.py b/nix/build-all.py index df092d6048..fb9f38a45b 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -294,6 +294,7 @@ DEPS_DIR = os.getenv("DEPS_DIR", DEFAULT_DEPS_DIR) if not os.path.exists(DEPS_DIR): os.makedirs(DEPS_DIR) +INSTALL_DIR = Path(DEPS_DIR) / "install" BUILD_CFG = os.getenv("BUILD_CFG", "RelWithDebInfo") @@ -1113,6 +1114,10 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag f"http://www.python.org/ftp/python/{PYTHON_VERSION}/", f"Python-{PYTHON_VERSION}.tgz", ) + python_bin = INSTALL_DIR / f"python-{PYTHON_VERSION}" / "bin" / "python3" + # `_ssl` module is present -> we will be able to install `numpy` later + # to verify IfcOpenShell installation + run([str(python_bin), "-c", "import _ssl"]) if MAC_CROSS_COMPILE_INTEL: assert original_path @@ -1536,7 +1541,7 @@ if "IfcOpenShell-Python" in targets: compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable) else: for python_version in PYTHON_VERSIONS: - python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}" + python_path = INSTALL_DIR / f"python-{python_version}" module_dir = compile_python_wrapper(python_version, python_path=python_path) assert module_dir # Not sure why, but added after reading this in the logs From 634600b65fcb0ccac6ba1e3d038f55653720cb47 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 13 Feb 2026 18:13:43 +0500 Subject: [PATCH 078/131] FindOpenCASCADE - rescan dependencies for cmake config --- cmake/FindOpenCASCADE.cmake | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmake/FindOpenCASCADE.cmake b/cmake/FindOpenCASCADE.cmake index f78ee33b99..aac5f0521e 100644 --- a/cmake/FindOpenCASCADE.cmake +++ b/cmake/FindOpenCASCADE.cmake @@ -43,6 +43,17 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR) set_target_properties(TKernel PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}") endif() + if( + OpenCASCADE_VERSION VERSION_LESS "7.9.0" + AND CMAKE_VERSION GREATER_EQUAL "3.24" + AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU" + ) + # Before 7.9.0 targets in OCCT cmake configs are not linked to each other + # leading to missing symbols on Unix. Link them as a single group as a workaround. + # Only needed for gcc, because other compilers (e.g. Apple Clang, MSVC) do rescan automatically. + set(OpenCASCADE_LIBRARIES "$") + endif() + if(OpenCASCADE_VERSION VERSION_LESS "7.9.0" AND WIN32) # Bug in OCCT cmake configs < 7.9.0 - missing linked library. list(APPEND OpenCASCADE_LIBRARIES WSOCK32.lib) From 4c4eed5dd4ddedac151e2fd1ef37732b7da341d7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 13 Feb 2026 18:15:59 +0500 Subject: [PATCH 079/131] build_rocky - switch to rocky 9 As rocky 8 is not updating anymore for 2 years and we need some updated dependencies (e.g. `bison` 3.5+ for newer version of `swig`). --- .github/workflows/build_rocky.yml | 8 ++++---- .github/workflows/build_rocky_arm.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index e613f8a572..217620987a 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -6,13 +6,13 @@ on: jobs: build_ifcopenshell: runs-on: ubuntu-22.04 - container: rockylinux:8 + container: rockylinux:9 steps: - name: Install Dependencies run: | dnf update -y - dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ + dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \ bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \ sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \ readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \ @@ -38,7 +38,7 @@ jobs: with: repository: IfcOpenShell/build-outputs path: ./build - ref: rockylinux8-x64 + ref: rockylinux9-x64 lfs: true token: ${{ secrets.BUILD_REPO_TOKEN }} @@ -51,7 +51,7 @@ jobs: # TODO: Use tag after 1.2.20 releases. uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 with: - key: ubuntu-22.04-${{ runner.arch }}-rockylinux8 + key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 - name: Run Build Script shell: bash diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index 36d7b01975..d195b7b868 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -6,13 +6,13 @@ on: jobs: build_ifcopenshell: runs-on: ubuntu-22.04-arm - container: arm64v8/rockylinux:8 + container: arm64v8/rockylinux:9 steps: - name: Install Dependencies run: | dnf update -y - dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 \ + dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \ bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \ sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \ readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \ @@ -38,7 +38,7 @@ jobs: with: repository: IfcOpenShell/build-outputs path: ./build - ref: rockylinux8-arm64 + ref: rockylinux9-arm64 lfs: true token: ${{ secrets.BUILD_REPO_TOKEN }} @@ -51,7 +51,7 @@ jobs: # TODO: Use tag after 1.2.20 releases. uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 with: - key: ubuntu-22.04-${{ runner.arch }}-rockylinux8 + key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 - name: Run Build Script shell: bash From 6face696cb30a9e1ee9f03d54f97b3ef7e17a688 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 16 Feb 2026 13:57:45 +0500 Subject: [PATCH 080/131] build-all - use cmake arg instead of a patch to disable ExpToCasExe --- nix/build-all.py | 20 +++++++++---------- nix/patches/occt/no_ExpToCasExe.patch | 22 +++++++++------------ nix/patches/occt/no_ExpToCasExe_7_7_2.patch | 13 ------------ nix/patches/occt/no_ExpToCasExe_7_8_1.patch | 13 ------------ nix/patches/occt/no_ExpToCasExe_7_9_1.patch | 13 ------------ 5 files changed, 18 insertions(+), 63 deletions(-) delete mode 100644 nix/patches/occt/no_ExpToCasExe_7_7_2.patch delete mode 100644 nix/patches/occt/no_ExpToCasExe_7_8_1.patch delete mode 100644 nix/patches/occt/no_ExpToCasExe_7_9_1.patch diff --git a/nix/build-all.py b/nix/build-all.py index fb9f38a45b..4faed1b54a 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -955,21 +955,18 @@ if "swig" in targets: ) if USE_OCCT and "occ" in targets: - patches = [] + occt_args: "list[str]" = [] + patches: "list[str]" = [] if OCCT_VERSION < "7.4": patches.append("./patches/occt/enable-exception-handling.patch") - if OCCT_VERSION == "7.7.1": + # Skip ExpToCasExe as we don't need it and it requires additional dependencies. + # Before 7.7.2 ExpToCasExe is part of DataExchange, DETools doesn't exist yet. + # Since we do need DataExchange (used for IgesSerializer), we use a patch to skip only ExpToCasExe. + if "7.7.2" > OCCT_VERSION >= "7.7": patches.append("./patches/occt/no_ExpToCasExe.patch") - - if OCCT_VERSION == "7.7.2": - patches.append("./patches/occt/no_ExpToCasExe_7_7_2.patch") - - if OCCT_VERSION == "7.8.1": - patches.append("./patches/occt/no_ExpToCasExe_7_8_1.patch") - - if OCCT_VERSION == "7.9.1": - patches.append("./patches/occt/no_ExpToCasExe_7_9_1.patch") + elif OCCT_VERSION >= "7.7.2": + occt_args.append("-DBUILD_MODULE_DETools=OFF") if "wasm" in flags: patches.append("./patches/occt/no_em_js.patch") @@ -990,6 +987,7 @@ if USE_OCCT and "occ" in targets: f"-DUSE_GLES2=OFF", f"-DCMAKE_POLICY_VERSION_MINIMUM=3.5", *MAC_CROSS_COMPILE_INTEL_ARGS, + *occt_args, ], download_url="https://github.com/Open-Cascade-SAS/OCCT", download_name="occt", diff --git a/nix/patches/occt/no_ExpToCasExe.patch b/nix/patches/occt/no_ExpToCasExe.patch index 6e10f2a9fb..2f99845925 100644 --- a/nix/patches/occt/no_ExpToCasExe.patch +++ b/nix/patches/occt/no_ExpToCasExe.patch @@ -1,13 +1,9 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index fd17283f77..6cecf9dad3 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -826,6 +826,8 @@ if (EMSCRIPTEN) - list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison") +--- a/adm/MODULES ++++ b/adm/MODULES +@@ -3,5 +3,5 @@ ModelingData TKG2d TKG3d TKGeomBase TKBRep + ModelingAlgorithms TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing + Visualization TKService TKV3d TKOpenGl TKOpenGles TKMeshVS TKIVtk TKD3DHost + ApplicationFramework TKCDF TKLCAF TKCAF TKBinL TKXmlL TKBin TKXml TKStdL TKStd TKTObj TKBinTObj TKXmlTObj TKVCAF +-DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress ExpToCasExe ++DataExchange TKXDE TKXSBase TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP TKIGES TKXCAF TKXDEIGES TKXDESTEP TKSTL TKVRML TKXmlXCAF TKBinXCAF TKRWMesh TKXDECascade TKExpress + Draw TKDraw TKTopTest TKOpenGlTest TKOpenGlesTest TKD3DHostTest TKViewerTest TKXSDRAW TKDCAF TKXDEDRAW TKTObjDRAW TKQADraw TKIVtkDraw DRAWEXE diff --git a/nix/patches/occt/no_ExpToCasExe_7_7_2.patch b/nix/patches/occt/no_ExpToCasExe_7_7_2.patch deleted file mode 100644 index 8b9f924e83..0000000000 --- a/nix/patches/occt/no_ExpToCasExe_7_7_2.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 1bacca1a48..11f931ad39 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -820,6 +820,8 @@ else() - OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE") - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison") diff --git a/nix/patches/occt/no_ExpToCasExe_7_8_1.patch b/nix/patches/occt/no_ExpToCasExe_7_8_1.patch deleted file mode 100644 index 63d6fd3206..0000000000 --- a/nix/patches/occt/no_ExpToCasExe_7_8_1.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 86905287dc..9d0bce984c 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -828,6 +828,8 @@ else() - OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE") - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/bison") diff --git a/nix/patches/occt/no_ExpToCasExe_7_9_1.patch b/nix/patches/occt/no_ExpToCasExe_7_9_1.patch deleted file mode 100644 index abe2b20ef9..0000000000 --- a/nix/patches/occt/no_ExpToCasExe_7_9_1.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 34300d41ad..09b2e0d45f 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -721,6 +721,8 @@ else() - OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE") - endif() - -+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe) -+ - # bison - if (BUILD_YACCLEX) - list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison") From 8eb0641e7020fa26b9cfb3354e07fabc45f6865f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 17 Feb 2026 12:09:45 +0500 Subject: [PATCH 081/131] build-all - mention zlib requirement --- nix/build-all.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index 4faed1b54a..52ba92c287 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -75,19 +75,19 @@ Used environment variables: # # # for python37 to install correctly additionally: # # * libffi(-dev[el]) # -# for Python build we also needs ssl # +# for Python build we also needs ssl and zlib # # (since we do `pip install numpy` at the end) # # * libssl-dev # # # # on debian 7.8 these can be obtained with: # # $ apt-get install git gcc g++ autoconf bison bzip2 cmake # # mesa-common-dev libffi-dev libfontconfig1-dev # -# libssl-dev xz # +# libssl-dev xz zlib1g-dev # # # # on ubuntu 14.04: # # $ apt-get install git gcc g++ autoconf bison make cmake # # mesa-common-dev libffi-dev libfontconfig1-dev # -# libssl-dev xz-utils # +# libssl-dev xz-utils zlib1g-dev # # # # on OS X El Capitan with homebrew: # # $ brew install git bison autoconf automake libffi cmake # From 6492fdeb05c33233afb8e7476948dcc162ee9cfd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 20 Feb 2026 13:19:06 +0500 Subject: [PATCH 082/131] build-all - add zlib and openssl to RHEL packages --- 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 52ba92c287..ab9a568dba 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -95,7 +95,7 @@ Used environment variables: # on RHEL-related distros: # # $ dnf install git gcc gcc-c++ autoconf bison make cmake # # mesa-libGL-devel libffi-devel fontconfig-devel bzip2 # -# automake patch byacc xz # +# automake patch byacc xz zlib-devel openssl-devel # """ From 4591b6d9266e190b089ded0d1f8ca2db6a07e228 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Feb 2026 20:40:45 +0500 Subject: [PATCH 083/131] cmake - ignore rocksdb shared library If makes code target it by default if it's available, leading to errors below, since we don't really support using shared rocksdb. See some more details in the code comment. IfcPythonPYTHON_wrap.cxx.obj : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::Cleanable::~Cleanable(void)" (??1Cleanable@rocksdb@@QEAA@XZ) IfcPythonPYTHON_wrap.cxx.obj : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::Cleanable::Cleanable(void)" (??0Cleanable@rocksdb@@QEAA@XZ) IfcPythonPYTHON_wrap.cxx.obj : error LNK2001: unresolved external symbol "public: class std::basic_string,class std::allocator > __cdecl rocksdb::Slice::ToString(bool)const " (?ToString@Slice@rocksdb@@QEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@_N@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "const rocksdb::WriteBatch::`vftable'" (??_7WriteBatch@rocksdb@@6B@) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual __cdecl rocksdb::WriteBatch::~WriteBatch(void)" (??1WriteBatch@rocksdb@@UEAA@XZ) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::WriteBatch::WriteBatch(unsigned __int64,unsigned __int64,unsigned __int64,unsigned __int64)" (??0WriteBatch@rocksdb@@QEAA@_K000@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::ColumnFamilyOptions::ColumnFamilyOptions(void)" (??0ColumnFamilyOptions@rocksdb@@QEAA@XZ) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string,class std::allocator > __cdecl rocksdb::Configurable::GetOptionName(class std::basic_string,class std::allocator > const &)const " (?GetOptionName@Configurable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBV34@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string,class std::allocator > __cdecl rocksdb::Configurable::SerializeOptions(struct rocksdb::ConfigOptions const &,class std::basic_string,class std::allocator > const &)const " (?SerializeOptions@Configurable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBUConfigOptions@2@AEBV34@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual bool __cdecl rocksdb::Configurable::OptionsAreEqual(struct rocksdb::ConfigOptions const &,class rocksdb::OptionTypeInfo const &,class std::basic_string,class std::allocator > const &,void const * const,void const * const,class std::basic_string,class std::allocator > *)const " (?OptionsAreEqual@Configurable@rocksdb@@MEBA_NAEBUConfigOptions@2@AEBVOptionTypeInfo@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QEBX3PEAV56@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ParseOption(struct rocksdb::ConfigOptions const &,class rocksdb::OptionTypeInfo const &,class std::basic_string,class std::allocator > const &,class std::basic_string,class std::allocator > const &,void *)" (?ParseOption@Configurable@rocksdb@@MEAA?AVStatus@2@AEBUConfigOptions@2@AEBVOptionTypeInfo@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@2PEAX@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ConfigureOptions(struct rocksdb::ConfigOptions const &,class std::unordered_map,class std::allocator >,class std::basic_string,class std::allocator >,struct std::hash,class std::allocator > >,struct std::equal_to,class std::allocator > >,class std::allocator,class std::allocator > const ,class std::basic_string,class std::allocator > > > > const &,class std::unordered_map,class std::allocator >,class std::basic_string,class std::allocator >,struct std::hash,class std::allocator > >,struct std::equal_to,class std::allocator > >,class std::allocator,class std::allocator > const ,class std::basic_string,class std::allocator > > > > *)" (?ConfigureOptions@Configurable@rocksdb@@MEAA?AVStatus@2@AEBUConfigOptions@2@AEBV?$unordered_map@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@V12@U?$hash@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@U?$equal_to@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@V?$allocator@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@V12@@std@@@2@@std@@PEAV56@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ParseStringOptions(struct rocksdb::ConfigOptions const &,class std::basic_string,class std::allocator > const &)" (?ParseStringOptions@Configurable@rocksdb@@MEAA?AVStatus@2@AEBUConfigOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual void const * __cdecl rocksdb::Configurable::GetOptionsPtr(class std::basic_string,class std::allocator > const &)const " (?GetOptionsPtr@Configurable@rocksdb@@MEBAPEBXAEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Configurable::ValidateOptions(struct rocksdb::DBOptions const &,struct rocksdb::ColumnFamilyOptions const &)const " (?ValidateOptions@Configurable@rocksdb@@UEBA?AVStatus@2@AEBUDBOptions@2@AEBUColumnFamilyOptions@2@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Configurable::PrepareOptions(struct rocksdb::ConfigOptions const &)" (?PrepareOptions@Configurable@rocksdb@@UEAA?AVStatus@2@AEBUConfigOptions@2@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::Configurable::AreEquivalent(struct rocksdb::ConfigOptions const &,class rocksdb::Configurable const *,class std::basic_string,class std::allocator > *)const " (?AreEquivalent@Configurable@rocksdb@@UEBA_NAEBUConfigOptions@2@PEBV12@PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Configurable::GetOption(struct rocksdb::ConfigOptions const &,class std::basic_string,class std::allocator > const &,class std::basic_string,class std::allocator > *)const " (?GetOption@Configurable@rocksdb@@UEBA?AVStatus@2@AEBUConfigOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV56@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "class rocksdb::TableFactory * __cdecl rocksdb::NewBlockBasedTableFactory(struct rocksdb::BlockBasedTableOptions const &)" (?NewBlockBasedTableFactory@rocksdb@@YAPEAVTableFactory@1@AEBUBlockBasedTableOptions@1@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: class std::shared_ptr __cdecl rocksdb::LRUCacheOptions::MakeSharedCache(void)const " (?MakeSharedCache@LRUCacheOptions@rocksdb@@QEBA?AV?$shared_ptr@VCache@rocksdb@@@std@@XZ) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: static class rocksdb::Status __cdecl rocksdb::DB::OpenForReadOnly(struct rocksdb::Options const &,class std::basic_string,class std::allocator > const &,class std::unique_ptr > *,bool)" (?OpenForReadOnly@DB@rocksdb@@SA?AVStatus@2@AEBUOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV?$unique_ptr@VDB@rocksdb@@U?$default_delete@VDB@rocksdb@@@std@@@6@_N@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: static class rocksdb::Status __cdecl rocksdb::DB::Open(struct rocksdb::Options const &,class std::basic_string,class std::allocator > const &,class std::unique_ptr > *)" (?Open@DB@rocksdb@@SA?AVStatus@2@AEBUOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV?$unique_ptr@VDB@rocksdb@@U?$default_delete@VDB@rocksdb@@@std@@@6@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "class std::vector > const & __cdecl rocksdb::GetSupportedCompressions(void)" (?GetSupportedCompressions@rocksdb@@YAAEBV?$vector@W4CompressionType@rocksdb@@V?$allocator@W4CompressionType@rocksdb@@@std@@@std@@XZ) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::MergeOperator::PartialMergeMulti(class rocksdb::Slice const &,class std::deque > const &,class std::basic_string,class std::allocator > *,class rocksdb::Logger *)const " (?PartialMergeMulti@MergeOperator@rocksdb@@UEBA_NAEBVSlice@2@AEBV?$deque@VSlice@rocksdb@@V?$allocator@VSlice@rocksdb@@@std@@@std@@PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@5@PEAVLogger@2@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::MergeOperator::FullMergeV3(struct rocksdb::MergeOperator::MergeOperationInputV3 const &,struct rocksdb::MergeOperator::MergeOperationOutputV3 *)const " (?FullMergeV3@MergeOperator@rocksdb@@UEBA_NAEBUMergeOperationInputV3@12@PEAUMergeOperationOutputV3@12@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::MergeOperator::FullMergeV2(struct rocksdb::MergeOperator::MergeOperationInput const &,struct rocksdb::MergeOperator::MergeOperationOutput *)const " (?FullMergeV2@MergeOperator@rocksdb@@UEBA_NAEBUMergeOperationInput@12@PEAUMergeOperationOutput@12@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string,class std::allocator > __cdecl rocksdb::Customizable::SerializeOptions(struct rocksdb::ConfigOptions const &,class std::basic_string,class std::allocator > const &)const " (?SerializeOptions@Customizable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBUConfigOptions@2@AEBV34@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "protected: virtual class std::basic_string,class std::allocator > __cdecl rocksdb::Customizable::GetOptionName(class std::basic_string,class std::allocator > const &)const " (?GetOptionName@Customizable@rocksdb@@MEBA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBV34@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual class rocksdb::Status __cdecl rocksdb::Customizable::GetOption(struct rocksdb::ConfigOptions const &,class std::basic_string,class std::allocator > const &,class std::basic_string,class std::allocator > *)const " (?GetOption@Customizable@rocksdb@@UEBA?AVStatus@2@AEBUConfigOptions@2@AEBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAV56@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: virtual bool __cdecl rocksdb::Customizable::AreEquivalent(struct rocksdb::ConfigOptions const &,class rocksdb::Configurable const *,class std::basic_string,class std::allocator > *)const " (?AreEquivalent@Customizable@rocksdb@@UEBA_NAEBUConfigOptions@2@PEBVConfigurable@2@PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "public: __cdecl rocksdb::DBOptions::DBOptions(void)" (??0DBOptions@rocksdb@@QEAA@XZ) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "private: virtual bool __cdecl rocksdb::AssociativeMergeOperator::PartialMerge(class rocksdb::Slice const &,class rocksdb::Slice const &,class rocksdb::Slice const &,class std::basic_string,class std::allocator > *,class rocksdb::Logger *)const " (?PartialMerge@AssociativeMergeOperator@rocksdb@@EEBA_NAEBVSlice@2@00PEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEAVLogger@2@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "private: virtual bool __cdecl rocksdb::AssociativeMergeOperator::FullMergeV2(struct rocksdb::MergeOperator::MergeOperationInput const &,struct rocksdb::MergeOperator::MergeOperationOutput *)const " (?FullMergeV2@AssociativeMergeOperator@rocksdb@@EEBA_NAEBUMergeOperationInput@MergeOperator@2@PEAUMergeOperationOutput@42@@Z) IfcParse.lib(IfcFile.cpp.obj) : error LNK2001: unresolved external symbol "bool const rocksdb::kDefaultToAdaptiveMutex" (?kDefaultToAdaptiveMutex@rocksdb@@3_NB) ifcwrap\_ifcopenshell_wrapper.cp311-win_amd64.pyd : fatal error LNK1120: 34 unresolved externals Or on Unix: /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcEntityInstanceData.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string, std::allocator >*)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcEntityInstanceData.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::Configurable::~Configurable()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/configurable.h:59: undefined reference to `vtable for rocksdb::Configurable' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/configurable.h:59: undefined reference to `vtable for rocksdb::Configurable' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/configurable.h:59: undefined reference to `vtable for rocksdb::Configurable' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::Customizable::GetOptionsPtr(std::__cxx11::basic_string, std::allocator > const&) const': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/customizable.h:105: undefined reference to `rocksdb::Configurable::GetOptionsPtr(std::__cxx11::basic_string, std::allocator > const&) const' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/customizable.h:105: undefined reference to `rocksdb::Configurable::GetOptionsPtr(std::__cxx11::basic_string, std::allocator > const&) const' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/customizable.h:105: undefined reference to `rocksdb::Configurable::GetOptionsPtr(std::__cxx11::basic_string, std::allocator > const&) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::Options::Options()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/options.h:1628: undefined reference to `rocksdb::DBOptions::DBOptions()' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/options.h:1628: undefined reference to `rocksdb::ColumnFamilyOptions::ColumnFamilyOptions()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `IfcParse::impl::rocks_db_file_storage::rocks_db_file_storage(std::__cxx11::basic_string, std::allocator > const&, IfcParse::IfcFile*, bool)': /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:421: undefined reference to `rocksdb::GetSupportedCompressions()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `init_db': /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:440: undefined reference to `rocksdb::kDefaultToAdaptiveMutex' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::NewLRUCache(unsigned long, int, bool, double, std::shared_ptr, bool, rocksdb::CacheMetadataChargePolicy, double)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/cache.h:282: undefined reference to `rocksdb::LRUCacheOptions::MakeSharedCache() const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `init_db': /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:445: undefined reference to `rocksdb::NewBlockBasedTableFactory(rocksdb::BlockBasedTableOptions const&)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::DB::OpenForReadOnly(rocksdb::Options const&, std::__cxx11::basic_string, std::allocator > const&, rocksdb::DB**, bool)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/db.h:243: undefined reference to `rocksdb::DB::OpenForReadOnly(rocksdb::Options const&, std::__cxx11::basic_string, std::allocator > const&, std::unique_ptr >*, bool)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::DB::Open(rocksdb::Options const&, std::__cxx11::basic_string, std::allocator > const&, rocksdb::DB**)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/db.h:187: undefined reference to `rocksdb::DB::Open(rocksdb::Options const&, std::__cxx11::basic_string, std::allocator > const&, std::unique_ptr >*)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb_set_view::iterator::extract_current_value() const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:70: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string, std::allocator >*)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb_set_view::iterator::iterator(rocksdb_set_view::iterator const&)': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:103: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:106: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `IfcParse::impl::rocks_db_file_storage::process_deletion_inverse(IfcUtil::IfcBaseClass*)': /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:526: undefined reference to `rocksdb::WriteBatch::~WriteBatch()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::WriteBatch::WriteBatch(unsigned long, unsigned long)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/write_batch.h:67: undefined reference to `rocksdb::WriteBatch::WriteBatch(unsigned long, unsigned long, unsigned long, unsigned long)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `rocksdb::WriteBatch::DeleteRange(rocksdb::Slice const&, rocksdb::Slice const&)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/write_batch.h:164: undefined reference to `rocksdb::WriteBatch::DeleteRange(rocksdb::ColumnFamilyHandle*, rocksdb::Slice const&, rocksdb::Slice const&)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o): in function `IfcParse::impl::rocks_db_file_storage::process_deletion_inverse(IfcUtil::IfcBaseClass*)': /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:526: undefined reference to `rocksdb::WriteBatch::~WriteBatch()' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:547: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/IfcFile.cpp:526: undefined reference to `rocksdb::WriteBatch::~WriteBatch()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTIN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x10): undefined reference to `typeinfo for rocksdb::AssociativeMergeOperator' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x20): undefined reference to `rocksdb::Customizable::GetOption(rocksdb::ConfigOptions const&, std::__cxx11::basic_string, std::allocator > const&, std::__cxx11::basic_string, std::allocator >*) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x28): undefined reference to `rocksdb::Customizable::AreEquivalent(rocksdb::ConfigOptions const&, rocksdb::Configurable const*, std::__cxx11::basic_string, std::allocator >*) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x38): undefined reference to `rocksdb::Configurable::PrepareOptions(rocksdb::ConfigOptions const&)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x40): undefined reference to `rocksdb::Configurable::ValidateOptions(rocksdb::DBOptions const&, rocksdb::ColumnFamilyOptions const&) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x50): undefined reference to `rocksdb::Configurable::ParseStringOptions(rocksdb::ConfigOptions const&, std::__cxx11::basic_string, std::allocator > const&)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x58): undefined reference to `rocksdb::Configurable::ConfigureOptions(rocksdb::ConfigOptions const&, std::unordered_map, std::allocator >, std::__cxx11::basic_string, std::allocator >, std::hash, std::allocator > >, std::equal_to, std::allocator > >, std::allocator, std::allocator > const, std::__cxx11::basic_string, std::allocator > > > > const&, std::unordered_map, std::allocator >, std::__cxx11::basic_string, std::allocator >, std::hash, std::allocator > >, std::equal_to, std::allocator > >, std::allocator, std::allocator > const, std::__cxx11::basic_string, std::allocator > > > >*)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x60): undefined reference to `rocksdb::Configurable::ParseOption(rocksdb::ConfigOptions const&, rocksdb::OptionTypeInfo const&, std::__cxx11::basic_string, std::allocator > const&, std::__cxx11::basic_string, std::allocator > const&, void*)' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x68): undefined reference to `rocksdb::Configurable::OptionsAreEqual(rocksdb::ConfigOptions const&, rocksdb::OptionTypeInfo const&, std::__cxx11::basic_string, std::allocator > const&, void const*, void const*, std::__cxx11::basic_string, std::allocator >*) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x70): undefined reference to `rocksdb::Customizable::SerializeOptions(rocksdb::ConfigOptions const&, std::__cxx11::basic_string, std::allocator > const&) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0x78): undefined reference to `rocksdb::Customizable::GetOptionName(std::__cxx11::basic_string, std::allocator > const&) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0xb8): undefined reference to `rocksdb::MergeOperator::FullMergeV3(rocksdb::MergeOperator::MergeOperationInputV3 const&, rocksdb::MergeOperator::MergeOperationOutputV3*) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0xc0): undefined reference to `rocksdb::AssociativeMergeOperator::PartialMerge(rocksdb::Slice const&, rocksdb::Slice const&, rocksdb::Slice const&, std::__cxx11::basic_string, std::allocator >*, rocksdb::Logger*) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcFile.cpp.o):(.data.rel.ro._ZTVN12_GLOBAL__N_126ConcatenateIdMergeOperatorE+0xc8): undefined reference to `rocksdb::MergeOperator::PartialMergeMulti(rocksdb::Slice const&, std::deque > const&, std::__cxx11::basic_string, std::allocator >*, rocksdb::Logger*) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::allocator >, unsigned long, DefaultCodec >::iterator::operator*() const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:261: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:263: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::allocator >, unsigned long, DefaultCodec >::iterator::operator==(rocksdb_map_adapter, std::allocator >, unsigned long, DefaultCodec >::iterator const&) const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:296: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:296: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string, std::allocator >*)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string, std::allocator >*)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::allocator >, DefaultCodec, std::allocator > > >::iterator::operator*() const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:261: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:263: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::allocator >, DefaultCodec, std::allocator > > >::find(unsigned long const&) const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:327: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::vector >, DefaultCodec > > >::find(std::tuple const&) const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:327: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::vector >, DefaultCodec > > >::iterator::operator*() const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:261: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o):/home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:263: more undefined references to `rocksdb::Slice::ToString[abi:cxx11](bool) const' follow /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string, std::allocator >*)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::allocator >, unsigned long, DefaultCodec >::find(std::__cxx11::basic_string, std::allocator > const&) const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:327: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_map_adapter, std::allocator >, unsigned long, DefaultCodec >::iterator::iterator(rocksdb_map_adapter, std::allocator >, unsigned long, DefaultCodec >::iterator const&)': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:230: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_map_adapter.h:233: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::PinnableSlice(std::__cxx11::basic_string, std::allocator >*)': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:141: undefined reference to `rocksdb::Cleanable::Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb::PinnableSlice::~PinnableSlice()': /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: /home/andrej/ifcopenshell/build/Linux/x86_64/install/rocksdb-9.11.2/include/rocksdb/slice.h:138: undefined reference to `rocksdb::Cleanable::~Cleanable()' /usr/bin/ld: ../ifcparse/libIfcParse.a(IfcParse.cpp.o): in function `rocksdb_set_view::iterator::operator==(rocksdb_set_view::iterator const&) const': /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:170: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' /usr/bin/ld: /home/andrej/ifcopenshell/src/ifcparse/rocksdb_set_view.h:170: undefined reference to `rocksdb::Slice::ToString[abi:cxx11](bool) const' collect2: error: ld returned 1 exit status make[2]: *** [ifcconvert/CMakeFiles/IfcConvert.dir/build.make:236: ifcconvert/IfcConvert] Error 1 make[1]: *** [CMakeFiles/Makefile2:569: ifcconvert/CMakeFiles/IfcConvert.dir/all] Error 2 --- cmake/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 9e88026ac7..51ab6c22e1 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -258,10 +258,10 @@ if(WITH_ROCKSDB) set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB") target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB) set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB) - target_link_libraries( - IFCOPENSHELL_RocksDB - INTERFACE $,RocksDB::rocksdb-shared,RocksDB::rocksdb> - ) + # Shared binaries for `rocksdb` only support limited API (only `c.h`), but we use `db.h` API. + # So rocksdb supported only as a static library. + # See https://github.com/facebook/rocksdb/issues/981. + target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb) if(WITH_ZSTD) # @todo do we actually need the zstd include dir or rather just pass From e54d16ef57ea237e3cac931c507492a239c11400 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 18 Feb 2026 13:52:57 +0500 Subject: [PATCH 084/131] build_osx - ensure we use `bison` from `brew` instead of the default one --- .github/workflows/build_osx.yml | 2 ++ nix/build-all.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index 056cf13763..eb680e54c1 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -40,6 +40,8 @@ jobs: # preinstalled: xz, cmake brew install git bison autoconf automake libffi findutils echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH + # Mac is using bison 2.5 by default, but we need 3.5+ for swig. + echo "$(brew --prefix bison)/bin" >> $GITHUB_PATH - name: Install aws cli run: | diff --git a/nix/build-all.py b/nix/build-all.py index ab9a568dba..22f3a73108 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -91,6 +91,9 @@ Used environment variables: # # # on OS X El Capitan with homebrew: # # $ brew install git bison autoconf automake libffi cmake # +# $ # `bison` shipped with Mac is too old for swig build, # +# $ # so we use `brew`. # +# $ export PATH=$(brew --prefix bison)/bin:$PATH # # # # on RHEL-related distros: # # $ dnf install git gcc gcc-c++ autoconf bison make cmake # From a61d5a12fb485e2d9b8b87a8117b4e0a20811424 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 18 Feb 2026 14:02:31 +0500 Subject: [PATCH 085/131] build-all - use cmake to build swig To keep it in sync with Windows build. Also Removed pcre2 dependency as apparently it's not required - we were not using it on Windows. --- nix/build-all.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index 22f3a73108..d80f529414 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -150,7 +150,6 @@ OCCT_VERSION = "7.8.1" BOOST_VERSION = "1.86.0" EIGEN_VERSION = "3.4.0" PCRE_VERSION = "8.41" -PCRE2_VERSION = "10.32" LIBXML2_VERSION = "2.13.8" SWIG_VERSION = "4.2.1" OPENCOLLADA_VERSION = "v1.6.68" @@ -349,13 +348,12 @@ dependency_tree: "dict[str, tuple[str, ...]]" = { "OpenCOLLADA": ("libxml2", "pcre"), "IfcGeomServer": ("IfcGeom",), "IfcOpenShell-Python": ("python", "swig", "IfcGeom"), - "swig": ("pcre2",), + "swig": (), "boost": (), "libxml2": (), "python": (), "occ": (), "pcre": (), - "pcre2": (), "json": (), "hdf5": (), "cgal": (), @@ -422,7 +420,6 @@ if WASM: "opencollada", "swig", "pcre", - "pcre2", "IfcGeom", "IfcConvert", "IfcGeomServer", @@ -551,14 +548,14 @@ BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BO # Helper functions -def run_autoconf(arg1: str, configure_args: "list[str]", cwd: str) -> None: +def run_autoconf(dependency_name: str, configure_args: "list[str]", cwd: str) -> None: configure_path = os.path.realpath(os.path.join(cwd, "..", "configure")) if not os.path.exists(configure_path): run( [bash, "./autogen.sh"], cwd=os.path.realpath(os.path.join(cwd, "..")) ) # only run autogen.sh in the directory it is located and use cwd to achieve that in order to not mess up things # Using `sh` over `bash` fixes issues with building swig - prefix = os.path.realpath(f"{DEPS_DIR}/install/{arg1}") + prefix = os.path.realpath(f"{DEPS_DIR}/install/{dependency_name}") wasm = [] if "wasm" in flags: @@ -937,20 +934,15 @@ if "pcre" in targets: restore_env("CC", OLD_CC) restore_env("CXX", OLD_CXX) -if "pcre2" in targets: - build_dependency( - name=f"pcre2-{PCRE2_VERSION}", - mode="autoconf", - build_tool_args=[DISABLE_FLAG], - download_url=f"https://downloads.sourceforge.net/project/pcre/pcre2/{PCRE2_VERSION}/", - download_name=f"pcre2-{PCRE2_VERSION}.tar.bz2", - ) - if "swig" in targets: + dependency_name = f"swig-{SWIG_VERSION}" build_dependency( - name=f"swig-{SWIG_VERSION}", - mode="autoconf", - build_tool_args=["--disable-ccache", f"--with-pcre2-prefix={DEPS_DIR}/install/pcre2-{PCRE2_VERSION}"], + name=dependency_name, + mode="cmake", + build_tool_args=[ + "-DWITH_PCRE=OFF", + f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}", + ], download_url="https://github.com/swig/swig.git", download_name="swig", download_tool=download_tool_git, From fb1c9eb7e3704eaecaee5ec92ca9f23350903ad1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 19 Feb 2026 15:50:40 +0500 Subject: [PATCH 086/131] build-all - fix missing f-string --- 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 d80f529414..7b1df4b514 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -253,7 +253,7 @@ if WASM: # https://github.com/pyodide/pyodide-build/issues/251 side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "") if side_module_cxx_flags.strip(): - print("SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').") + print(f"SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').") print("Maybe it's time to stop overriding them in the script?") os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"] From 526b9537a947cb385f64be70d2d5a731fc3d71fd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 19 Feb 2026 15:51:01 +0500 Subject: [PATCH 087/131] build_pyodide.sh - allow executing multiple times --- pyodide/build_pyodide.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyodide/build_pyodide.sh b/pyodide/build_pyodide.sh index 2994a44ae6..20ad946162 100755 --- a/pyodide/build_pyodide.sh +++ b/pyodide/build_pyodide.sh @@ -1,9 +1,12 @@ #!/usr/bin/bash set -ex +# Script is assuming that it will be possible to execute it multiple times +# therefore we're clearing venv each time and ignoring existing 'emsdk' folder. + # Install uv. curl -LsSf https://astral.sh/uv/install.sh | sh -uv venv --python 3.13 +uv venv --python 3.13 --clear source .venv/bin/activate # Install pyodide cross build environment. @@ -13,7 +16,9 @@ uv pip install pyodide-build uv run pyodide xbuildenv install # Emscripten doesn't come with xbuildenv. -git clone https://github.com/emscripten-core/emsdk +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} From 34ffaea2c9145f1705782e48816615ad90309f6a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 19 Feb 2026 16:17:45 +0500 Subject: [PATCH 088/131] cmake - link serializers against IfcGeom to fix wasm build jsonserializer is using ifcgeom and also eigen3 --- src/serializers/schema_dependent/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serializers/schema_dependent/CMakeLists.txt b/src/serializers/schema_dependent/CMakeLists.txt index ef877342e9..73c1e5d3e7 100644 --- a/src/serializers/schema_dependent/CMakeLists.txt +++ b/src/serializers/schema_dependent/CMakeLists.txt @@ -9,9 +9,9 @@ foreach(schema ${SCHEMA_VERSIONS}) Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}" ) - target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES} ${GLTF_LIBRARIES}) + target_link_libraries(Serializers_ifc${schema} IfcGeom ${HDF5_LIBRARIES} ${GLTF_LIBRARIES}) if(NOT WASM_BUILD) - target_link_libraries(Serializers_ifc${schema} IfcGeom ${OpenCASCADE_LIBRARIES}) + target_link_libraries(Serializers_ifc${schema} ${OpenCASCADE_LIBRARIES}) endif() endforeach() From ff3933a11763a9ec3004187f10866a97bc363d02 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Feb 2026 12:20:06 +0500 Subject: [PATCH 089/131] Remove some unused imports --- src/bonsai/bonsai/__init__.py | 1 - src/bonsai/bonsai/bim/export_ifc.py | 7 ------- src/bonsai/bonsai/bim/helper.py | 1 - src/bonsai/bonsai/bim/import_ifc.py | 1 - .../bonsai/bim/module/aggregate/decorator.py | 2 -- .../bonsai/bim/module/aggregate/operator.py | 2 -- src/bonsai/bonsai/bim/module/aggregate/prop.py | 7 ------- src/bonsai/bonsai/bim/module/aggregate/ui.py | 1 - .../bonsai/bim/module/alignment/operator.py | 10 ---------- src/bonsai/bonsai/bim/module/attribute/prop.py | 6 +----- src/bonsai/bonsai/bim/module/bcf/bcfstore.py | 2 -- src/bonsai/bonsai/bim/module/bcf/operator.py | 9 ++------- src/bonsai/bonsai/bim/module/bcf/prop.py | 2 -- src/bonsai/bonsai/bim/module/bcf/ui.py | 1 - .../bonsai/bim/module/boundary/decorator.py | 1 - .../bonsai/bim/module/boundary/operator.py | 3 --- src/bonsai/bonsai/bim/module/boundary/prop.py | 6 ------ src/bonsai/bonsai/bim/module/boundary/ui.py | 3 +-- src/bonsai/bonsai/bim/module/brick/operator.py | 1 - src/bonsai/bonsai/bim/module/brick/prop.py | 5 +---- src/bonsai/bonsai/bim/module/bsdd/data.py | 5 ----- src/bonsai/bonsai/bim/module/bsdd/operator.py | 1 - src/bonsai/bonsai/bim/module/bsdd/prop.py | 5 +---- src/bonsai/bonsai/bim/module/bsdd/ui.py | 1 - src/bonsai/bonsai/bim/module/cad/operator.py | 4 +--- src/bonsai/bonsai/bim/module/cad/prop.py | 2 -- src/bonsai/bonsai/bim/module/cad/workspace.py | 4 +--- src/bonsai/bonsai/bim/module/clash/data.py | 3 --- src/bonsai/bonsai/bim/module/clash/decorator.py | 1 - src/bonsai/bonsai/bim/module/clash/operator.py | 3 --- src/bonsai/bonsai/bim/module/clash/prop.py | 3 +-- .../bim/module/classification/operator.py | 1 - .../bonsai/bim/module/classification/prop.py | 3 --- .../bonsai/bim/module/classification/ui.py | 1 - .../bonsai/bim/module/constraint/operator.py | 1 - src/bonsai/bonsai/bim/module/constraint/prop.py | 6 ------ src/bonsai/bonsai/bim/module/context/data.py | 1 - src/bonsai/bonsai/bim/module/context/prop.py | 8 +------- src/bonsai/bonsai/bim/module/cost/data.py | 2 -- src/bonsai/bonsai/bim/module/cost/operator.py | 4 ++-- src/bonsai/bonsai/bim/module/cost/prop.py | 3 --- .../bonsai/bim/module/covering/workspace.py | 1 - .../bonsai/bim/module/covetool/operator.py | 1 - src/bonsai/bonsai/bim/module/csv/operator.py | 4 ---- src/bonsai/bonsai/bim/module/csv/prop.py | 6 +----- src/bonsai/bonsai/bim/module/debug/operator.py | 1 - src/bonsai/bonsai/bim/module/debug/prop.py | 4 ---- src/bonsai/bonsai/bim/module/demo/prop.py | 8 -------- src/bonsai/bonsai/bim/module/diff/prop.py | 6 +----- src/bonsai/bonsai/bim/module/document/data.py | 2 -- .../bonsai/bim/module/document/operator.py | 3 +-- src/bonsai/bonsai/bim/module/document/prop.py | 5 +---- src/bonsai/bonsai/bim/module/document/ui.py | 1 - .../bonsai/bim/module/drawing/annotation.py | 5 +---- src/bonsai/bonsai/bim/module/drawing/data.py | 2 -- .../bonsai/bim/module/drawing/decoration.py | 3 +-- src/bonsai/bonsai/bim/module/drawing/helper.py | 2 -- .../bonsai/bim/module/drawing/operator.py | 7 +------ src/bonsai/bonsai/bim/module/drawing/prop.py | 10 ++-------- .../bonsai/bim/module/drawing/scheduler.py | 2 -- src/bonsai/bonsai/bim/module/drawing/sheeter.py | 1 - .../bonsai/bim/module/drawing/svgwriter.py | 3 --- src/bonsai/bonsai/bim/module/drawing/ui.py | 1 - src/bonsai/bonsai/bim/module/fm/data.py | 2 -- src/bonsai/bonsai/bim/module/fm/operator.py | 3 --- src/bonsai/bonsai/bim/module/fm/prop.py | 4 ---- .../bonsai/bim/module/geometry/__init__.py | 2 -- .../bonsai/bim/module/geometry/decorator.py | 1 - src/bonsai/bonsai/bim/module/geometry/helper.py | 7 +------ .../bonsai/bim/module/geometry/operator.py | 5 ----- src/bonsai/bonsai/bim/module/geometry/prop.py | 3 --- .../bonsai/bim/module/georeference/data.py | 1 - .../bonsai/bim/module/georeference/decorator.py | 3 --- .../bonsai/bim/module/georeference/operator.py | 1 - .../bonsai/bim/module/georeference/prop.py | 4 ---- src/bonsai/bonsai/bim/module/group/data.py | 3 --- src/bonsai/bonsai/bim/module/group/operator.py | 3 +-- src/bonsai/bonsai/bim/module/group/prop.py | 6 +----- src/bonsai/bonsai/bim/module/ifcgit/data.py | 2 -- src/bonsai/bonsai/bim/module/layer/data.py | 1 - src/bonsai/bonsai/bim/module/layer/operator.py | 1 - src/bonsai/bonsai/bim/module/layer/prop.py | 5 +---- .../bonsai/bim/module/library/operator.py | 1 - src/bonsai/bonsai/bim/module/library/prop.py | 6 +----- src/bonsai/bonsai/bim/module/library/ui.py | 2 +- src/bonsai/bonsai/bim/module/light/__init__.py | 2 -- src/bonsai/bonsai/bim/module/light/data.py | 2 -- src/bonsai/bonsai/bim/module/light/decorator.py | 2 -- src/bonsai/bonsai/bim/module/material/data.py | 2 -- .../bonsai/bim/module/material/operator.py | 1 - src/bonsai/bonsai/bim/module/material/prop.py | 6 +----- src/bonsai/bonsai/bim/module/material/ui.py | 1 - src/bonsai/bonsai/bim/module/misc/operator.py | 3 +-- src/bonsai/bonsai/bim/module/misc/prop.py | 9 --------- src/bonsai/bonsai/bim/module/model/array.py | 4 +--- src/bonsai/bonsai/bim/module/model/covering.py | 2 -- src/bonsai/bonsai/bim/module/model/data.py | 8 ++++---- src/bonsai/bonsai/bim/module/model/decorator.py | 3 +-- src/bonsai/bonsai/bim/module/model/door.py | 5 +---- src/bonsai/bonsai/bim/module/model/grid.py | 1 - src/bonsai/bonsai/bim/module/model/handler.py | 4 +--- src/bonsai/bonsai/bim/module/model/mep.py | 9 +-------- src/bonsai/bonsai/bim/module/model/opening.py | 14 +++----------- src/bonsai/bonsai/bim/module/model/polyline.py | 17 +---------------- src/bonsai/bonsai/bim/module/model/product.py | 5 +---- src/bonsai/bonsai/bim/module/model/profile.py | 3 --- src/bonsai/bonsai/bim/module/model/prop.py | 1 - src/bonsai/bonsai/bim/module/model/railing.py | 1 - src/bonsai/bonsai/bim/module/model/roof.py | 4 +--- src/bonsai/bonsai/bim/module/model/slab.py | 6 +----- src/bonsai/bonsai/bim/module/model/stair.py | 3 +-- .../bim/module/model/sverchok_modifier.py | 1 - src/bonsai/bonsai/bim/module/model/task.py | 1 - src/bonsai/bonsai/bim/module/model/ui.py | 3 +-- src/bonsai/bonsai/bim/module/model/wall.py | 7 ++----- src/bonsai/bonsai/bim/module/model/window.py | 5 +---- src/bonsai/bonsai/bim/module/model/workspace.py | 4 +--- src/bonsai/bonsai/bim/module/nest/decorator.py | 2 -- src/bonsai/bonsai/bim/module/nest/operator.py | 1 - src/bonsai/bonsai/bim/module/nest/prop.py | 7 ------- src/bonsai/bonsai/bim/module/owner/prop.py | 5 ----- src/bonsai/bonsai/bim/module/patch/operator.py | 1 - src/bonsai/bonsai/bim/module/patch/prop.py | 7 +------ src/bonsai/bonsai/bim/module/profile/data.py | 1 - .../bonsai/bim/module/profile/operator.py | 1 - src/bonsai/bonsai/bim/module/profile/prop.py | 7 +------ src/bonsai/bonsai/bim/module/project/data.py | 2 -- .../bonsai/bim/module/project/decorator.py | 2 -- src/bonsai/bonsai/bim/module/project/gizmo.py | 1 - .../bonsai/bim/module/project/operator.py | 7 +------ src/bonsai/bonsai/bim/module/project/prop.py | 4 +--- src/bonsai/bonsai/bim/module/project/ui.py | 2 -- .../bonsai/bim/module/project/workspace.py | 1 - src/bonsai/bonsai/bim/module/pset/operator.py | 2 +- src/bonsai/bonsai/bim/module/pset/prop.py | 4 +--- .../bonsai/bim/module/pset_template/data.py | 3 --- .../bonsai/bim/module/pset_template/operator.py | 1 - .../bonsai/bim/module/pset_template/prop.py | 4 ---- .../bonsai/bim/module/pset_template/ui.py | 1 - src/bonsai/bonsai/bim/module/qto/operator.py | 1 - src/bonsai/bonsai/bim/module/qto/prop.py | 6 ------ src/bonsai/bonsai/bim/module/resource/prop.py | 3 --- src/bonsai/bonsai/bim/module/root/data.py | 1 - src/bonsai/bonsai/bim/module/root/operator.py | 2 -- src/bonsai/bonsai/bim/module/root/prop.py | 7 ------- src/bonsai/bonsai/bim/module/root/ui.py | 1 - src/bonsai/bonsai/bim/module/search/operator.py | 6 +----- src/bonsai/bonsai/bim/module/search/prop.py | 4 ---- src/bonsai/bonsai/bim/module/sequence/data.py | 1 - src/bonsai/bonsai/bim/module/sequence/helper.py | 1 - src/bonsai/bonsai/bim/module/sequence/prop.py | 6 +----- src/bonsai/bonsai/bim/module/sequence/ui.py | 1 - .../bonsai/bim/module/spatial/decorator.py | 1 - src/bonsai/bonsai/bim/module/spatial/prop.py | 4 ---- .../bonsai/bim/module/structural/operator.py | 5 +---- src/bonsai/bonsai/bim/module/structural/prop.py | 4 +--- src/bonsai/bonsai/bim/module/style/prop.py | 1 - src/bonsai/bonsai/bim/module/system/data.py | 1 - .../bonsai/bim/module/system/decorator.py | 4 ---- src/bonsai/bonsai/bim/module/system/operator.py | 1 - src/bonsai/bonsai/bim/module/system/prop.py | 5 +---- src/bonsai/bonsai/bim/module/tester/data.py | 1 - src/bonsai/bonsai/bim/module/tester/operator.py | 1 - src/bonsai/bonsai/bim/module/tester/prop.py | 5 +---- src/bonsai/bonsai/bim/module/type/operator.py | 5 ----- src/bonsai/bonsai/bim/module/type/prop.py | 5 ----- src/bonsai/bonsai/bim/module/unit/data.py | 1 - src/bonsai/bonsai/bim/module/unit/operator.py | 2 -- src/bonsai/bonsai/bim/module/unit/prop.py | 5 +---- src/bonsai/bonsai/bim/module/void/data.py | 1 - src/bonsai/bonsai/bim/module/web/data.py | 2 -- src/bonsai/bonsai/bim/module/web/operator.py | 1 - src/bonsai/bonsai/bim/module/web/prop.py | 7 ------- src/bonsai/bonsai/bim/prop.py | 10 ---------- src/bonsai/bonsai/bim/ui.py | 5 +---- src/bonsai/bonsai/core/attribute.py | 2 -- src/bonsai/bonsai/core/brick.py | 3 +-- src/bonsai/bonsai/core/bsdd.py | 5 +---- src/bonsai/bonsai/core/context.py | 1 - src/bonsai/bonsai/core/cost.py | 1 - src/bonsai/bonsai/core/covering.py | 4 +--- src/bonsai/bonsai/core/debug.py | 4 +--- src/bonsai/bonsai/core/document.py | 2 +- src/bonsai/bonsai/core/drawing.py | 2 +- src/bonsai/bonsai/core/georeference.py | 4 +--- src/bonsai/bonsai/core/ifcgit.py | 3 +-- src/bonsai/bonsai/core/library.py | 2 +- src/bonsai/bonsai/core/misc.py | 3 +-- src/bonsai/bonsai/core/model.py | 1 - src/bonsai/bonsai/core/nest.py | 2 +- src/bonsai/bonsai/core/owner.py | 3 +-- src/bonsai/bonsai/core/patch.py | 4 +--- src/bonsai/bonsai/core/profile.py | 4 +--- src/bonsai/bonsai/core/project.py | 2 -- src/bonsai/bonsai/core/pset.py | 2 +- src/bonsai/bonsai/core/resource.py | 1 - src/bonsai/bonsai/core/search.py | 1 - src/bonsai/bonsai/core/sequence.py | 1 - src/bonsai/bonsai/core/spatial.py | 2 +- src/bonsai/bonsai/core/structural.py | 3 +-- src/bonsai/bonsai/core/style.py | 2 +- src/bonsai/bonsai/core/system.py | 3 +-- src/bonsai/bonsai/core/type.py | 5 +---- src/bonsai/bonsai/core/unit.py | 3 +-- src/bonsai/bonsai/core/web.py | 4 +--- src/bonsai/bonsai/tool/__init__.py | 3 +++ src/bonsai/bonsai/tool/attribute.py | 4 ---- src/bonsai/bonsai/tool/bcf.py | 1 - src/bonsai/bonsai/tool/blender.py | 2 +- src/bonsai/bonsai/tool/brick.py | 2 -- src/bonsai/bonsai/tool/cad.py | 1 - src/bonsai/bonsai/tool/clash.py | 5 +---- src/bonsai/bonsai/tool/classification.py | 2 -- src/bonsai/bonsai/tool/collector.py | 1 - src/bonsai/bonsai/tool/covering.py | 1 - src/bonsai/bonsai/tool/drawing.py | 2 -- src/bonsai/bonsai/tool/feature.py | 2 -- src/bonsai/bonsai/tool/geometry.py | 3 +-- src/bonsai/bonsai/tool/georeference.py | 1 - src/bonsai/bonsai/tool/group.py | 1 - src/bonsai/bonsai/tool/layer.py | 1 - src/bonsai/bonsai/tool/material.py | 1 - src/bonsai/bonsai/tool/model.py | 2 -- src/bonsai/bonsai/tool/owner.py | 2 +- src/bonsai/bonsai/tool/polyline.py | 4 +--- src/bonsai/bonsai/tool/profile.py | 3 --- src/bonsai/bonsai/tool/project.py | 2 -- src/bonsai/bonsai/tool/pset.py | 1 - src/bonsai/bonsai/tool/pset_template.py | 2 -- src/bonsai/bonsai/tool/qto.py | 1 - src/bonsai/bonsai/tool/raycast.py | 2 -- src/bonsai/bonsai/tool/root.py | 1 - src/bonsai/bonsai/tool/sequence.py | 5 ----- src/bonsai/bonsai/tool/snap.py | 3 --- src/bonsai/bonsai/tool/spatial.py | 1 - src/bonsai/bonsai/tool/style.py | 1 - src/bonsai/bonsai/tool/surveyor.py | 2 -- src/bonsai/bonsai/tool/system.py | 1 - src/bonsai/bonsai/tool/unit.py | 2 +- src/bonsai/bonsai/tool/web.py | 2 -- src/bonsai/pyproject.toml | 3 +++ src/bonsai/scripts/bonsai_translations.py | 1 - src/bonsai/scripts/classifications/vbis.py | 3 --- src/bonsai/scripts/gbxml.py | 3 --- src/bonsai/scripts/generate_au_library.py | 1 - .../scripts/generate_entourage_library.py | 2 -- .../scripts/generate_furniture_library.py | 4 +--- .../scripts/generate_landscape_library.py | 7 ++----- src/bonsai/scripts/generate_site_library.py | 1 - .../scripts/generate_steel_profiles_library.py | 3 +-- .../scripts/geonodes_modifier_prototype.py | 1 - src/bonsai/scripts/get_all_qtos.py | 1 - src/bonsai/scripts/obj2ifc-meshlab.py | 2 -- src/bonsai/scripts/obj2ifc.py | 2 -- src/bonsai/scripts/replace_drawing_path.py | 1 - src/bonsai/scripts/setup_pytest.py | 8 ++++---- src/bonsai/scripts/standalone_drawer.py | 1 - src/bonsai/scripts/waldo.py | 1 - src/bonsai/test/pyproject.toml | 5 +++++ win/build-all-win.py | 2 +- 260 files changed, 113 insertions(+), 687 deletions(-) create mode 100644 src/bonsai/test/pyproject.toml diff --git a/src/bonsai/bonsai/__init__.py b/src/bonsai/bonsai/__init__.py index b4083b0eef..e30e2bf699 100644 --- a/src/bonsai/bonsai/__init__.py +++ b/src/bonsai/bonsai/__init__.py @@ -34,7 +34,6 @@ IN_PACKAGE = __package__ == "bonsai" import platform import re -import shutil import traceback import uuid import webbrowser diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index cfab523942..633d6292f9 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -24,21 +24,14 @@ import os import tempfile import zipfile from logging import Logger -from math import radians from typing import Union import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.util.element -import ifcopenshell.util.placement import ifcopenshell.util.unit -from mathutils import Vector -import bonsai.core.aggregate import bonsai.core.geometry -import bonsai.core.spatial -import bonsai.core.style import bonsai.tool as tool from bonsai.bim.ifc import IfcStore diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 9267134988..ab4a0875ab 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -28,7 +28,6 @@ import bpy import ifcopenshell import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.attribute -import ifcopenshell.util.element import ifcopenshell.util.unit from ifcopenshell.util.doc import ( get_attribute_doc, diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 9f846d8ccc..7e8f40560e 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -32,7 +32,6 @@ import ifcopenshell.api.pset import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element -import ifcopenshell.util.geolocation import ifcopenshell.util.placement import ifcopenshell.util.representation import ifcopenshell.util.shape diff --git a/src/bonsai/bonsai/bim/module/aggregate/decorator.py b/src/bonsai/bonsai/bim/module/aggregate/decorator.py index 080f965bf8..eb389a58bd 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/decorator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/decorator.py @@ -19,7 +19,6 @@ import blf import bpy import gpu -import ifcopenshell import ifcopenshell.util.element from bpy.types import SpaceView3D from bpy_extras import view3d_utils @@ -27,7 +26,6 @@ from gpu_extras.batch import batch_for_shader from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.module.geometry.decorator import ItemDecorator def transparent_color(color, alpha=0.1): diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index e9f23afece..98bc612784 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -19,8 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.group import ifcopenshell.api.pset import ifcopenshell.api.root diff --git a/src/bonsai/bonsai/bim/module/aggregate/prop.py b/src/bonsai/bonsai/bim/module/aggregate/prop.py index 241663882f..0595c200c5 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/prop.py +++ b/src/bonsai/bonsai/bim/module/aggregate/prop.py @@ -23,12 +23,7 @@ import ifcopenshell.util.element from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup @@ -37,8 +32,6 @@ from bonsai.bim.module.aggregate.decorator import ( AggregateDecorator, AggregateModeDecorator, ) -from bonsai.bim.module.spatial.data import SpatialData -from bonsai.bim.prop import Attribute, StrProperty def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool: diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py index d693843401..f59ead6308 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/ui.py +++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py @@ -21,7 +21,6 @@ from bpy.types import Panel import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.aggregate.data import AggregateData -from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData class BIM_PT_aggregate(Panel): diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py index 0705dcb1b9..42ad1d25de 100644 --- a/src/bonsai/bonsai/bim/module/alignment/operator.py +++ b/src/bonsai/bonsai/bim/module/alignment/operator.py @@ -18,24 +18,14 @@ # pyright: reportUnnecessaryTypeIgnoreComment=error -import calendar -import json -import os import time -from datetime import datetime import bpy import ifcopenshell.api.alignment import ifcopenshell.api.spatial import ifcopenshell.geom -import ifcopenshell.util.selector -import ifcopenshell.util.sequence -import isodate from bpy_extras.io_utils import ImportHelper -from dateutil import parser, relativedelta -import bonsai.bim.module.sequence.helper as helper -import bonsai.core.sequence as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py index 98ffb00c5c..acff2424b7 100644 --- a/src/bonsai/bonsai/bim/module/attribute/prop.py +++ b/src/bonsai/bonsai/bim/module/attribute/prop.py @@ -23,16 +23,12 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute class BIMAttributeProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/bcf/bcfstore.py b/src/bonsai/bonsai/bim/module/bcf/bcfstore.py index a1fec8aa6e..b1abc97c6e 100644 --- a/src/bonsai/bonsai/bim/module/bcf/bcfstore.py +++ b/src/bonsai/bonsai/bim/module/bcf/bcfstore.py @@ -19,9 +19,7 @@ import os from typing import Union -import bcf import bcf.bcfxml -import bcf.v2.bcfxml import bpy import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index b0a6f1f699..6edf257f05 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -16,22 +16,18 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os import tempfile import uuid import webbrowser -from math import atan, cos, degrees, radians, sin, tan +from math import atan, degrees, radians, tan from pathlib import Path -import bcf import bcf.agnostic.topic import bcf.agnostic.visinfo -import bcf.bcfxml import bcf.v2.bcfxml import bcf.v2.model import bcf.v2.topic import bcf.v2.visinfo -import bcf.v3 import bcf.v3.bcfxml import bcf.v3.document import bcf.v3.model @@ -43,11 +39,10 @@ import ifcopenshell.util.geolocation import ifcopenshell.util.unit import numpy as np from bpy_extras.io_utils import ExportHelper, ImportHelper -from mathutils import Euler, Matrix, Vector, geometry +from mathutils import Matrix, Vector from xsdata.models.datatype import XmlDateTime import bonsai.bim.module.bcf.bcfstore as bcfstore -import bonsai.bim.module.bcf.prop as bcf_prop import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/bcf/prop.py b/src/bonsai/bonsai/bim/module/bcf/prop.py index b720a2ff74..3ac751387e 100644 --- a/src/bonsai/bonsai/bim/module/bcf/prop.py +++ b/src/bonsai/bonsai/bim/module/bcf/prop.py @@ -24,8 +24,6 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, diff --git a/src/bonsai/bonsai/bim/module/bcf/ui.py b/src/bonsai/bonsai/bim/module/bcf/ui.py index e3478f63cf..881bef488b 100644 --- a/src/bonsai/bonsai/bim/module/bcf/ui.py +++ b/src/bonsai/bonsai/bim/module/bcf/ui.py @@ -18,7 +18,6 @@ from __future__ import annotations -import os from typing import TYPE_CHECKING import bpy diff --git a/src/bonsai/bonsai/bim/module/boundary/decorator.py b/src/bonsai/bonsai/bim/module/boundary/decorator.py index 1f5859824e..a2d134a135 100644 --- a/src/bonsai/bonsai/bim/module/boundary/decorator.py +++ b/src/bonsai/bonsai/bim/module/boundary/decorator.py @@ -20,7 +20,6 @@ import bmesh import gpu from bpy.types import SpaceView3D from gpu_extras.batch import batch_for_shader -from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index 4b26a0a4d9..5720d6aae5 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -29,18 +29,15 @@ import ifcopenshell.api.root import ifcopenshell.geom import ifcopenshell.util.element import ifcopenshell.util.placement -import ifcopenshell.util.representation import ifcopenshell.util.shape import ifcopenshell.util.unit import mathutils -import numpy as np import shapely import shapely.ops from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Matrix, Vector import bonsai.bim.import_ifc as import_ifc -import bonsai.core import bonsai.core.geometry import bonsai.tool as tool from bonsai.bim.ifc import IfcStore diff --git a/src/bonsai/bonsai/bim/module/boundary/prop.py b/src/bonsai/bonsai/bim/module/boundary/prop.py index cd7e0b9de3..2e9ab8c975 100644 --- a/src/bonsai/bonsai/bim/module/boundary/prop.py +++ b/src/bonsai/bonsai/bim/module/boundary/prop.py @@ -21,13 +21,7 @@ from typing import TYPE_CHECKING, Union import bpy from bpy.props import ( BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py index 54694d752c..0ca21b9ce5 100644 --- a/src/bonsai/bonsai/bim/module/boundary/ui.py +++ b/src/bonsai/bonsai/bim/module/boundary/ui.py @@ -16,8 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy -from bpy.types import Panel, UIList +from bpy.types import Panel import bonsai.tool as tool from bonsai.bim.module.boundary.data import SpaceBoundariesData diff --git a/src/bonsai/bonsai/bim/module/brick/operator.py b/src/bonsai/bonsai/bim/module/brick/operator.py index cb0a5f675f..2f71e6b638 100644 --- a/src/bonsai/bonsai/bim/module/brick/operator.py +++ b/src/bonsai/bonsai/bim/module/brick/operator.py @@ -19,7 +19,6 @@ import os import bpy -import ifcopenshell.api from bpy_extras.io_utils import ExportHelper, ImportHelper import bonsai.bim.handler diff --git a/src/bonsai/bonsai/bim/module/brick/prop.py b/src/bonsai/bonsai/bim/module/brick/prop.py index 9fe6d78407..6201113a09 100644 --- a/src/bonsai/bonsai/bim/module/brick/prop.py +++ b/src/bonsai/bonsai/bim/module/brick/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -34,7 +31,7 @@ from bpy.types import PropertyGroup import bonsai.core.brick as core import bonsai.tool.brick as tool from bonsai.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import StrProperty from bonsai.tool.brick import BrickStore diff --git a/src/bonsai/bonsai/bim/module/bsdd/data.py b/src/bonsai/bonsai/bim/module/bsdd/data.py index 6e916d3355..37d030d867 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/data.py +++ b/src/bonsai/bonsai/bim/module/bsdd/data.py @@ -16,13 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy -import ifcopenshell -import ifcopenshell.util.classification -import ifcopenshell.util.date import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore def refresh(): diff --git a/src/bonsai/bonsai/bim/module/bsdd/operator.py b/src/bonsai/bonsai/bim/module/bsdd/operator.py index d3f5533c1a..8b94e12fd9 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/operator.py +++ b/src/bonsai/bonsai/bim/module/bsdd/operator.py @@ -19,7 +19,6 @@ import textwrap from typing import Any import bpy -import bsdd import ifcopenshell.api.pset import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index efd8db46db..d595ad8018 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -34,7 +31,7 @@ from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.bsdd.data import BSDDData from bonsai.bim.module.classification.data import ClassificationsData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS: diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index f239eb41cf..f49f48abb9 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -24,7 +24,6 @@ import bpy from bpy.types import Panel, UIList import bonsai.tool as tool -import bsdd from bonsai.bim.module.bsdd.data import BSDDData if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index 857d757ca1..3b6ef8fc69 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -17,13 +17,11 @@ # along with Bonsai. If not, see . import math -from math import cos, pi, radians, sin, sqrt -from typing import Union +from math import pi, sqrt import bmesh import bpy import bpy_extras -import ifcopenshell.util.unit import mathutils from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/cad/prop.py b/src/bonsai/bonsai/bim/module/cad/prop.py index db737b51dc..7dab36df91 100644 --- a/src/bonsai/bonsai/bim/module/cad/prop.py +++ b/src/bonsai/bonsai/bim/module/cad/prop.py @@ -22,8 +22,6 @@ from typing import TYPE_CHECKING import bpy from bpy.types import PropertyGroup -from bonsai.bim.module.model.data import AuthoringData - class BIMCadProperties(PropertyGroup): resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index b384463ac2..20faa4dcc7 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -20,12 +20,10 @@ import os from functools import partial import bpy -import ifcopenshell.util.unit from bpy.types import WorkSpaceTool -import bonsai.bim.module.type.prop as type_prop import bonsai.tool as tool -from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData +from bonsai.bim.module.model.data import RailingData, RoofData def load_custom_icons(): diff --git a/src/bonsai/bonsai/bim/module/clash/data.py b/src/bonsai/bonsai/bim/module/clash/data.py index 4ecbb229a4..f5b87ede95 100644 --- a/src/bonsai/bonsai/bim/module/clash/data.py +++ b/src/bonsai/bonsai/bim/module/clash/data.py @@ -18,9 +18,6 @@ import json -import bpy -import ifcopenshell.util.element - import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/clash/decorator.py b/src/bonsai/bonsai/bim/module/clash/decorator.py index d1d0daf095..ab95c92d9f 100644 --- a/src/bonsai/bonsai/bim/module/clash/decorator.py +++ b/src/bonsai/bonsai/bim/module/clash/decorator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import blf -import bmesh import gpu from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 688568270f..94866a4e97 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -18,16 +18,13 @@ import json import logging -import os import tempfile from math import radians from pathlib import Path from typing import TYPE_CHECKING -import bmesh import bpy import ifcopenshell -import numpy as np from bpy_extras.io_utils import ExportHelper, ImportHelper from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/clash/prop.py b/src/bonsai/bonsai/bim/module/clash/prop.py index 5f42a400e7..8bcd71632b 100644 --- a/src/bonsai/bonsai/bim/module/clash/prop.py +++ b/src/bonsai/bonsai/bim/module/clash/prop.py @@ -26,7 +26,6 @@ from bpy.props import ( FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -34,7 +33,7 @@ from ifcopenshell.geom.main import CLASH_TYPE_ITEMS, ClashType from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.prop import Attribute, BIMFilterGroup, StrProperty +from bonsai.bim.prop import BIMFilterGroup, StrProperty class ClashSource(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index 7bf699cc31..77b77ba541 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -18,7 +18,6 @@ import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.classification import ifcopenshell.api.pset import ifcopenshell.util.classification diff --git a/src/bonsai/bonsai/bim/module/classification/prop.py b/src/bonsai/bonsai/bim/module/classification/prop.py index 454f861f07..44f174bd13 100644 --- a/src/bonsai/bonsai/bim/module/classification/prop.py +++ b/src/bonsai/bonsai/bim/module/classification/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index 77310be49a..6840238927 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -25,7 +25,6 @@ import ifcopenshell.util.classification from bpy.types import Panel, UIList import bonsai.bim.helper -import bonsai.bim.module.classification.prop as classification_prop import bonsai.tool as tool from bonsai.bim.module.classification.data import ( ClassificationsData, diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py index 976f4ff6a3..d438325f44 100644 --- a/src/bonsai/bonsai/bim/module/constraint/operator.py +++ b/src/bonsai/bonsai/bim/module/constraint/operator.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.constraint import bonsai.bim.helper diff --git a/src/bonsai/bonsai/bim/module/constraint/prop.py b/src/bonsai/bonsai/bim/module/constraint/prop.py index c76d48e8a6..55ae9d1223 100644 --- a/src/bonsai/bonsai/bim/module/constraint/prop.py +++ b/src/bonsai/bonsai/bim/module/constraint/prop.py @@ -20,19 +20,13 @@ from typing import TYPE_CHECKING, Literal import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from ifcopenshell.util.doc import get_entity_doc -import bonsai.tool as tool from bonsai.bim.module.constraint.data import ConstraintsData from bonsai.bim.prop import Attribute diff --git a/src/bonsai/bonsai/bim/module/context/data.py b/src/bonsai/bonsai/bim/module/context/data.py index ade8061a4b..e2717a166a 100644 --- a/src/bonsai/bonsai/bim/module/context/data.py +++ b/src/bonsai/bonsai/bim/module/context/data.py @@ -18,7 +18,6 @@ from typing import Any -import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/context/prop.py b/src/bonsai/bonsai/bim/module/context/prop.py index ff73c53a5a..e8540746ff 100644 --- a/src/bonsai/bonsai/bim/module/context/prop.py +++ b/src/bonsai/bonsai/bim/module/context/prop.py @@ -20,19 +20,13 @@ from typing import TYPE_CHECKING import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.module.context.data import ContextData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute class BIMContextProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/cost/data.py b/src/bonsai/bonsai/bim/module/cost/data.py index aab9ea9231..32299b8e41 100644 --- a/src/bonsai/bonsai/bim/module/cost/data.py +++ b/src/bonsai/bonsai/bim/module/cost/data.py @@ -18,13 +18,11 @@ from typing import Any, Union -import bpy import ifcopenshell import ifcopenshell.util.cost import ifcopenshell.util.date import ifcopenshell.util.element import ifcopenshell.util.unit -from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index 48b672a2c2..b612100a1d 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -987,10 +987,10 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper): @classmethod def poll(cls, context): try: - import typst + import typst # noqa: F401 return True - except: + except ModuleNotFoundError: cls.poll_message_set( "Typst not available.\nIt can be installed from Quality and\nControl -> Debug and using 'typst' with Pip Install.\n(Run Blender as Administrator)" ) diff --git a/src/bonsai/bonsai/bim/module/cost/prop.py b/src/bonsai/bonsai/bim/module/cost/prop.py index 121ca617e7..426d5c1e33 100644 --- a/src/bonsai/bonsai/bim/module/cost/prop.py +++ b/src/bonsai/bonsai/bim/module/cost/prop.py @@ -19,16 +19,13 @@ from typing import TYPE_CHECKING, Literal, Union import bpy -import ifcopenshell.api import ifcopenshell.api.cost from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index 68b2704166..728b4f6013 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -21,7 +21,6 @@ import os from functools import partial import bpy -import ifcopenshell from bpy.types import WorkSpaceTool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py index fa298ada9c..fb2b2f3086 100644 --- a/src/bonsai/bonsai/bim/module/covetool/operator.py +++ b/src/bonsai/bonsai/bim/module/covetool/operator.py @@ -20,7 +20,6 @@ import json from math import atan2, degrees import bpy -import ifcopenshell import ifcopenshell.util.element import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index d42e8d3852..3b1db40b6b 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -19,14 +19,10 @@ from __future__ import annotations import json -import logging -import os -import tempfile from collections import Counter from typing import TYPE_CHECKING import bpy -import ifccsv import ifcopenshell import ifcopenshell.util.selector from bpy_extras.io_utils import ExportHelper, ImportHelper diff --git a/src/bonsai/bonsai/bim/module/csv/prop.py b/src/bonsai/bonsai/bim/module/csv/prop.py index a698f89b76..8104a8d75b 100644 --- a/src/bonsai/bonsai/bim/module/csv/prop.py +++ b/src/bonsai/bonsai/bim/module/csv/prop.py @@ -23,15 +23,11 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.prop import BIMFilterGroup, StrProperty +from bonsai.bim.prop import BIMFilterGroup class CsvAttribute(PropertyGroup): diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 6e30a3cc0f..9315386008 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W diff --git a/src/bonsai/bonsai/bim/module/debug/prop.py b/src/bonsai/bonsai/bim/module/debug/prop.py index e52ec5ff7d..9744159e20 100644 --- a/src/bonsai/bonsai/bim/module/debug/prop.py +++ b/src/bonsai/bonsai/bim/module/debug/prop.py @@ -20,13 +20,9 @@ from typing import TYPE_CHECKING, Literal, get_args import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/demo/prop.py b/src/bonsai/bonsai/bim/module/demo/prop.py index f206fe3156..6570c4cbc0 100644 --- a/src/bonsai/bonsai/bim/module/demo/prop.py +++ b/src/bonsai/bonsai/bim/module/demo/prop.py @@ -32,18 +32,10 @@ from typing import TYPE_CHECKING -import bpy - # Properties have many different data types. We won't use all of them in this # demo module, but this is a list for your reference. from bpy.props import ( BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/diff/prop.py b/src/bonsai/bonsai/bim/module/diff/prop.py index c8f9d48c84..cfe5b147fe 100644 --- a/src/bonsai/bonsai/bim/module/diff/prop.py +++ b/src/bonsai/bonsai/bim/module/diff/prop.py @@ -23,16 +23,12 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup from bonsai.bim.module.diff.data import DiffData -from bonsai.bim.prop import BIMFilterGroup, StrProperty +from bonsai.bim.prop import BIMFilterGroup def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 97b4a1f206..f194e2f4e0 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -19,8 +19,6 @@ import os import bpy -import ifcopenshell -import ifcopenshell.util.schema from natsort import natsorted import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index a4e5cfc4ac..006ed27957 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -20,10 +20,9 @@ import json import bpy -import bonsai.bim.handler import bonsai.core.document as core import bonsai.tool as tool -from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData +from bonsai.bim.module.document.data import ObjectDocumentData class LoadProjectDocuments(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index b1eb41cc2e..c51f7b636c 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -5,17 +5,14 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.document.data import DocumentData, refresh -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_document_name(self: "Document", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index eda5d34a1d..d618d6d966 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.types import Panel, UIList import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 18d914872a..d1e2dc4b2b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -18,15 +18,12 @@ from __future__ import annotations -import math -import os -from pathlib import Path from typing import Optional import bmesh import bpy import ifcopenshell.util.element -from mathutils import Matrix, Vector +from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index 1668d386d3..8373ed29bc 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -23,8 +23,6 @@ from typing import Any, Union import bpy import ifcopenshell.util.element -import ifcopenshell.util.representation -import ifcopenshell.util.selector import ifcopenshell.util.unit from natsort import natsorted diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 705fabfcf9..f950f7a1bb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -23,7 +23,7 @@ from functools import cache from math import acos, atan, cos, degrees, pi, radians, sin from pathlib import Path from timeit import default_timer as timer -from typing import Optional, Union +from typing import Optional import blf import bmesh @@ -34,7 +34,6 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.unit import numpy as np -import shapely from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d from gpu_extras.batch import batch_for_shader diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index ee8ecf805d..7dce81359d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -17,10 +17,8 @@ # along with Bonsai. If not, see . import math -from typing import Union import bpy -import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.unit import mathutils.geometry diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 2cddf46f9e..55804498ec 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -20,7 +20,6 @@ import hashlib import json import multiprocessing import os -import re import shutil import subprocess import time @@ -42,7 +41,6 @@ import bmesh import bpy import logging import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.document import ifcopenshell.api.pset import ifcopenshell.api.style @@ -55,17 +53,14 @@ import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import numpy as np import shapely -import shapely.ops from bpy_extras.image_utils import load_image from bpy_extras.io_utils import ImportHelper from lxml import etree -from mathutils import Color, Matrix, Vector +from mathutils import Color, Vector import bonsai.bim.import_ifc import bonsai.bim.export_ifc import bonsai.bim.handler -import bonsai.bim.helper -import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.sheeter as sheeter import bonsai.bim.module.drawing.svgwriter as svgwriter import bonsai.core.drawing as core diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index b323b8d41e..cc597a4802 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -18,14 +18,11 @@ import enum import json -import os from collections.abc import Callable -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Union, get_args +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.element from bpy.props import ( @@ -34,7 +31,6 @@ from bpy.props import ( CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -42,19 +38,17 @@ from bpy.props import ( from bpy.types import PropertyGroup from mathutils import Matrix -import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.decoration as decoration import bonsai.core.drawing as core import bonsai.tool as tool from bonsai.bim.module.drawing.data import ( AnnotationData, - DecoratorData, DrawingsData, ElementValuesData, SheetsData, ) from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data -from bonsai.bim.prop import Attribute, BIMFilterGroup, StrProperty +from bonsai.bim.prop import Attribute, BIMFilterGroup diagram_scales_enum = [] diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index 3b120e8d99..a1610020c4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -22,7 +22,6 @@ import string from pathlib import Path from textwrap import wrap -import bpy import openpyxl import openpyxl.cell # Unnecessary, bug in typeshed. import openpyxl.utils # Unnecessary, bug in typeshed. @@ -33,7 +32,6 @@ from odf.table import Table, TableCell, TableColumn, TableRow from odf.text import P import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.drawing.svgwriter import SvgWriter DEBUG = False diff --git a/src/bonsai/bonsai/bim/module/drawing/sheeter.py b/src/bonsai/bonsai/bim/module/drawing/sheeter.py index 7d8ebb67f4..df57b6efb5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/sheeter.py +++ b/src/bonsai/bonsai/bim/module/drawing/sheeter.py @@ -26,7 +26,6 @@ import xml.etree.ElementTree as ET from pathlib import Path from xml.dom import minidom -import bpy import ifcopenshell.util.geolocation import pystache from mathutils import Vector diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index ef64fdefc6..83566c4d0b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -18,7 +18,6 @@ import math import os -import re import shutil import xml.etree.ElementTree as ET from collections.abc import Callable, Sequence @@ -30,10 +29,8 @@ import bmesh import bpy import ifcopenshell import ifcopenshell.util.element -import ifcopenshell.util.representation import ifcopenshell.util.selector import ifcopenshell.util.unit -import mathutils import svgwrite import svgwrite.container import svgwrite.text diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index dc98715686..1b6a11cd75 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -30,7 +30,6 @@ from bonsai.bim.module.drawing.data import ( DocumentsData, DrawingsData, ElementFiltersData, - ElementValuesData, ProductAssignmentsData, SheetsData, ) diff --git a/src/bonsai/bonsai/bim/module/fm/data.py b/src/bonsai/bonsai/bim/module/fm/data.py index 80a686e364..6d486fd9ce 100644 --- a/src/bonsai/bonsai/bim/module/fm/data.py +++ b/src/bonsai/bonsai/bim/module/fm/data.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import importlib -import os import ifcfm diff --git a/src/bonsai/bonsai/bim/module/fm/operator.py b/src/bonsai/bonsai/bim/module/fm/operator.py index 629251282a..43e9bb50e7 100644 --- a/src/bonsai/bonsai/bim/module/fm/operator.py +++ b/src/bonsai/bonsai/bim/module/fm/operator.py @@ -16,10 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json -import logging import os -import tempfile import bpy import ifcfm diff --git a/src/bonsai/bonsai/bim/module/fm/prop.py b/src/bonsai/bonsai/bim/module/fm/prop.py index 2eb1ae4739..cd6b038bc7 100644 --- a/src/bonsai/bonsai/bim/module/fm/prop.py +++ b/src/bonsai/bonsai/bim/module/fm/prop.py @@ -23,11 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index bc64947436..111b67b5b0 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import math - import bpy import ifcopenshell.util.element from bpy.app.handlers import persistent diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py index c5a82c1d59..589b18ec84 100644 --- a/src/bonsai/bonsai/bim/module/geometry/decorator.py +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -19,7 +19,6 @@ from collections.abc import Sequence import blf -import bmesh import bpy import gpu import ifcopenshell diff --git a/src/bonsai/bonsai/bim/module/geometry/helper.py b/src/bonsai/bonsai/bim/module/geometry/helper.py index 511669d60d..3b14003435 100644 --- a/src/bonsai/bonsai/bim/module/geometry/helper.py +++ b/src/bonsai/bonsai/bim/module/geometry/helper.py @@ -16,22 +16,17 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from math import pi, pow +from math import pi from typing import Any, Optional, TypeVar, Union import bmesh import bpy import ifcopenshell -import ifcopenshell.util.shape import ifcopenshell.util.unit import mathutils -import numpy as np -import shapely from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Matrix, Vector, geometry -import bonsai.tool as tool - T = TypeVar("T") diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 7937acca30..9ea6ba72f5 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import re from collections.abc import Sequence from time import time from typing import ( @@ -32,11 +31,9 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.boundary import ifcopenshell.api.drawing import ifcopenshell.api.geometry -import ifcopenshell.api.grid import ifcopenshell.api.group import ifcopenshell.api.layer import ifcopenshell.api.material @@ -61,10 +58,8 @@ import bonsai.core.geometry as core import bonsai.core.nest import bonsai.core.root import bonsai.core.spatial -import bonsai.core.style import bonsai.tool as tool from bonsai.bim.ifc import IfcStore -from bonsai.bim.module.model.data import AuthoringData from bonsai.bim.module.model.decorator import ProfileDecorator if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 5336eef1c0..549c3bd227 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -24,8 +24,6 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -34,7 +32,6 @@ from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.geometry.data import RepresentationsData, ViewportData -from bonsai.bim.prop import Attribute, ObjProperty, StrProperty def get_contexts(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/georeference/data.py b/src/bonsai/bonsai/bim/module/georeference/data.py index cf86314f9a..bc9e5b9ac4 100644 --- a/src/bonsai/bonsai/bim/module/georeference/data.py +++ b/src/bonsai/bonsai/bim/module/georeference/data.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . -import bpy import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.schema diff --git a/src/bonsai/bonsai/bim/module/georeference/decorator.py b/src/bonsai/bonsai/bim/module/georeference/decorator.py index 6cece45cc8..05bce0e20b 100644 --- a/src/bonsai/bonsai/bim/module/georeference/decorator.py +++ b/src/bonsai/bonsai/bim/module/georeference/decorator.py @@ -19,10 +19,7 @@ from math import radians import blf -import bmesh -import bpy import gpu -import ifcopenshell import ifcopenshell.util.geolocation from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d diff --git a/src/bonsai/bonsai/bim/module/georeference/operator.py b/src/bonsai/bonsai/bim/module/georeference/operator.py index c2af3d9225..93a4e0ef24 100644 --- a/src/bonsai/bonsai/bim/module/georeference/operator.py +++ b/src/bonsai/bonsai/bim/module/georeference/operator.py @@ -21,7 +21,6 @@ from bpy_extras.io_utils import ImportHelper import bonsai.core.georeference as core import bonsai.tool as tool -from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/georeference/prop.py b/src/bonsai/bonsai/bim/module/georeference/prop.py index 1f4ca321ac..41035398e9 100644 --- a/src/bonsai/bonsai/bim/module/georeference/prop.py +++ b/src/bonsai/bonsai/bim/module/georeference/prop.py @@ -23,11 +23,7 @@ import ifcopenshell.util.geolocation from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/group/data.py b/src/bonsai/bonsai/bim/module/group/data.py index c7be2ff393..fd5eba102d 100644 --- a/src/bonsai/bonsai/bim/module/group/data.py +++ b/src/bonsai/bonsai/bim/module/group/data.py @@ -17,9 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell -import ifcopenshell.util.cost -import ifcopenshell.util.element import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index 66872ed90a..adea9ee48c 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -16,10 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import TYPE_CHECKING, Literal, get_args +from typing import TYPE_CHECKING, get_args import bpy -import ifcopenshell.api import ifcopenshell.api.group import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/bim/module/group/prop.py b/src/bonsai/bonsai/bim/module/group/prop.py index 48bff62e35..56cf7aae07 100644 --- a/src/bonsai/bonsai/bim/module/group/prop.py +++ b/src/bonsai/bonsai/bim/module/group/prop.py @@ -22,18 +22,14 @@ import bpy from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.pset.data import refresh as refresh_pset -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_active_group_index(self, context): diff --git a/src/bonsai/bonsai/bim/module/ifcgit/data.py b/src/bonsai/bonsai/bim/module/ifcgit/data.py index 9cb9f2655d..92da517a1c 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/data.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/data.py @@ -1,8 +1,6 @@ import os import shutil -import bpy - # import tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/layer/data.py b/src/bonsai/bonsai/bim/module/layer/data.py index 4550d3257e..1ec9db8b39 100644 --- a/src/bonsai/bonsai/bim/module/layer/data.py +++ b/src/bonsai/bonsai/bim/module/layer/data.py @@ -19,7 +19,6 @@ from typing import Any import bpy -import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py index 8475bebd54..4ff2a4fe0e 100644 --- a/src/bonsai/bonsai/bim/module/layer/operator.py +++ b/src/bonsai/bonsai/bim/module/layer/operator.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.layer import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/bim/module/layer/prop.py b/src/bonsai/bonsai/bim/module/layer/prop.py index 26a6c3680d..f91681af76 100644 --- a/src/bonsai/bonsai/bim/module/layer/prop.py +++ b/src/bonsai/bonsai/bim/module/layer/prop.py @@ -23,16 +23,13 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_layer_property(self: "Layer", context: bpy.types.Context, *, property: str) -> None: diff --git a/src/bonsai/bonsai/bim/module/library/operator.py b/src/bonsai/bonsai/bim/module/library/operator.py index 6c7abe8be5..058b474732 100644 --- a/src/bonsai/bonsai/bim/module/library/operator.py +++ b/src/bonsai/bonsai/bim/module/library/operator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell.api import bonsai.core.library as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/library/prop.py b/src/bonsai/bonsai/bim/module/library/prop.py index 69647c195f..d7a9b51e1f 100644 --- a/src/bonsai/bonsai/bim/module/library/prop.py +++ b/src/bonsai/bonsai/bim/module/library/prop.py @@ -20,20 +20,16 @@ from typing import TYPE_CHECKING, Literal import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.library.data import LibrariesData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def update_active_reference_index(self, context): diff --git a/src/bonsai/bonsai/bim/module/library/ui.py b/src/bonsai/bonsai/bim/module/library/ui.py index f76e6a3251..32780f2797 100644 --- a/src/bonsai/bonsai/bim/module/library/ui.py +++ b/src/bonsai/bonsai/bim/module/library/ui.py @@ -28,7 +28,7 @@ import bonsai.tool as tool from bonsai.bim.module.library.data import LibrariesData, LibraryReferencesData if TYPE_CHECKING: - from bonsai.bim.module.library.prop import BIMLibraryProperties, LibraryReference + from bonsai.bim.module.library.prop import LibraryReference class BIM_PT_libraries(Panel): diff --git a/src/bonsai/bonsai/bim/module/light/__init__.py b/src/bonsai/bonsai/bim/module/light/__init__.py index 1bfe11be7a..fa167bc518 100644 --- a/src/bonsai/bonsai/bim/module/light/__init__.py +++ b/src/bonsai/bonsai/bim/module/light/__init__.py @@ -16,10 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import importlib import importlib.util import stat -import traceback from pathlib import Path import bpy diff --git a/src/bonsai/bonsai/bim/module/light/data.py b/src/bonsai/bonsai/bim/module/light/data.py index d708b6170f..d14835bb27 100644 --- a/src/bonsai/bonsai/bim/module/light/data.py +++ b/src/bonsai/bonsai/bim/module/light/data.py @@ -16,9 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy import ifcopenshell.util.geolocation -from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/light/decorator.py b/src/bonsai/bonsai/bim/module/light/decorator.py index 9cfb9ae186..f72a941ab8 100644 --- a/src/bonsai/bonsai/bim/module/light/decorator.py +++ b/src/bonsai/bonsai/bim/module/light/decorator.py @@ -16,10 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from math import degrees, radians import blf -import bmesh import bpy import gpu from bpy.types import SpaceView3D diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 36c93add4f..543a44824b 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -16,14 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from typing import Any, Union import bpy import ifcopenshell import ifcopenshell.util.doc import ifcopenshell.util.element -import ifcopenshell.util.schema from natsort import natsorted import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 81ef17e6e4..e0761bf416 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -20,7 +20,6 @@ import json from typing import TYPE_CHECKING, Any, Literal, Union import bpy -import ifcopenshell.api import ifcopenshell.api.material import ifcopenshell.api.profile import ifcopenshell.api.style diff --git a/src/bonsai/bonsai/bim/module/material/prop.py b/src/bonsai/bonsai/bim/module/material/prop.py index 27567ab27e..9c8454f05e 100644 --- a/src/bonsai/bonsai/bim/module/material/prop.py +++ b/src/bonsai/bonsai/bim/module/material/prop.py @@ -19,25 +19,21 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from ifcopenshell.util.doc import get_entity_doc import bonsai.tool as tool from bonsai.bim.module.classification.data import MaterialClassificationsData from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData from bonsai.bim.module.profile.data import ProfileData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_profile_classes(self, context): diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 2ac1908327..f743306767 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any import bpy import ifcopenshell.util.element -import ifcopenshell.util.unit from bpy.types import Panel, UIList import bonsai.bim.helper diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 33703331f8..02cc56834b 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -19,12 +19,11 @@ from typing import TYPE_CHECKING, Literal, assert_never, get_args import bpy -import ifcopenshell import ifcopenshell.util.geolocation import ifcopenshell.util.placement import ifcopenshell.util.unit import numpy as np -from mathutils import Euler, Matrix, Vector +from mathutils import Matrix import bonsai.core.geometry as core_geometry import bonsai.core.misc as core diff --git a/src/bonsai/bonsai/bim/module/misc/prop.py b/src/bonsai/bonsai/bim/module/misc/prop.py index 889e1f5099..f596f22fc6 100644 --- a/src/bonsai/bonsai/bim/module/misc/prop.py +++ b/src/bonsai/bonsai/bim/module/misc/prop.py @@ -16,21 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.props import ( - BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.prop import Attribute, StrProperty - class BIMMiscProperties(PropertyGroup): total_storeys: IntProperty( diff --git a/src/bonsai/bonsai/bim/module/model/array.py b/src/bonsai/bonsai/bim/module/model/array.py index 5f045435ac..dd54bf0ab3 100644 --- a/src/bonsai/bonsai/bim/module/model/array.py +++ b/src/bonsai/bonsai/bim/module/model/array.py @@ -19,12 +19,10 @@ import json import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.element import ifcopenshell.util.unit -from mathutils import Matrix, Vector +from mathutils import Matrix import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/model/covering.py b/src/bonsai/bonsai/bim/module/model/covering.py index 468554b9be..dd3889a7cb 100644 --- a/src/bonsai/bonsai/bim/module/model/covering.py +++ b/src/bonsai/bonsai/bim/module/model/covering.py @@ -18,8 +18,6 @@ import bpy -import ifcopenshell -import ifcopenshell.util.element import bonsai.core.covering as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 0b97e05e77..10553f1bed 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -25,7 +25,7 @@ import bpy import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.schema -from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc +from ifcopenshell.util.doc import get_entity_doc from natsort import natsorted import bonsai.tool as tool @@ -424,12 +424,12 @@ class SverchokData: return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Sverchok") @classmethod - def has_sverchok(cls): + def has_sverchok(cls) -> bool: try: - import sverchok + import sverchok # noqa: F401 return True - except: + except ModuleNotFoundError: return False diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 1a580b9fc7..d4dc1a218d 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -19,9 +19,8 @@ from __future__ import annotations import math -from itertools import chain from math import cos, radians, sin, tan -from typing import Any, Literal, Union +from typing import Any, Literal import blf import bmesh diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index dbdf5e8dcf..5a14cde101 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -17,21 +17,18 @@ # along with Bonsai. If not, see . -import collections import collections.abc import json -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.schema import ifcopenshell.util.unit from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/model/grid.py b/src/bonsai/bonsai/bim/module/model/grid.py index df4f140ebc..8ceaa605dc 100644 --- a/src/bonsai/bonsai/bim/module/model/grid.py +++ b/src/bonsai/bonsai/bim/module/model/grid.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.grid from bpy.props import FloatProperty, IntProperty from bpy.types import Operator diff --git a/src/bonsai/bonsai/bim/module/model/handler.py b/src/bonsai/bonsai/bim/module/model/handler.py index 47075cdcfa..bfa064cda7 100644 --- a/src/bonsai/bonsai/bim/module/model/handler.py +++ b/src/bonsai/bonsai/bim/module/model/handler.py @@ -16,12 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy -import ifcopenshell import ifcopenshell.api from bpy.app.handlers import persistent -from bonsai.bim.module.model import opening, product, profile, slab, task, wall +from bonsai.bim.module.model import opening, product, profile, task @persistent diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index cdb2538d28..245e10a21b 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -16,18 +16,13 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import collections import collections.abc import json -import math import re from copy import copy -from math import asin, cos, degrees, pi, radians, sin, tan +from math import cos, degrees, pi, radians, sin, tan -import bmesh import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset @@ -41,9 +36,7 @@ import numpy as np from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Matrix, Vector -import bonsai.core.geometry import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.module.model.profile import DumbProfileJoiner from bonsai.tool.cad import VTX_PRECISION diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index b0806ec33d..c20325c2e7 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -16,37 +16,29 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json -import logging -from collections import defaultdict from collections.abc import Sequence -from math import pi, radians -from typing import Any, Optional, Union, cast +from math import radians +from typing import Any, Optional, Union import bmesh import bpy import gpu import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.feature import ifcopenshell.api.geometry import ifcopenshell.api.root import ifcopenshell.geom import ifcopenshell.util.element -import ifcopenshell.util.placement import ifcopenshell.util.representation import ifcopenshell.util.shape import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import numpy as np import shapely -from bpy.props import FloatProperty from bpy.types import Operator, SpaceView3D -from bpy_extras.object_utils import AddObjectHelper, object_data_add from gpu_extras.batch import batch_for_shader -from mathutils import Euler, Matrix, Vector +from mathutils import Matrix, Vector -import bonsai.bim.import_ifc as import_ifc import bonsai.core.geometry import bonsai.tool as tool from bonsai.bim.module.drawing.decoration import DecoratorData diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 92ec36581b..e33fe2cf5d 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -18,28 +18,13 @@ from __future__ import annotations -import copy -import math -from typing import Any, Literal, Optional, Union +from typing import Literal, Union -import bmesh import bpy import ifcopenshell -import ifcopenshell.api -import ifcopenshell.geom -import ifcopenshell.util.element -import ifcopenshell.util.placement -import ifcopenshell.util.representation -import ifcopenshell.util.type import ifcopenshell.util.unit -import mathutils.geometry -from lark import Lark, Transformer from mathutils import Vector -import bonsai.core.geometry -import bonsai.core.model as core -import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.module.model.decorator import PolylineDecorator diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 012cba4889..d7c96bce1d 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -19,12 +19,11 @@ # pyright: reportUnnecessaryTypeIgnoreComment=error import json -from typing import TYPE_CHECKING, Any, Literal, Optional, Union, assert_never, get_args +from typing import TYPE_CHECKING, Any, Literal, get_args import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.system import ifcopenshell.util.element @@ -34,8 +33,6 @@ import ifcopenshell.util.shape_builder import ifcopenshell.util.system import ifcopenshell.util.type import ifcopenshell.util.unit -import mathutils -import numpy as np from bpy_extras.object_utils import AddObjectHelper from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 4e16f9b296..f759d40a94 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -20,10 +20,8 @@ import copy from math import atan2, degrees, pi, radians from typing import Any, Literal, Optional, Union -import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.pset import ifcopenshell.api.type @@ -38,7 +36,6 @@ from mathutils import Matrix, Vector import bonsai.core.geometry import bonsai.core.material import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.decorator import ( diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index ec79a49196..cb246ac7c5 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -22,7 +22,6 @@ from math import pi, radians from typing import TYPE_CHECKING, Any, Literal, Optional, Union, get_args import bpy -import ifcopenshell import ifcopenshell.util.element from bpy.types import NodeTree, PropertyGroup from mathutils import Vector diff --git a/src/bonsai/bonsai/bim/module/model/railing.py b/src/bonsai/bonsai/bim/module/model/railing.py index 624b1b64a7..7ca66d8dbc 100644 --- a/src/bonsai/bonsai/bim/module/model/railing.py +++ b/src/bonsai/bonsai/bim/module/model/railing.py @@ -23,7 +23,6 @@ from typing import Any import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.pset import ifcopenshell.util.representation diff --git a/src/bonsai/bonsai/bim/module/model/roof.py b/src/bonsai/bonsai/bim/module/model/roof.py index 4038faa993..e1f7903299 100644 --- a/src/bonsai/bonsai/bim/module/model/roof.py +++ b/src/bonsai/bonsai/bim/module/model/roof.py @@ -23,14 +23,12 @@ from typing import Any, Literal, Union import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.representation import ifcopenshell.util.unit -import mathutils.geometry import shapely from bpypolyskel import bpypolyskel -from mathutils import Matrix, Quaternion, Vector +from mathutils import Quaternion, Vector import bonsai.core.root import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 602c36df11..58a353ab28 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -17,13 +17,10 @@ # along with Bonsai. If not, see . import json -from math import acos, cos, degrees, pi, sin -from typing import Optional +from math import cos, pi -import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset @@ -37,7 +34,6 @@ from mathutils import Matrix, Vector import bonsai.core.geometry import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.decorator import ( diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index 5b52308542..87152c645b 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -23,7 +23,6 @@ import bpy import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element -import ifcopenshell.util.representation import ifcopenshell.util.unit from mathutils import Matrix, Vector @@ -38,7 +37,7 @@ from bonsai.tool.numeric_input import ( ) V_ = tool.Blender.V_ -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING from bmesh.types import BMVert from bpy.props import IntProperty diff --git a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py index 0a318b1fb1..08f29890c4 100644 --- a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py +++ b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py @@ -22,7 +22,6 @@ import zipfile import bmesh import bpy -import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element from bpy_extras.io_utils import ExportHelper, ImportHelper diff --git a/src/bonsai/bonsai/bim/module/model/task.py b/src/bonsai/bonsai/bim/module/model/task.py index 93a361b73c..a6fe2607a2 100644 --- a/src/bonsai/bonsai/bim/module/model/task.py +++ b/src/bonsai/bonsai/bim/module/model/task.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset import ifcopenshell.util.date diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index d8a297433b..512eaf3fdf 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -22,11 +22,10 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, Any import bpy -from bpy.types import Menu, Panel +from bpy.types import Panel import bonsai.bim import bonsai.tool as tool -from bonsai.bim import module from bonsai.bim.helper import prop_with_search from bonsai.bim.module.model.data import ( ArrayData, diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 7f4579df31..441566e3e6 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -20,12 +20,11 @@ import copy import math -from math import acos, atan2, cos, degrees, pi, sin -from typing import TYPE_CHECKING, Any, Literal, Optional, Union, assert_never, get_args +from math import atan2, cos, degrees, pi, sin +from typing import TYPE_CHECKING, Any, Literal, Union, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.feature import ifcopenshell.api.geometry import ifcopenshell.api.material @@ -45,11 +44,9 @@ from mathutils import Matrix, Vector import bonsai.core.geometry import bonsai.core.model as core import bonsai.core.root -import bonsai.core.type import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator -from bonsai.bim.module.model.opening import FilledOpeningGenerator from bonsai.bim.module.model.polyline import PolylineOperator diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index ad473a110e..30e8d767b5 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -17,21 +17,18 @@ # along with Bonsai. If not, see . -import collections import collections.abc import json -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.shape_builder import ifcopenshell.util.unit from bmesh.types import BMVert from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 7b7a91906d..5f7cf7699d 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -17,15 +17,13 @@ # along with Bonsai. If not, see . import os -import sys from functools import partial -from typing import Any, Optional, Union +from typing import Optional, Union import bpy import bpy.utils.previews from bpy.types import Menu, WorkSpaceTool -import bonsai.bim import bonsai.core.model as core import bonsai.tool as tool from bonsai.bim.helper import draw_attribute, prop_with_search diff --git a/src/bonsai/bonsai/bim/module/nest/decorator.py b/src/bonsai/bonsai/bim/module/nest/decorator.py index e9630ebc26..66608b0171 100644 --- a/src/bonsai/bonsai/bim/module/nest/decorator.py +++ b/src/bonsai/bonsai/bim/module/nest/decorator.py @@ -19,7 +19,6 @@ import blf import bpy import gpu -import ifcopenshell import ifcopenshell.util.element from bpy.types import SpaceView3D from bpy_extras import view3d_utils @@ -27,7 +26,6 @@ from gpu_extras.batch import batch_for_shader from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.module.geometry.decorator import ItemDecorator def transparent_color(color, alpha=0.1): diff --git a/src/bonsai/bonsai/bim/module/nest/operator.py b/src/bonsai/bonsai/bim/module/nest/operator.py index 9d7eb2df67..6961fea0c8 100644 --- a/src/bonsai/bonsai/bim/module/nest/operator.py +++ b/src/bonsai/bonsai/bim/module/nest/operator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell import ifcopenshell.util.element import bonsai.core.nest as core diff --git a/src/bonsai/bonsai/bim/module/nest/prop.py b/src/bonsai/bonsai/bim/module/nest/prop.py index c36388398c..4c7533455a 100644 --- a/src/bonsai/bonsai/bim/module/nest/prop.py +++ b/src/bonsai/bonsai/bim/module/nest/prop.py @@ -22,19 +22,12 @@ import bpy from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.nest.decorator import NestDecorator, NestModeDecorator -from bonsai.bim.module.spatial.data import SpatialData -from bonsai.bim.prop import Attribute, StrProperty def update_relating_object(self: "BIMObjectNestProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/owner/prop.py b/src/bonsai/bonsai/bim/module/owner/prop.py index dfe2b95ecd..171712e799 100644 --- a/src/bonsai/bonsai/bim/module/owner/prop.py +++ b/src/bonsai/bonsai/bim/module/owner/prop.py @@ -20,14 +20,9 @@ from typing import TYPE_CHECKING, Literal import bpy from bpy.props import ( - BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index 57413214d6..99459b99e9 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import json -import os from pathlib import Path from typing import TYPE_CHECKING, cast diff --git a/src/bonsai/bonsai/bim/module/patch/prop.py b/src/bonsai/bonsai/bim/module/patch/prop.py index 9382b4e786..e14bb3b1ef 100644 --- a/src/bonsai/bonsai/bim/module/patch/prop.py +++ b/src/bonsai/bonsai/bim/module/patch/prop.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import importlib import importlib.util from pathlib import Path from typing import TYPE_CHECKING, Literal, Union @@ -27,15 +26,11 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute ifcpatchrecipes_enum: list[tuple[str, str, str]] = [] diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 34ea4d1b7b..09f8eaf383 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -19,7 +19,6 @@ from typing import Any import bpy -import bpy.utils import bpy.utils.previews import ifcopenshell.util.doc diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 871d8f3b86..42a37989a0 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.util.element import bonsai.bim.helper import bonsai.bim.module.model.profile as model_profile -import bonsai.core.profile as core import bonsai.tool as tool from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.decorator import ProfileDecorator diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index 61e2546193..3a4a71650e 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -19,15 +19,10 @@ from typing import TYPE_CHECKING, Union import bpy -import ifcopenshell -import ifcopenshell.util.attribute -import ifcopenshell.util.schema from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -36,7 +31,7 @@ from bpy.types import PropertyGroup import bonsai.tool as tool from bonsai.bim.module.profile.data import ProfileData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_profile_classes(self: "BIMProfileProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index e2e960b382..56a77d1880 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -16,12 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from collections import defaultdict from pathlib import Path from typing import Any, Union -import bpy import ifcopenshell.util.file import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index 14035ca91c..8c2ec0e08b 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -18,13 +18,11 @@ from typing import Union -import blf import bmesh import bpy import gpu from bpy.app.handlers import persistent from bpy.types import SpaceView3D -from bpy_extras import view3d_utils from gpu_extras.batch import batch_for_shader from mathutils import Vector diff --git a/src/bonsai/bonsai/bim/module/project/gizmo.py b/src/bonsai/bonsai/bim/module/project/gizmo.py index d91668eaa8..fa8edb8c7e 100644 --- a/src/bonsai/bonsai/bim/module/project/gizmo.py +++ b/src/bonsai/bonsai/bim/module/project/gizmo.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . -import bpy from bpy.types import GizmoGroup from mathutils import Matrix diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 43588584bd..7bffc724e5 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -18,7 +18,6 @@ import datetime import json -import math import logging import os import subprocess @@ -32,7 +31,6 @@ from typing import TYPE_CHECKING, Literal, Union, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.attribute import ifcopenshell.api.nest import ifcopenshell.api.project @@ -50,12 +48,10 @@ import ifcopenshell.util.unit import numpy as np from bpy.app.handlers import persistent from bpy_extras.io_utils import ExportHelper, ImportHelper -from ifcopenshell.geom import ShapeElementType from mathutils import Matrix, Vector import bonsai.bim.handler import bonsai.bim.helper -import bonsai.bim.schema import bonsai.core.project as core import bonsai.tool as tool from bonsai.bim import export_ifc, import_ifc @@ -63,12 +59,11 @@ from bonsai.bim.ifc import IfcStore from bonsai.bim.ui import IFCFileSelector from bonsai.bim import import_ifc from bonsai.bim import export_ifc -from math import radians, degrees +from math import radians from pathlib import Path from collections import defaultdict from mathutils import Vector, Matrix from bpy.app.handlers import persistent -from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.model.decorator import FaceAreaDecorator, PolylineDecorator from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.project.data import LinksData, ProjectLibraryData diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index a34b5726f7..57b153428b 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -16,9 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import math from collections.abc import Generator -from pathlib import Path from typing import TYPE_CHECKING, Literal, Union, assert_never, get_args import bpy @@ -39,7 +37,7 @@ import bonsai.bim.helper import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.project.data import ProjectData, ProjectLibraryData -from bonsai.bim.prop import Attribute, ObjProperty, StrProperty +from bonsai.bim.prop import Attribute, ObjProperty def get_export_schema(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 1836500c54..7029eb7227 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -22,8 +22,6 @@ import os from typing import TYPE_CHECKING import bpy -import math -import ifcopenshell from bpy.types import Menu, Panel, UIList import bonsai.bim diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index a72981c625..dca65f72e6 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -19,7 +19,6 @@ import os import bpy -from bpy.types import WorkSpaceTool import bonsai.tool as tool from bonsai.bim.module.project.data import LinksData diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 3257760531..d7755cf80a 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -32,7 +32,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore if TYPE_CHECKING: - from bonsai.bim.module.pset.prop import AddEditPropertyEntry, RenamePropertyEntry + from bonsai.bim.module.pset.prop import AddEditPropertyEntry class TogglePsetExpansion(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 4b16e15010..1777fa0f94 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -22,13 +22,11 @@ import bpy import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.doc -import ifcopenshell.util.element from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -44,7 +42,7 @@ from bonsai.bim.module.pset.data import ( ObjectPsetsData, PsetsGeneralData, ) -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute psetnames = {} qtonames = {} diff --git a/src/bonsai/bonsai/bim/module/pset_template/data.py b/src/bonsai/bonsai/bim/module/pset_template/data.py index 1725b974f1..0d922fe2ae 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/data.py +++ b/src/bonsai/bonsai/bim/module/pset_template/data.py @@ -16,11 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os -import pathlib from typing import Any -import bpy import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.doc diff --git a/src/bonsai/bonsai/bim/module/pset_template/operator.py b/src/bonsai/bonsai/bim/module/pset_template/operator.py index e956917df3..0ae20ce0ea 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/operator.py +++ b/src/bonsai/bonsai/bim/module/pset_template/operator.py @@ -20,7 +20,6 @@ import os import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.pset_template import bonsai.bim.handler diff --git a/src/bonsai/bonsai/bim/module/pset_template/prop.py b/src/bonsai/bonsai/bim/module/pset_template/prop.py index 9ec31b364f..3b2d6c5d9e 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/prop.py +++ b/src/bonsai/bonsai/bim/module/pset_template/prop.py @@ -16,18 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os from typing import TYPE_CHECKING import bpy -import ifcopenshell import ifcopenshell.util.attribute from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -38,7 +35,6 @@ from ifcopenshell.util.doc import get_attribute_doc import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from bonsai.bim.module.pset_template.data import PsetTemplatesData -from bonsai.bim.prop import Attribute, StrProperty def updatePsetTemplateFiles(self, context): diff --git a/src/bonsai/bonsai/bim/module/pset_template/ui.py b/src/bonsai/bonsai/bim/module/pset_template/ui.py index 6576e8f56c..92cd78e4ba 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/ui.py +++ b/src/bonsai/bonsai/bim/module/pset_template/ui.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.types import Panel import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 572c65e258..6aa10c9beb 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -18,7 +18,6 @@ import bpy import ifcopenshell -import ifcopenshell.api from ifcopenshell.util.profiler import Profiler import bonsai.core.qto as core diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index 9952285d30..7ffbfcdfa7 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -22,18 +22,12 @@ import bpy import ifc5d.qto from bpy.props import ( BoolProperty, - CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup import bonsai.tool as tool -from bonsai.bim.prop import Attribute, StrProperty CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = [] diff --git a/src/bonsai/bonsai/bim/module/resource/prop.py b/src/bonsai/bonsai/bim/module/resource/prop.py index 4c6828e56c..9f2c5ba85c 100644 --- a/src/bonsai/bonsai/bim/module/resource/prop.py +++ b/src/bonsai/bonsai/bim/module/resource/prop.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Literal, get_args import bpy -import ifcopenshell.api import ifcopenshell.api.resource import ifcopenshell.util.resource from bpy.props import ( @@ -27,9 +26,7 @@ from bpy.props import ( CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 7adf7b5fb9..c1988c8b82 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -19,7 +19,6 @@ from collections import defaultdict from typing import Union -import bpy import ifcopenshell.util.attribute import ifcopenshell.util.element import ifcopenshell.util.schema diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index ac766172bd..159f157d44 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -22,7 +22,6 @@ import bmesh import bpy import idprop import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.material import ifcopenshell.api.pset @@ -34,7 +33,6 @@ import ifcopenshell.util.type import ifcopenshell.util.unit from mathutils import Vector -import bonsai.bim.handler import bonsai.bim.module.root.prop as root_prop import bonsai.core.geometry import bonsai.core.root as core diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py index 854eb6d0f0..35b7b9f403 100644 --- a/src/bonsai/bonsai/bim/module/root/prop.py +++ b/src/bonsai/bonsai/bim/module/root/prop.py @@ -19,17 +19,10 @@ from typing import TYPE_CHECKING, Union import bpy -import ifcopenshell import ifcopenshell.util.element -import ifcopenshell.util.schema import ifcopenshell.util.type from bpy.props import ( - BoolProperty, - CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, StringProperty, ) diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 72eadb4e39..f9eae350e4 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy from bpy.types import Panel import bonsai.bim.module.root.prop as root_prop diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index d9645fe67a..b7ba62acdb 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -23,7 +23,6 @@ from typing import TYPE_CHECKING, Any, Literal, assert_never, get_args import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.group import ifcopenshell.util.element import ifcopenshell.util.selector @@ -31,13 +30,10 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) -from bpy.types import Operator, PropertyGroup +from bpy.types import Operator from natsort import natsorted import bonsai.core.search as core diff --git a/src/bonsai/bonsai/bim/module/search/prop.py b/src/bonsai/bonsai/bim/module/search/prop.py index e3403b5abe..0f45112420 100644 --- a/src/bonsai/bonsai/bim/module/search/prop.py +++ b/src/bonsai/bonsai/bim/module/search/prop.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Literal, get_args import bpy -import ifcopenshell.util.schema from bpy.props import ( BoolProperty, CollectionProperty, @@ -27,7 +26,6 @@ from bpy.props import ( FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -40,8 +38,6 @@ from bonsai.bim.module.search.data import ( ) from bonsai.bim.prop import BIMFilterGroup, ObjProperty -from . import operator, prop, ui - def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SelectSimilarData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/sequence/data.py b/src/bonsai/bonsai/bim/module/sequence/data.py index 7ea6050ca3..20027281eb 100644 --- a/src/bonsai/bonsai/bim/module/sequence/data.py +++ b/src/bonsai/bonsai/bim/module/sequence/data.py @@ -20,7 +20,6 @@ import json from typing import Any import bpy -import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.date from ifcopenshell.util.doc import get_predefined_type_doc diff --git a/src/bonsai/bonsai/bim/module/sequence/helper.py b/src/bonsai/bonsai/bim/module/sequence/helper.py index 50d2367144..9e16e38402 100644 --- a/src/bonsai/bonsai/bim/module/sequence/helper.py +++ b/src/bonsai/bonsai/bim/module/sequence/helper.py @@ -23,7 +23,6 @@ from typing import Any, Union import bpy import ifcopenshell.util.date -import isodate from dateutil import parser from bonsai.bim.prop import ISODuration diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index 6a5937ac45..abb1aa9f0c 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -16,14 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import TYPE_CHECKING, Literal, Union, get_args +from typing import TYPE_CHECKING, Literal, get_args import bpy -import ifcopenshell.api import ifcopenshell.api.sequence -import ifcopenshell.util.attribute import ifcopenshell.util.date -import isodate from bpy.props import ( BoolProperty, CollectionProperty, @@ -31,7 +28,6 @@ from bpy.props import ( FloatProperty, FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index 0ecd772276..f758e89ef5 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any, Optional import bpy -import ifcopenshell import isodate from bpy.types import Panel, UIList diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py index 8d66d4e8a6..047a9ff555 100644 --- a/src/bonsai/bonsai/bim/module/spatial/decorator.py +++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import blf -import bmesh import gpu from bpy.types import SpaceView3D from bpy_extras.view3d_utils import location_3d_to_region_2d diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 3d3d2e74f4..b0d393b265 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -19,15 +19,11 @@ from typing import TYPE_CHECKING, Literal, Union import bpy -import ifcopenshell import ifcopenshell.api.attribute -import ifcopenshell.util.unit from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index edb3ab18ae..500dce4371 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -16,13 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json from math import degrees -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal import bpy -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.group import ifcopenshell.api.structural diff --git a/src/bonsai/bonsai/bim/module/structural/prop.py b/src/bonsai/bonsai/bim/module/structural/prop.py index 24d6ead470..e1e658ce7a 100644 --- a/src/bonsai/bonsai/bim/module/structural/prop.py +++ b/src/bonsai/bonsai/bim/module/structural/prop.py @@ -25,21 +25,19 @@ from bpy.props import ( CollectionProperty, EnumProperty, FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, ) from bpy.types import PropertyGroup -import bonsai.tool as tool from bonsai.bim.module.structural.data import ( BoundaryConditionsData, LoadGroupDecorationData, StructuralLoadCasesData, StructuralLoadsData, ) -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_load_groups_to_show(self: "BIMStructuralProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index b1660dfd4c..5d14bbd248 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -24,7 +24,6 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, FloatVectorProperty, IntProperty, PointerProperty, diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index c5d8897cc8..65faca50d5 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -19,7 +19,6 @@ from typing import Any, Union import bpy -import ifcopenshell import ifcopenshell.util.schema import ifcopenshell.util.system import ifcopenshell.util.unit diff --git a/src/bonsai/bonsai/bim/module/system/decorator.py b/src/bonsai/bonsai/bim/module/system/decorator.py index f2448ba6e6..13ac7519f1 100644 --- a/src/bonsai/bonsai/bim/module/system/decorator.py +++ b/src/bonsai/bonsai/bim/module/system/decorator.py @@ -16,19 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from math import cos, radians, sin import bmesh import bpy import gpu -import ifcopenshell from bpy.app.handlers import persistent from bpy.types import SpaceView3D from gpu_extras.batch import batch_for_shader -from mathutils import Matrix, Vector import bonsai.tool as tool -from bonsai.bim.module.system.data import SystemDecorationData ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index d365964968..e1f6f5e9e4 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api import ifcopenshell.api.attribute import ifcopenshell.api.system import ifcopenshell.util.system diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index a40f50a90e..a03b2a0cfb 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -23,10 +23,7 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup @@ -35,7 +32,7 @@ import bonsai.bim.handler import bonsai.bim.module.system.decorator as decorator import bonsai.tool as tool from bonsai.bim.module.system.data import SystemData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/tester/data.py b/src/bonsai/bonsai/bim/module/tester/data.py index 1d1a1c4901..8535d879c6 100644 --- a/src/bonsai/bonsai/bim/module/tester/data.py +++ b/src/bonsai/bonsai/bim/module/tester/data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import bpy import ifctester.reporter import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index 6cb7999184..c8485d3339 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -31,7 +31,6 @@ from typing import Union import bpy import ifcopenshell -import ifctester import ifctester.ids import ifctester.reporter import socketio diff --git a/src/bonsai/bonsai/bim/module/tester/prop.py b/src/bonsai/bonsai/bim/module/tester/prop.py index 4beb07c533..1bfa32c1eb 100644 --- a/src/bonsai/bonsai/bim/module/tester/prop.py +++ b/src/bonsai/bonsai/bim/module/tester/prop.py @@ -22,9 +22,6 @@ import bpy from bpy.props import ( BoolProperty, CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, PointerProperty, StringProperty, @@ -32,7 +29,7 @@ from bpy.props import ( from bpy.types import PropertyGroup from bonsai.bim.module.tester.data import TesterData -from bonsai.bim.prop import MultipleFileSelect, StrProperty +from bonsai.bim.prop import MultipleFileSelect def update_active_specification_index(self: "IfcTesterProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 8d24989f12..4a6ce053fc 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -18,16 +18,11 @@ from typing import TYPE_CHECKING -import bmesh import bpy -import ifcopenshell.api import ifcopenshell.api.attribute import ifcopenshell.api.type import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.schema -import ifcopenshell.util.type -import ifcopenshell.util.unit import bonsai.bim.helper import bonsai.core.geometry diff --git a/src/bonsai/bonsai/bim/module/type/prop.py b/src/bonsai/bonsai/bim/module/type/prop.py index 13141a7947..d624fd59a0 100644 --- a/src/bonsai/bonsai/bim/module/type/prop.py +++ b/src/bonsai/bonsai/bim/module/type/prop.py @@ -20,16 +20,11 @@ from typing import TYPE_CHECKING, Union import bpy import ifcopenshell.util.element -import ifcopenshell.util.type from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/unit/data.py b/src/bonsai/bonsai/bim/module/unit/data.py index fc51ef3710..6d83bb4e16 100644 --- a/src/bonsai/bonsai/bim/module/unit/data.py +++ b/src/bonsai/bonsai/bim/module/unit/data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import ifcopenshell import ifcopenshell.util.attribute import ifcopenshell.util.schema import ifcopenshell.util.unit diff --git a/src/bonsai/bonsai/bim/module/unit/operator.py b/src/bonsai/bonsai/bim/module/unit/operator.py index da2c7edc1d..f0cee9a6be 100644 --- a/src/bonsai/bonsai/bim/module/unit/operator.py +++ b/src/bonsai/bonsai/bim/module/unit/operator.py @@ -19,8 +19,6 @@ from typing import TYPE_CHECKING import bpy -import ifcopenshell.api -import ifcopenshell.util.unit import bonsai.core.unit as core import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index 02fe7ac803..60639ac3a1 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -23,16 +23,13 @@ from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, StringProperty, ) from bpy.types import PropertyGroup from bonsai.bim.module.unit.data import UnitsData -from bonsai.bim.prop import Attribute, StrProperty +from bonsai.bim.prop import Attribute def get_unit_classes(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/void/data.py b/src/bonsai/bonsai/bim/module/void/data.py index 6d1fcc12a2..0253140651 100644 --- a/src/bonsai/bonsai/bim/module/void/data.py +++ b/src/bonsai/bonsai/bim/module/void/data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from collections.abc import Generator from typing import Any, Union import bpy diff --git a/src/bonsai/bonsai/bim/module/web/data.py b/src/bonsai/bonsai/bim/module/web/data.py index e74b7fa0b6..652d5f7169 100644 --- a/src/bonsai/bonsai/bim/module/web/data.py +++ b/src/bonsai/bonsai/bim/module/web/data.py @@ -18,8 +18,6 @@ import os -import bpy - import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/web/operator.py b/src/bonsai/bonsai/bim/module/web/operator.py index 8efd15494e..a2910b9805 100644 --- a/src/bonsai/bonsai/bim/module/web/operator.py +++ b/src/bonsai/bonsai/bim/module/web/operator.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os import bpy diff --git a/src/bonsai/bonsai/bim/module/web/prop.py b/src/bonsai/bonsai/bim/module/web/prop.py index 1538454270..42bd272578 100644 --- a/src/bonsai/bonsai/bim/module/web/prop.py +++ b/src/bonsai/bonsai/bim/module/web/prop.py @@ -18,16 +18,9 @@ from typing import TYPE_CHECKING -import bpy from bpy.props import ( BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, IntProperty, - PointerProperty, - StringProperty, ) from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 4e93c1c690..61b916965e 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -21,8 +21,6 @@ import os from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args import bpy -import ifcopenshell -import ifcopenshell.util.pset import ifcopenshell.util.unit from bpy.props import ( BoolProperty, @@ -35,17 +33,9 @@ from bpy.props import ( StringProperty, ) from bpy.types import PropertyGroup -from ifcopenshell.util.doc import ( - get_attribute_doc, - get_entity_doc, - get_predefined_type_doc, - get_property_doc, - get_property_set_doc, -) import bonsai.bim import bonsai.bim.handler -import bonsai.bim.schema import bonsai.tool as tool cwd = os.path.dirname(os.path.realpath(__file__)) diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 26c06c5b8d..ad1c14787d 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -24,10 +24,9 @@ from typing import TYPE_CHECKING, Literal, Optional import bpy import platformdirs -from bpy.props import BoolProperty, IntProperty, StringProperty +from bpy.props import BoolProperty, StringProperty from bpy.types import Panel from ifcopenshell.util.doc import ( - get_attribute_doc, get_entity_doc, get_property_set_doc, get_type_doc, @@ -38,7 +37,6 @@ from natsort import natsorted import bonsai.bim import bonsai.bim.helper import bonsai.tool as tool -from bonsai import get_debug_info from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty from bonsai.bim.module.model.prop import ( BIMDoorProperties, @@ -57,7 +55,6 @@ from bonsai.bim.module.model.ui import ( from bonsai.bim.module.pset.prop import IfcProperty from bonsai.bim.prop import Attribute -from . import ifc if TYPE_CHECKING: from bonsai.bim.module.project.prop import BIMProjectProperties diff --git a/src/bonsai/bonsai/core/attribute.py b/src/bonsai/bonsai/core/attribute.py index 2bc2340cd8..803cc353bf 100644 --- a/src/bonsai/bonsai/core/attribute.py +++ b/src/bonsai/bonsai/core/attribute.py @@ -21,8 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/brick.py b/src/bonsai/bonsai/core/brick.py index a21c6d0656..95b673f88b 100644 --- a/src/bonsai/bonsai/core/brick.py +++ b/src/bonsai/bonsai/core/brick.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/bsdd.py b/src/bonsai/bonsai/core/bsdd.py index 1cbf610ea7..55268c131f 100644 --- a/src/bonsai/bonsai/core/bsdd.py +++ b/src/bonsai/bonsai/core/bsdd.py @@ -18,12 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import bsdd - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/context.py b/src/bonsai/bonsai/core/context.py index 49815881aa..7ea77fc2e8 100644 --- a/src/bonsai/bonsai/core/context.py +++ b/src/bonsai/bonsai/core/context.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index 6cd2d916c3..66067cf36e 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal, Optional, Union if TYPE_CHECKING: - import bpy import ifcopenshell import ifcopenshell.util.cost diff --git a/src/bonsai/bonsai/core/covering.py b/src/bonsai/bonsai/core/covering.py index c308c861fc..a8c7a73f25 100644 --- a/src/bonsai/bonsai/core/covering.py +++ b/src/bonsai/bonsai/core/covering.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/debug.py b/src/bonsai/bonsai/core/debug.py index b79bd4f0e7..3702a9cca3 100644 --- a/src/bonsai/bonsai/core/debug.py +++ b/src/bonsai/bonsai/core/debug.py @@ -19,11 +19,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index e38fc5d1e1..b7fe4ef2e5 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import ifcopenshell diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 42b0f77fbb..953f57e7f4 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -19,7 +19,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Literal, Optional, Union +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/georeference.py b/src/bonsai/bonsai/core/georeference.py index 442c928b4e..f594f41416 100644 --- a/src/bonsai/bonsai/core/georeference.py +++ b/src/bonsai/bonsai/core/georeference.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/ifcgit.py b/src/bonsai/bonsai/core/ifcgit.py index 703013e266..fb23652ebc 100644 --- a/src/bonsai/bonsai/core/ifcgit.py +++ b/src/bonsai/bonsai/core/ifcgit.py @@ -19,12 +19,11 @@ from __future__ import annotations import platform -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import bpy import git - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/library.py b/src/bonsai/bonsai/core/library.py index e6c58bf0f7..e006db797e 100644 --- a/src/bonsai/bonsai/core/library.py +++ b/src/bonsai/bonsai/core/library.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/misc.py b/src/bonsai/bonsai/core/misc.py index 297b81eac8..a5f0deb2f3 100644 --- a/src/bonsai/bonsai/core/misc.py +++ b/src/bonsai/bonsai/core/misc.py @@ -18,11 +18,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index 6edce4d100..7b17d5de5d 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Literal, Optional if TYPE_CHECKING: import bpy - import ifcopenshell from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/nest.py b/src/bonsai/bonsai/core/nest.py index 4ab491666b..ffa594e731 100644 --- a/src/bonsai/bonsai/core/nest.py +++ b/src/bonsai/bonsai/core/nest.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/owner.py b/src/bonsai/bonsai/core/owner.py index 90383e7885..aeecb93a64 100644 --- a/src/bonsai/bonsai/core/owner.py +++ b/src/bonsai/bonsai/core/owner.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell from ifcopenshell.api.owner.add_actor import ACTOR_TYPE from ifcopenshell.api.owner.add_address import ADDRESS_TYPE diff --git a/src/bonsai/bonsai/core/patch.py b/src/bonsai/bonsai/core/patch.py index 4acee34494..1ea0a71d4a 100644 --- a/src/bonsai/bonsai/core/patch.py +++ b/src/bonsai/bonsai/core/patch.py @@ -19,11 +19,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/profile.py b/src/bonsai/bonsai/core/profile.py index e102360062..77f64c4606 100644 --- a/src/bonsai/bonsai/core/profile.py +++ b/src/bonsai/bonsai/core/profile.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/project.py b/src/bonsai/bonsai/core/project.py index 18dbd3fab2..0e4b044b5e 100644 --- a/src/bonsai/bonsai/core/project.py +++ b/src/bonsai/bonsai/core/project.py @@ -21,8 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/pset.py b/src/bonsai/bonsai/core/pset.py index 6af9f1e1ee..62517b7ab2 100644 --- a/src/bonsai/bonsai/core/pset.py +++ b/src/bonsai/bonsai/core/pset.py @@ -19,7 +19,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/resource.py b/src/bonsai/bonsai/core/resource.py index cab03b7ebc..63f2e02b90 100644 --- a/src/bonsai/bonsai/core/resource.py +++ b/src/bonsai/bonsai/core/resource.py @@ -24,7 +24,6 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/search.py b/src/bonsai/bonsai/core/search.py index cc16fb56f6..fb4771d92b 100644 --- a/src/bonsai/bonsai/core/search.py +++ b/src/bonsai/bonsai/core/search.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/sequence.py b/src/bonsai/bonsai/core/sequence.py index d62b4325ad..4e1ead7d08 100644 --- a/src/bonsai/bonsai/core/sequence.py +++ b/src/bonsai/bonsai/core/sequence.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index c464f84766..5086a8a872 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union, assert_never +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/structural.py b/src/bonsai/bonsai/core/structural.py index c3f787905a..fb5a28fe24 100644 --- a/src/bonsai/bonsai/core/structural.py +++ b/src/bonsai/bonsai/core/structural.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/style.py b/src/bonsai/bonsai/core/style.py index 20fd98509c..04ee971f97 100644 --- a/src/bonsai/bonsai/core/style.py +++ b/src/bonsai/bonsai/core/style.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/core/system.py b/src/bonsai/bonsai/core/system.py index 418c472645..f0a3ad0380 100644 --- a/src/bonsai/bonsai/core/system.py +++ b/src/bonsai/bonsai/core/system.py @@ -18,10 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/type.py b/src/bonsai/bonsai/core/type.py index a203b9ff76..cf8f3effbe 100644 --- a/src/bonsai/bonsai/core/type.py +++ b/src/bonsai/bonsai/core/type.py @@ -18,12 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional - -import bonsai.core.geometry +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/unit.py b/src/bonsai/bonsai/core/unit.py index 863821e6f5..c0ecc46fcb 100644 --- a/src/bonsai/bonsai/core/unit.py +++ b/src/bonsai/bonsai/core/unit.py @@ -19,10 +19,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/core/web.py b/src/bonsai/bonsai/core/web.py index b0d9222460..81b9e8a90c 100644 --- a/src/bonsai/bonsai/core/web.py +++ b/src/bonsai/bonsai/core/web.py @@ -18,11 +18,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING if TYPE_CHECKING: - import bpy - import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py index 68fbf0e381..06e498e8be 100644 --- a/src/bonsai/bonsai/tool/__init__.py +++ b/src/bonsai/bonsai/tool/__init__.py @@ -15,6 +15,9 @@ # # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +# +# Ignore unused imports. +# ruff: noqa: F401 from bonsai.tool.aggregate import Aggregate from bonsai.tool.attribute import Attribute diff --git a/src/bonsai/bonsai/tool/attribute.py b/src/bonsai/bonsai/tool/attribute.py index 426283ac9e..ce5cdba030 100644 --- a/src/bonsai/bonsai/tool/attribute.py +++ b/src/bonsai/bonsai/tool/attribute.py @@ -21,11 +21,8 @@ from __future__ import annotations from typing import ( TYPE_CHECKING, Any, - Literal, - Optional, TypeVar, Union, - assert_never, ) import bpy @@ -37,7 +34,6 @@ import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.attribute.prop import ( - BIMAttributeProperties, BIMExplorerProperties, ) diff --git a/src/bonsai/bonsai/tool/bcf.py b/src/bonsai/bonsai/tool/bcf.py index c98cfeb4e0..7be0f97a5b 100644 --- a/src/bonsai/bonsai/tool/bcf.py +++ b/src/bonsai/bonsai/tool/bcf.py @@ -33,7 +33,6 @@ import bcf.v3.topic import bpy import bonsai.core.tool -import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.bcf.prop import BCFProperties diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 9471cad28c..63c876dae1 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -74,7 +74,7 @@ if TYPE_CHECKING: BIMSolarProperties, RadianceExporterProperties, ) - from bonsai.bim.prop import BIMObjectProperties, BIMProperties, BIMSnapProperties + from bonsai.bim.prop import BIMObjectProperties, BIMProperties T = TypeVar("T") diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py index 42b80cc288..d585353745 100644 --- a/src/bonsai/bonsai/tool/brick.py +++ b/src/bonsai/bonsai/tool/brick.py @@ -37,13 +37,11 @@ import bonsai.core.tool import bonsai.tool as tool try: - import urllib.parse import brickschema import brickschema.persistent from brickschema.namespaces import REF, A from rdflib import BNode, Literal, Namespace, URIRef - from rdflib.namespace import RDF except: # See #1860 print("Warning: brickschema not available.") diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 4e3d8e1499..13678df15a 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -30,7 +30,6 @@ from __future__ import annotations -import itertools import math import sys from typing import TYPE_CHECKING, Union diff --git a/src/bonsai/bonsai/tool/clash.py b/src/bonsai/bonsai/tool/clash.py index 7c9961646c..fb64e82b49 100644 --- a/src/bonsai/bonsai/tool/clash.py +++ b/src/bonsai/bonsai/tool/clash.py @@ -19,12 +19,9 @@ from __future__ import annotations import json -import os -from contextlib import contextmanager -from typing import TYPE_CHECKING, Literal, Union, get_args +from typing import TYPE_CHECKING, Literal, Union import bpy -import ifcopenshell from ifcclash import ifcclash from ifcclash.ifcclash import ClashSource from mathutils import Vector diff --git a/src/bonsai/bonsai/tool/classification.py b/src/bonsai/bonsai/tool/classification.py index 7b2ebb073e..9264e5bee2 100644 --- a/src/bonsai/bonsai/tool/classification.py +++ b/src/bonsai/bonsai/tool/classification.py @@ -22,10 +22,8 @@ from typing import TYPE_CHECKING, Union, assert_never import bpy import ifcopenshell.api -import ifcopenshell.util.classification import bonsai.core.tool -import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.classification.prop import ( diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 74a55d7dc1..b0aa358629 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import Union import bpy import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/tool/covering.py b/src/bonsai/bonsai/tool/covering.py index 018a39eb46..28e2a80716 100644 --- a/src/bonsai/bonsai/tool/covering.py +++ b/src/bonsai/bonsai/tool/covering.py @@ -21,7 +21,6 @@ from __future__ import annotations from typing import TYPE_CHECKING import bpy -import ifcopenshell import ifcopenshell.util.element import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 864568c835..ca13168942 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -18,8 +18,6 @@ from __future__ import annotations -import collections -import collections.abc import json import logging import math diff --git a/src/bonsai/bonsai/tool/feature.py b/src/bonsai/bonsai/tool/feature.py index d58a8df55c..3a06cbf625 100644 --- a/src/bonsai/bonsai/tool/feature.py +++ b/src/bonsai/bonsai/tool/feature.py @@ -22,11 +22,9 @@ from collections.abc import Iterable from typing import TYPE_CHECKING import bpy -import ifcopenshell import ifcopenshell.api.feature import ifcopenshell.util.representation -import bonsai.bim.helper import bonsai.core.geometry import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 13ec6695d2..e24152d642 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -39,7 +39,6 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.boundary import ifcopenshell.api.geometry import ifcopenshell.api.grid @@ -81,7 +80,7 @@ if TYPE_CHECKING: BIMGeometryProperties, BIMObjectGeometryProperties, ) - from bonsai.bim.prop import Attribute, BIMMeshProperties + from bonsai.bim.prop import BIMMeshProperties class Geometry(bonsai.core.tool.Geometry): diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index 3fa3e51f62..6e39d88fe8 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -22,7 +22,6 @@ import json from typing import TYPE_CHECKING, Any, Literal, Union import bpy -import ifcopenshell import ifcopenshell.api.georeference import ifcopenshell.util.geolocation import ifcopenshell.util.placement diff --git a/src/bonsai/bonsai/tool/group.py b/src/bonsai/bonsai/tool/group.py index f0f3664f6f..3e221b57aa 100644 --- a/src/bonsai/bonsai/tool/group.py +++ b/src/bonsai/bonsai/tool/group.py @@ -25,7 +25,6 @@ import bpy import ifcopenshell from natsort import natsorted -import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/layer.py b/src/bonsai/bonsai/tool/layer.py index 396cc13605..a49bf8bf05 100644 --- a/src/bonsai/bonsai/tool/layer.py +++ b/src/bonsai/bonsai/tool/layer.py @@ -23,7 +23,6 @@ from typing import TYPE_CHECKING import bpy import bonsai.core.tool -import bonsai.tool as tool if TYPE_CHECKING: from bonsai.bim.module.layer.prop import BIMLayerProperties diff --git a/src/bonsai/bonsai/tool/material.py b/src/bonsai/bonsai/tool/material.py index 6f862100e2..e09462a8c2 100644 --- a/src/bonsai/bonsai/tool/material.py +++ b/src/bonsai/bonsai/tool/material.py @@ -38,7 +38,6 @@ if TYPE_CHECKING: BIMMaterialProperties, BIMObjectMaterialProperties, ) - from bonsai.bim.module.material.prop import Material as MaterialItem class Material(bonsai.core.tool.Material): diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 48b106fd74..23c0071b1b 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -18,7 +18,6 @@ from __future__ import annotations -import collections import collections.abc import json from collections.abc import Iterable, Sequence @@ -38,7 +37,6 @@ from typing import ( import bmesh import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.geometry import ifcopenshell.api.grid import ifcopenshell.api.pset diff --git a/src/bonsai/bonsai/tool/owner.py b/src/bonsai/bonsai/tool/owner.py index 9ed0bda219..9f8cf4ab7c 100644 --- a/src/bonsai/bonsai/tool/owner.py +++ b/src/bonsai/bonsai/tool/owner.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, Union, assert_never +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 5fbc909910..5a91fc79b8 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -18,12 +18,10 @@ import math from dataclasses import dataclass, field -from math import cos, degrees, radians, sin, tan +from math import radians from typing import Literal, Optional, Union -import bmesh import bpy -import ifcopenshell import ifcopenshell.util.unit from lark import Lark, Transformer from mathutils import Matrix, Vector diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index 99d2164a32..4c7c2fc448 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -25,9 +25,6 @@ import ifcopenshell import ifcopenshell.api.profile import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W -import ifcopenshell.util.element -import ifcopenshell.util.placement -import ifcopenshell.util.representation import ifcopenshell.util.shape import ifcopenshell.util.unit import numpy as np diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index fc1e4d5e77..6e1c9b2186 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -20,7 +20,6 @@ from __future__ import annotations import os import json -import math import shutil import numpy as np from collections import defaultdict @@ -33,7 +32,6 @@ import ifcopenshell import ifcopenshell.api.document import ifcopenshell.util.element import ifcopenshell.util.representation -import ifcopenshell.util.unit from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES import bonsai.bim.schema diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index 8eabcd155d..2e2fc4383c 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any, Literal, Union, assert_never import bpy import ifcopenshell -import ifcopenshell.api.pset import ifcopenshell.util.attribute import ifcopenshell.util.element diff --git a/src/bonsai/bonsai/tool/pset_template.py b/src/bonsai/bonsai/tool/pset_template.py index e4c1c3dbe1..fea5362d4e 100644 --- a/src/bonsai/bonsai/tool/pset_template.py +++ b/src/bonsai/bonsai/tool/pset_template.py @@ -24,8 +24,6 @@ from typing import TYPE_CHECKING, Literal, final import bpy import ifcopenshell import ifcopenshell.api.pset_template -import ifcopenshell.util.attribute -import ifcopenshell.util.element import bonsai.bim import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/qto.py b/src/bonsai/bonsai/tool/qto.py index 4fd271b283..ec14df6f4c 100644 --- a/src/bonsai/bonsai/tool/qto.py +++ b/src/bonsai/bonsai/tool/qto.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union import bpy import ifcopenshell -import ifcopenshell.util.element import ifcopenshell.util.unit from mathutils import Vector diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index ee06066dd7..d98aaf26ab 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -16,14 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import copy from typing import Union import bmesh import bpy import mathutils import numpy as np -from bpy_extras import view3d_utils from mathutils import Vector import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index be7e350adc..02ddbe9745 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union import bpy import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.feature import ifcopenshell.api.geometry import ifcopenshell.api.root diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 8c5339d438..2afed8741a 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -18,11 +18,8 @@ from __future__ import annotations -import base64 import json -import os import re -import webbrowser from collections.abc import Iterable from datetime import datetime from datetime import time as datetime_time @@ -32,14 +29,12 @@ import bpy import ifcopenshell import ifcopenshell.api.group import ifcopenshell.api.sequence -import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.date import ifcopenshell.util.element import ifcopenshell.util.selector import ifcopenshell.util.sequence import isodate import mathutils -import pystache from dateutil import parser from mathutils import Color diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 0dc11e86b0..02f484d388 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -23,10 +23,7 @@ from typing import TYPE_CHECKING, Any, Union import bmesh import bpy -import ifcopenshell import ifcopenshell.util.unit -import mathutils -from lark import Lark, Transformer from mathutils import Matrix, Vector import bonsai.core.tool diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index d38d895c22..b185b9ab59 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -49,7 +49,6 @@ import bonsai.core.root import bonsai.core.spatial import bonsai.core.tool import bonsai.core.type -import bonsai.core.unit import bonsai.tool as tool if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index 78cb19680e..8db3ed30fe 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -26,7 +26,6 @@ import ifcopenshell import ifcopenshell.api.style import ifcopenshell.util.element import ifcopenshell.util.representation -import numpy as np from mathutils import Color import bonsai.bim.helper diff --git a/src/bonsai/bonsai/tool/surveyor.py b/src/bonsai/bonsai/tool/surveyor.py index f59d9cd7da..42fe540c63 100644 --- a/src/bonsai/bonsai/tool/surveyor.py +++ b/src/bonsai/bonsai/tool/surveyor.py @@ -17,12 +17,10 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell.api import ifcopenshell.util.geolocation import ifcopenshell.util.unit import numpy as np import numpy.typing as npt -from mathutils import Matrix import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 8b949a36c9..926bceca91 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -28,7 +28,6 @@ import ifcopenshell.api.system import ifcopenshell.util.element import ifcopenshell.util.system from mathutils import Matrix, Vector -from natsort import natsorted import bonsai.bim.helper import bonsai.core.geometry diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index 1221f28be9..5825c6f382 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -20,7 +20,7 @@ from __future__ import annotations import json import math -from typing import TYPE_CHECKING, Any, Literal, Union, assert_never +from typing import TYPE_CHECKING, Any, Literal, Union import bpy import ifcopenshell diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 35811b3819..9ebf211ce8 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -30,7 +30,6 @@ import threading import time import webbrowser from pathlib import Path -from time import sleep from typing import TYPE_CHECKING, Any, Optional, Union import bpy @@ -118,7 +117,6 @@ class Web(bonsai.core.tool.Web): :param port: The port number on which to start the WebSocket server. """ - import addon_utils global ws_process diff --git a/src/bonsai/pyproject.toml b/src/bonsai/pyproject.toml index 3ec2cb8748..398687a715 100644 --- a/src/bonsai/pyproject.toml +++ b/src/bonsai/pyproject.toml @@ -38,3 +38,6 @@ exclude = ["test*"] [tool.ruff] extend = "../../pyproject.toml" +lint.select = [ + "F401", # unused imports +] diff --git a/src/bonsai/scripts/bonsai_translations.py b/src/bonsai/scripts/bonsai_translations.py index 7a0fe86ea9..1597eaabcb 100644 --- a/src/bonsai/scripts/bonsai_translations.py +++ b/src/bonsai/scripts/bonsai_translations.py @@ -5,7 +5,6 @@ try: if not hasattr(bpy, "context"): raise ModuleNotFoundError import addon_utils - import bl_i18n_utils BPY_IS_LOADED = True except ModuleNotFoundError: diff --git a/src/bonsai/scripts/classifications/vbis.py b/src/bonsai/scripts/classifications/vbis.py index 7c4f43b967..71d0af5fe9 100644 --- a/src/bonsai/scripts/classifications/vbis.py +++ b/src/bonsai/scripts/classifications/vbis.py @@ -1,9 +1,6 @@ import csv -import json -import os # import pystache -import subprocess from pathlib import Path import ifcopenshell diff --git a/src/bonsai/scripts/gbxml.py b/src/bonsai/scripts/gbxml.py index 7280c69c2a..7bd8063a15 100644 --- a/src/bonsai/scripts/gbxml.py +++ b/src/bonsai/scripts/gbxml.py @@ -17,14 +17,11 @@ # along with Bonsai. If not, see . import math -import sys import uuid import bpy -import bspy # pyright: ignore[reportMissingImports] # sys.path.append('C:\Program Files\Python37\Lib\site-packages') -import lxml import lxml.etree from bspy import Gbxml # pyright: ignore[reportMissingImports] diff --git a/src/bonsai/scripts/generate_au_library.py b/src/bonsai/scripts/generate_au_library.py index 43f05b45da..91fe224255 100644 --- a/src/bonsai/scripts/generate_au_library.py +++ b/src/bonsai/scripts/generate_au_library.py @@ -20,7 +20,6 @@ # pylint: skip-file import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/generate_entourage_library.py b/src/bonsai/scripts/generate_entourage_library.py index 7435186396..fe7a75856e 100644 --- a/src/bonsai/scripts/generate_entourage_library.py +++ b/src/bonsai/scripts/generate_entourage_library.py @@ -17,10 +17,8 @@ # along with Bonsai. If not, see . import os -from pathlib import Path import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/generate_furniture_library.py b/src/bonsai/scripts/generate_furniture_library.py index 1c1572c854..7b9d411772 100644 --- a/src/bonsai/scripts/generate_furniture_library.py +++ b/src/bonsai/scripts/generate_furniture_library.py @@ -17,7 +17,7 @@ # along with Bonsai. If not, see . from itertools import chain -from math import cos, pi, tan +from math import pi, tan from pathlib import Path from typing import Optional, Union @@ -25,10 +25,8 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry -import ifcopenshell.api.material import ifcopenshell.api.project import ifcopenshell.api.root -import ifcopenshell.api.style import ifcopenshell.api.unit import ifcopenshell.util.element import numpy as np diff --git a/src/bonsai/scripts/generate_landscape_library.py b/src/bonsai/scripts/generate_landscape_library.py index 34d2366fd5..4e6ad3eba1 100644 --- a/src/bonsai/scripts/generate_landscape_library.py +++ b/src/bonsai/scripts/generate_landscape_library.py @@ -20,13 +20,10 @@ import csv import os import random from collections import namedtuple -from itertools import chain -from math import cos, pi, sin, tan -from pathlib import Path +from math import cos, pi, sin from random import uniform import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry @@ -36,7 +33,7 @@ import ifcopenshell.api.style import ifcopenshell.api.unit import ifcopenshell.util.unit from ifcopenshell.util.shape_builder import ShapeBuilder -from mathutils import Matrix, Vector +from mathutils import Vector import bonsai.tool as tool diff --git a/src/bonsai/scripts/generate_site_library.py b/src/bonsai/scripts/generate_site_library.py index 0b0511e9da..d8439287ad 100644 --- a/src/bonsai/scripts/generate_site_library.py +++ b/src/bonsai/scripts/generate_site_library.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/generate_steel_profiles_library.py b/src/bonsai/scripts/generate_steel_profiles_library.py index c54818df4b..255a448127 100644 --- a/src/bonsai/scripts/generate_steel_profiles_library.py +++ b/src/bonsai/scripts/generate_steel_profiles_library.py @@ -19,11 +19,10 @@ # fmt: off # pylint: skip-file -from math import cos, pi +from math import pi from pathlib import Path import boltspy as bolts # pyright: ignore[reportMissingImports] -import ifcopenshell import ifcopenshell.api import ifcopenshell.api.material import ifcopenshell.api.project diff --git a/src/bonsai/scripts/geonodes_modifier_prototype.py b/src/bonsai/scripts/geonodes_modifier_prototype.py index 16370c1ca2..185200942e 100644 --- a/src/bonsai/scripts/geonodes_modifier_prototype.py +++ b/src/bonsai/scripts/geonodes_modifier_prototype.py @@ -21,7 +21,6 @@ import json import bmesh import bpy -import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element diff --git a/src/bonsai/scripts/get_all_qtos.py b/src/bonsai/scripts/get_all_qtos.py index d364115559..8d407120eb 100644 --- a/src/bonsai/scripts/get_all_qtos.py +++ b/src/bonsai/scripts/get_all_qtos.py @@ -7,7 +7,6 @@ from typing import Union import ifc5d import ifcopenshell.util.pset -import ifcopenshell.util.type def order_dict(dictionary): diff --git a/src/bonsai/scripts/obj2ifc-meshlab.py b/src/bonsai/scripts/obj2ifc-meshlab.py index c8292bb0f7..70849e11d9 100644 --- a/src/bonsai/scripts/obj2ifc-meshlab.py +++ b/src/bonsai/scripts/obj2ifc-meshlab.py @@ -21,8 +21,6 @@ import argparse from pathlib import Path -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/obj2ifc.py b/src/bonsai/scripts/obj2ifc.py index 70f3bf7b41..ec5459c4a3 100644 --- a/src/bonsai/scripts/obj2ifc.py +++ b/src/bonsai/scripts/obj2ifc.py @@ -21,8 +21,6 @@ import argparse from pathlib import Path -import ifcopenshell -import ifcopenshell.api import ifcopenshell.api.aggregate import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/scripts/replace_drawing_path.py b/src/bonsai/scripts/replace_drawing_path.py index 500726082f..2dd4296fad 100644 --- a/src/bonsai/scripts/replace_drawing_path.py +++ b/src/bonsai/scripts/replace_drawing_path.py @@ -31,7 +31,6 @@ import os import sys from sys import platform -import ifcopenshell import ifcopenshell.api.pset import ifcopenshell.util.element diff --git a/src/bonsai/scripts/setup_pytest.py b/src/bonsai/scripts/setup_pytest.py index 28dc7565f1..a4e56ca35c 100644 --- a/src/bonsai/scripts/setup_pytest.py +++ b/src/bonsai/scripts/setup_pytest.py @@ -45,10 +45,10 @@ for dep in dependencies: subprocess.check_call(command + [dep]) try: - import pygments - import pytest - import pytest_bdd - import pytest_blender + import pygments # noqa: F401 + import pytest # noqa: F401 + import pytest_bdd # noqa: F401 + import pytest_blender # noqa: F401 print("Test dependency installation was successful!") except Exception as e: diff --git a/src/bonsai/scripts/standalone_drawer.py b/src/bonsai/scripts/standalone_drawer.py index 3eed11b9c9..886887bf32 100644 --- a/src/bonsai/scripts/standalone_drawer.py +++ b/src/bonsai/scripts/standalone_drawer.py @@ -4,7 +4,6 @@ from typing import NamedTuple import ifcopenshell import ifcopenshell.geom -import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element # W.turn_on_detailed_logging() diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 00243c5782..c58e1dc1ec 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -3,7 +3,6 @@ from itertools import cycle from math import radians -import ifcopenshell import ifcopenshell.api.aggregate import ifcopenshell.api.context import ifcopenshell.api.geometry diff --git a/src/bonsai/test/pyproject.toml b/src/bonsai/test/pyproject.toml new file mode 100644 index 0000000000..7567fe8627 --- /dev/null +++ b/src/bonsai/test/pyproject.toml @@ -0,0 +1,5 @@ +[tool.ruff] +extend = "../pyproject.toml" +lint.ignore = [ + "F401", # unused imports +] diff --git a/win/build-all-win.py b/win/build-all-win.py index 2c0533c253..cfbad3b746 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -28,7 +28,7 @@ ZIP_TEMPLATE = f"{{package_name}}-v{VERSION}-{SHA}-win64.zip" def run(command: list[str]) -> None: print("Running:", command) - subprocess.check_call(command) # nosec B603 + subprocess.check_call(command) def set_env(var_name: str, value: str) -> tuple[str, str | None]: From 333b6210a4068eeac40749fb9964f6197bc18606 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 26 Feb 2026 17:08:36 +0500 Subject: [PATCH 090/131] black . --- src/bonsai/bonsai/bim/module/bsdd/prop.py | 4 +-- src/bonsai/bonsai/bim/module/bsdd/ui.py | 1 + .../bonsai/bim/module/drawing/operator.py | 3 +- .../bonsai/bim/module/geometry/operator.py | 2 +- src/bonsai/bonsai/bim/ui.py | 3 +- src/bonsai/bonsai/tool/blender.py | 8 +++-- src/bonsai/bonsai/tool/georeference.py | 4 ++- src/bonsai/test/tool/test_drawing.py | 2 +- .../_create_offset_curve_representation.py | 2 +- .../ifcopenshell/express/bootstrap.py | 7 ++-- .../ifcopenshell/express/schema_class.py | 14 +++++--- .../test/util/test_selector.py | 32 +++++++++---------- 12 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index d595ad8018..5103358b6e 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -160,7 +160,7 @@ class BIMBSDDProperties(PropertyGroup): default=False, ) classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) - + if TYPE_CHECKING: active_dictionary: str active_dictionary: str @@ -179,7 +179,7 @@ class BIMBSDDProperties(PropertyGroup): should_filter_ifc_class: bool use_only_ifc_properties: bool classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset] - + @property def active_class(self) -> Union[BSDDClassification, None]: return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index) diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index f49f48abb9..c6966c01d8 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -83,6 +83,7 @@ class BIM_PT_bsdd(Panel): row = self.layout.row() row.operator("bim.load_bsdd_dictionaries") + class BIM_UL_bsdd_dictionaries(UIList): def draw_item( self, diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 55804498ec..66a1482acc 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3871,7 +3871,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): layout.prop(self, "x_length") layout.prop(self, "y_length") - def _execute(self, context): project_props = tool.Project.get_project_props() project_props.load_indexed_maps = self.show_texture_solid_mode @@ -3900,7 +3899,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) hx = self.x_length * 0.5 / unit_scale hy = self.y_length * 0.5 / unit_scale - verts = [(-hx, -hy, 0.0), ( hx, -hy, 0.0), ( hx, hy, 0.0), (-hx, hy, 0.0)] + verts = [(-hx, -hy, 0.0), (hx, -hy, 0.0), (hx, hy, 0.0), (-hx, hy, 0.0)] item = builder.mesh(verts, [[0, 1, 2, 3]]) ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 9ea6ba72f5..f8389cb3af 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -897,7 +897,7 @@ class OverrideDelete(bpy.types.Operator): if not is_valid_data_block: continue - + element = tool.Ifc.get_entity(obj) if element: if tool.Geometry.is_locked(element): diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index ad1c14787d..4cacd49e42 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -658,7 +658,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False ) bsdd_baseurl: StringProperty( - name="bSDD API Base URL", description="Base URL for data dictionary API requests, e.g. https://api.bsdd.buildingsmart.org/api/", + name="bSDD API Base URL", + description="Base URL for data dictionary API requests, e.g. https://api.bsdd.buildingsmart.org/api/", default="https://api.bsdd.buildingsmart.org/api/", ) should_disable_undo_on_save: BoolProperty( diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 63c876dae1..9c01ce8c20 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1581,8 +1581,12 @@ class Blender(bonsai.core.tool.Blender): default_scale = default_dpi * default_pixel_size system = bpy.context.preferences.system system_scale = system.dpi * system.pixel_size - return (system_scale / default_scale) * base_size *platform_scale * tool.Blender.get_addon_preferences().decorator_font_scale - + return ( + (system_scale / default_scale) + * base_size + * platform_scale + * tool.Blender.get_addon_preferences().decorator_font_scale + ) @classmethod def apply_transform_as_local(cls, obj: bpy.types.Object) -> bool: diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index 6e39d88fe8..0c47002a36 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -316,7 +316,9 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def global2local(cls, matrix, is_specified_in_map_units: bool) -> tuple[float, float, float]: - matrix = ifcopenshell.util.geolocation.auto_global2local(tool.Ifc.get(), matrix, is_specified_in_map_units=is_specified_in_map_units) + matrix = ifcopenshell.util.geolocation.auto_global2local( + tool.Ifc.get(), matrix, is_specified_in_map_units=is_specified_in_map_units + ) props = cls.get_georeference_props() if props.has_blender_offset: matrix = ifcopenshell.util.geolocation.global2local( diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 69d7fbde92..3fa2e68cae 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -162,7 +162,7 @@ class TestEditTextLiterals(NewFile): context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation") item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left") builder = ShapeBuilder(tool.Ifc.get()) - polyline = builder.polyline([(0.,0.,0.), (1.,0.,0.)]) + polyline = builder.polyline([(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]) representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item, polyline]) element.Representation.Representations = [representation] tool.Ifc.link(element, obj) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py index a5675a4dc2..28a9f6ef49 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/_create_offset_curve_representation.py @@ -36,7 +36,7 @@ def _create_offset_curve_representation( expected_type = "IfcAlignment" if not alignment.is_a(expected_type): raise TypeError(f"Expected {expected_type} but got {alignment.is_a()}") - + expected_type = "IfcPointByDistanceExpression" for offset in offsets: if not offset.is_a(expected_type): diff --git a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py index ef3c3c3ef0..578e561e79 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py +++ b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py @@ -217,7 +217,8 @@ for id in to_emit: statements.append("%s << %s" % (id, stmt)) if __name__ == "__main__": - print(r""" + print( + r""" # This file is generated by IfcOpenShell ifcexpressparser bootstrap.py from __future__ import annotations @@ -256,4 +257,6 @@ if __name__ == "__main__": mdl = importlib.import_module(output) mdl.Generator(m).emit() sys.stdout.write(m.schema.name) -""" % ("\n ".join(statements))) +""" + % ("\n ".join(statements)) + ) diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index d7595ad6cb..e017c03d7a 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -363,18 +363,24 @@ class EarlyBoundCodeWriter: ) ) - self.statements[self.statements.index("{factory_placeholder}")] = """ + self.statements[self.statements.index("{factory_placeholder}")] = ( + """ class %(schema_name)s_instance_factory : public IfcParse::instance_factory { virtual IfcUtil::IfcBaseClass* operator()(const IfcParse::declaration* decl, IfcEntityInstanceData&& data) const { %(instance_mapping)s } }; -""" % locals() +""" + % locals() + ) "" - self.statements[self.statements.index("{string_pool_placeholder}")] = """ + self.statements[self.statements.index("{string_pool_placeholder}")] = ( + """ const std::string strings[] = {%s}; -""" % ",".join(map(lambda s: '"%s"s' % s, self.strings)) +""" + % ",".join(map(lambda s: '"%s"s' % s, self.strings)) + ) def __str__(self): return "\n".join(self.statements) diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 3c94876a66..b8302e0f4a 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -54,7 +54,7 @@ class TestFormat(test.bootstrap.IFC4): def test_number_formatting(self): assert subject.format("round(123, 5)") == "125" assert subject.format('round("123", 5)') == "125" - assert subject.format('round(-123, 5)') == "-125" + assert subject.format("round(-123, 5)") == "-125" assert subject.format("int(123.123)") == "123" assert subject.format("int(123)") == "123" assert subject.format("number(123)") == "123" @@ -79,14 +79,14 @@ class TestFormat(test.bootstrap.IFC4): assert subject.format('imperial_length(3.0, 4, "foot", "foot", False)') == "3' - 0\"" def test_variable_formatting(self): - assert subject.format('{{undefined}}') is None - assert subject.format('upper({{undefined}})') == "NONE" - assert subject.format('int({{undefined}})') == "0" + assert subject.format("{{undefined}}") is None + assert subject.format("upper({{undefined}})") == "NONE" + assert subject.format("int({{undefined}})") == "0" element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - assert subject.format('{{undefined}}', element) is None - assert subject.format('{{class}}', element) == "IfcWall" - assert subject.format('{{ class }}', element) == "IfcWall" - assert subject.format('upper({{ class }})', element) == "IFCWALL" + assert subject.format("{{undefined}}", element) is None + assert subject.format("{{class}}", element) == "IfcWall" + assert subject.format("{{ class }}", element) == "IfcWall" + assert subject.format("upper({{ class }})", element) == "IFCWALL" def test_list_formatting(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") @@ -98,17 +98,17 @@ class TestFormat(test.bootstrap.IFC4): layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material2) layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material3) ifcopenshell.api.material.assign_material(self.file, products=[element], material=material_set) - assert subject.format('{{materials.Name}}', element) == "CON01, CON03, CON02" - assert subject.format('sort({{materials.Name}})', element) == "CON01, CON02, CON03" - assert subject.format('reverse({{materials.Name}})', element) == "CON02, CON03, CON01" + assert subject.format("{{materials.Name}}", element) == "CON01, CON03, CON02" + assert subject.format("sort({{materials.Name}})", element) == "CON01, CON02, CON03" + assert subject.format("reverse({{materials.Name}})", element) == "CON02, CON03, CON01" assert subject.format('join("-", {{materials.Name}})', element) == "CON01-CON03-CON02" def test_expressions(self): - assert subject.format('2+3') == "5" - assert subject.format('-2+3') == "1" - assert subject.format('2-3') == "-1" - assert subject.format('3*2') == "6" - assert subject.format('3/2') == "1.5" + assert subject.format("2+3") == "5" + assert subject.format("-2+3") == "1" + assert subject.format("2-3") == "-1" + assert subject.format("3*2") == "6" + assert subject.format("3/2") == "1.5" class TestGetElementValue(test.bootstrap.IFC4): From f8f47250548ea70a3fd5ad1fb98644be93ea87b9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 19 Feb 2026 16:52:25 +0500 Subject: [PATCH 091/131] build-all - remove wasm cxx flags workaround As issue is now fixed upstream (https://github.com/pyodide/pyodide-build/issues/251) --- nix/build-all.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index 7b1df4b514..f34dcf6978 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -248,15 +248,8 @@ if WASM: # https://github.com/pyodide/pyodide-build/pull/249 WASM_CMAKE_IS_USING_INIT_VARS = get_pyodide_build_version() >= (99, 0, 0) - # pyodide provide empty `CXXFLAGS`, leading to issues using C++ files compiled with `-fexceptions` - # which is used by OCCT. - # https://github.com/pyodide/pyodide-build/issues/251 - side_module_cxx_flags = os.environ.get("SIDE_MODULE_CXXFLAGS", "") - if side_module_cxx_flags.strip(): - print(f"SIDE_MODULE_CXXFLAGS are already passed from pyodide build ('{side_module_cxx_flags}').") - print("Maybe it's time to stop overriding them in the script?") - - os.environ["SIDE_MODULE_CXXFLAGS"] = os.environ["SIDE_MODULE_CFLAGS"] + # 0.31 is required for SIDE_MODULE_CXXFLAGS to be provided. + assert get_pyodide_build_version() >= (0, 31) # Set defaults for missing empty environment variables From fc7d15324fee2c8cb8c673e17d7ac220f4221331 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 19 Feb 2026 20:24:49 +0500 Subject: [PATCH 092/131] build-all - don't use main repo pyproject.toml for wasm builds --- nix/build-all.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nix/build-all.py b/nix/build-all.py index f34dcf6978..139e1dda2a 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -1521,6 +1521,9 @@ 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 + # otherwise the wheel will use version and dependencies from toml, not setup.py. + (REPO_PATH / "pyproject.toml").write_text("") elif USE_CURRENT_PYTHON_VERSION: python_info = sysconfig.get_paths() From aa7710dd743eaef56e2955a315cfb8ad745ee0dd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 20 Feb 2026 10:56:06 +0500 Subject: [PATCH 093/131] cache_dependencies.py - note expected cwd --- pyodide/cache_dependencies.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyodide/cache_dependencies.py b/pyodide/cache_dependencies.py index 5800cba067..b01b552f8c 100644 --- a/pyodide/cache_dependencies.py +++ b/pyodide/cache_dependencies.py @@ -5,6 +5,8 @@ This script is finding common install directory and either packs each folder into a tar.gz archive, if it wasn't packed before, or unpacks existing archives. +Expected to be executed from 'build' directory (e.g. that might contain 'Linux/x86_64/install'). + Usage: python cache_dependencies.py [pack|unpack] """ From 00cd0b76f9d3dfc2829211d98257564d6cb52367 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 20 Feb 2026 13:27:00 +0500 Subject: [PATCH 094/131] .gersemirc - search `src` for definitions To fix errors when parsing custom macro from `src\examples\CMakeLists.txt`, see https://github.com/BlankSpruce/gersemi/issues/105 --- .gersemirc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gersemirc b/.gersemirc index 4d74cb215a..44ff8cc11c 100644 --- a/.gersemirc +++ b/.gersemirc @@ -1,7 +1,8 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/0.24.0/gersemi/configuration.schema.json -# Needed for gersemi to detect custom functions and macros. -definitions: ["./cmake"] +# Gersemi doesn't support autodetection of macros/functions from other files or from the current one +# and requires to explicitly list directories/cmake files that define them. +definitions: ["./cmake", "./src"] disable_formatting: false extensions: [] indent: 4 From 7909997d42bd8f60157554a67e83299cd786721a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Feb 2026 12:42:50 +0500 Subject: [PATCH 095/131] build workflows - reuse cache_dependencies.py --- .github/workflows/build_osx.yml | 8 +++----- .github/workflows/build_pyodide.yml | 4 ++-- .github/workflows/build_rocky.yml | 8 +++----- .github/workflows/build_rocky_arm.yml | 8 +++----- {pyodide => nix}/cache_dependencies.py | 7 ++++++- 5 files changed, 17 insertions(+), 18 deletions(-) rename {pyodide => nix}/cache_dependencies.py (92%) diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index eb680e54c1..6cabc97578 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -49,8 +49,8 @@ jobs: - name: Unpack Dependencies run: | - install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true) - [ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true + cd build + python ../nix/cache_dependencies.py unpack - name: ccache uses: hendrikmuhs/ccache-action@v1.2 @@ -95,9 +95,7 @@ jobs: - name: Pack Dependencies run: | cd build - for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do - test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir"); - done + python ../nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_pyodide.yml b/.github/workflows/build_pyodide.yml index 744bd9e772..d0feb08a8b 100644 --- a/.github/workflows/build_pyodide.yml +++ b/.github/workflows/build_pyodide.yml @@ -26,7 +26,7 @@ jobs: - name: Unpack Dependencies run: | cd ifcopenshell_build - python ../IfcOpenShell/pyodide/cache_dependencies.py unpack + python ../IfcOpenShell/nix/cache_dependencies.py unpack - name: ccache uses: hendrikmuhs/ccache-action@v1.2 @@ -65,7 +65,7 @@ jobs: - name: Pack Dependencies run: | cd ifcopenshell_build - python ../IfcOpenShell/pyodide/cache_dependencies.py pack + python ../IfcOpenShell/nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index 217620987a..3f608ab5f6 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -44,8 +44,8 @@ jobs: - name: Unpack Dependencies run: | - install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true) - [ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true + cd build + python3 ../nix/cache_dependencies.py unpack - name: ccache # TODO: Use tag after 1.2.20 releases. @@ -72,9 +72,7 @@ jobs: - name: Pack Dependencies run: | cd build - for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do - test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir"); - done + python3 ../nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index d195b7b868..e08cf2c78f 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -44,8 +44,8 @@ jobs: - name: Unpack Dependencies run: | - install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true) - [ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true + cd build + python3 ../nix/cache_dependencies.py unpack - name: ccache # TODO: Use tag after 1.2.20 releases. @@ -72,9 +72,7 @@ jobs: - name: Pack Dependencies run: | cd build - for install_dir in $(find $(find . -maxdepth 4 -name install) -mindepth 1 -maxdepth 1 -type d); do - test -f $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz || tar -czf $(dirname "$install_dir")/cache-$(basename "$install_dir").tar.gz -C $(dirname "$install_dir") $(basename "$install_dir"); - done + python3 ../nix/cache_dependencies.py pack - name: Commit and Push Changes to Build Repository run: | diff --git a/pyodide/cache_dependencies.py b/nix/cache_dependencies.py similarity index 92% rename from pyodide/cache_dependencies.py rename to nix/cache_dependencies.py index b01b552f8c..d03bda3dbc 100644 --- a/pyodide/cache_dependencies.py +++ b/nix/cache_dependencies.py @@ -10,6 +10,7 @@ Expected to be executed from 'build' directory (e.g. that might contain 'Linux/x Usage: python cache_dependencies.py [pack|unpack] """ +import platform import sys import tarfile from pathlib import Path @@ -19,7 +20,11 @@ CACHE_PREFIX = "cache-" def get_install_dir() -> Path: - for data in Path.cwd().glob("*/*/install"): + if platform.system() == "Darwin": + pattern = "Darwin/*/*/install" + else: + pattern = "*/*/install" + for data in Path.cwd().glob(pattern): return data raise Exception("No install dir found") From be4806471d6476a8ee75350fa595b62b325bf2e2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 23 Feb 2026 17:56:02 +0500 Subject: [PATCH 096/131] cache_dependencies - use `tar` instead of `tarfile` for archiving --- nix/cache_dependencies.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nix/cache_dependencies.py b/nix/cache_dependencies.py index d03bda3dbc..465d6002f1 100644 --- a/nix/cache_dependencies.py +++ b/nix/cache_dependencies.py @@ -11,6 +11,7 @@ Usage: python cache_dependencies.py [pack|unpack] """ import platform +import subprocess import sys import tarfile from pathlib import Path @@ -29,6 +30,11 @@ def get_install_dir() -> Path: raise Exception("No install dir found") +def run(cmd: str) -> None: + print(f"Running command: `{cmd}`") + subprocess.check_call(cmd, shell=True) + + def pack_dependencies(install_dir: Path) -> None: # Process each install_dir for dependency_path in install_dir.iterdir(): @@ -39,8 +45,8 @@ def pack_dependencies(install_dir: Path) -> None: if tar_path.exists(): print(f"Skipping existing cache: '{tar_path}'") else: - with tarfile.open(tar_path, "w:gz") as tar: - tar.add(dependency_path, arcname=dependency_path.name) + # Python's `tarfile` is 10x slower than `tar` cli, so we use `tar`. + run(f'tar -czf "{tar_path}" -C "{install_dir}" "{dependency_name}"') print(f"Created cache: '{tar_path}'") From fc13bcd055d0c5ffeac4d876a5710199f6da5c2f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 24 Feb 2026 15:00:17 +0500 Subject: [PATCH 097/131] bump ccache-action --- .github/workflows/build_osx.yml | 2 +- .github/workflows/build_pyodide.yml | 2 +- .github/workflows/build_rocky.yml | 3 +-- .github/workflows/build_rocky_arm.yml | 3 +-- .github/workflows/build_win.yml | 3 +-- .github/workflows/ci-ifcopenshell-docker.yml | 2 +- .github/workflows/ci.yml | 5 +---- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index 6cabc97578..0e15cdc830 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -53,7 +53,7 @@ jobs: python ../nix/cache_dependencies.py unpack - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: mac-${{ matrix.arch }} diff --git a/.github/workflows/build_pyodide.yml b/.github/workflows/build_pyodide.yml index d0feb08a8b..cd162cd9ea 100644 --- a/.github/workflows/build_pyodide.yml +++ b/.github/workflows/build_pyodide.yml @@ -29,7 +29,7 @@ jobs: python ../IfcOpenShell/nix/cache_dependencies.py unpack - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: ubuntu-22.04-${{ runner.arch }} diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index 3f608ab5f6..3fcd759877 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -48,8 +48,7 @@ jobs: python3 ../nix/cache_dependencies.py unpack - name: ccache - # TODO: Use tag after 1.2.20 releases. - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index e08cf2c78f..cced443ad8 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -48,8 +48,7 @@ jobs: python3 ../nix/cache_dependencies.py unpack - name: ccache - # TODO: Use tag after 1.2.20 releases. - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: ubuntu-22.04-${{ runner.arch }}-rockylinux9 diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml index ab9d7b6f80..783084c1ae 100644 --- a/.github/workflows/build_win.yml +++ b/.github/workflows/build_win.yml @@ -37,8 +37,7 @@ jobs: } - name: ccache - # TODO: Use tag after 1.2.20 releases. - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: win-${{ matrix.arch }} # Windows ccache needs ~1GB diff --git a/.github/workflows/ci-ifcopenshell-docker.yml b/.github/workflows/ci-ifcopenshell-docker.yml index d5691ecfce..e6668490c5 100644 --- a/.github/workflows/ci-ifcopenshell-docker.yml +++ b/.github/workflows/ci-ifcopenshell-docker.yml @@ -35,7 +35,7 @@ jobs: - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.20 - name: Build ifcopenshell diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65031e83d9..896a179758 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,10 +79,7 @@ jobs: libhdf5-dev libcgal-dev libeigen3-dev - name: ccache - # TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released - # to update hardcoded references to commits in some other workflows. - # Then we can switch back to 1.2 in all actions. - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.20 with: key: ubuntu-22.04-${{ runner.arch }} From 5b86eedb26f04d3950cede9ee46426feea6e1779 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 20 Feb 2026 14:40:37 +0500 Subject: [PATCH 098/131] cmake - fix ifc geom mapping not linking against IfcGeom library --- src/ifcgeom/mapping/CMakeLists.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ifcgeom/mapping/CMakeLists.txt b/src/ifcgeom/mapping/CMakeLists.txt index 4e185b9fa9..6369b98b60 100644 --- a/src/ifcgeom/mapping/CMakeLists.txt +++ b/src/ifcgeom/mapping/CMakeLists.txt @@ -1,12 +1,21 @@ find_package(Eigen3 REQUIRED) +# When using `ENABLE_BUILD_OPTIMIZATIONS``/GL` compilation flags makes resulting mapping libs huge (e.g. 1.5GB each) +# and combining them together to a single IfcGeom won't be possible due to 4GB file size limit. +# So in this case we have to ensure each mapping is built as separate libs instead of .obj files to be linked together. +if(ENABLE_BUILD_OPTIMIZATIONS AND MSVC) + set(mapping_library_type "STATIC") +else() + set(mapping_library_type "OBJECT") +endif() + foreach(schema ${SCHEMA_VERSIONS}) file(GLOB IFCGEOM_I_FILES *.i) file(GLOB IFCGEOM_H_FILES *.h) file(GLOB IFCGEOM_CPP_FILES *.cpp) set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES} ${IFCGEOM_I_FILES}) - add_library(geometry_mapping_ifc${schema} OBJECT ${IFCGEOM_FILES}) + add_library(geometry_mapping_ifc${schema} ${mapping_library_type} ${IFCGEOM_FILES}) set_target_properties( geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}" From cfea3de552018c6e1ae627fbc13d003125c697f5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 26 Feb 2026 16:45:15 +0500 Subject: [PATCH 099/131] cmake - fix msvc warning on linking without /ltcg flag Linking flags were missing for `MODULE` type libraries, example warning: `IfcPythonPYTHON_wrap.obj : MSIL .netmodule or module compiled with /GL found; restarting link with /LTCG; add /LTCG to the link command line to improve linker performance` --- cmake/CMakeLists.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 51ab6c22e1..af28df300c 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -368,12 +368,16 @@ if(ENABLE_BUILD_OPTIMIZATIONS) # Linker # /OPT:REF enables also /OPT:ICF and disables INCREMENTAL - set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF") - + set(LINKER_FLAGS_RELEASE "/LTCG /OPT:REF") # /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx) - set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF") - set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF") - set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF") + set(LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /OPT:NOICF") + + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") + set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") + set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") + set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") else() # GCC-like: Release should use O3 but RelWithDebInfo 02 so enforce 03. Anything other useful that could be added here? set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") From 097c8af7c743639da7598e9967cc10c272804f2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:39:54 +0000 Subject: [PATCH 100/131] Bump rollup from 4.41.1 to 4.59.0 in /src/ifctester/webapp Bumps [rollup](https://github.com/rollup/rollup) from 4.41.1 to 4.59.0. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.41.1...v4.59.0) --- updated-dependencies: - dependency-name: rollup dependency-version: 4.59.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- src/ifctester/webapp/package-lock.json | 253 ++++++++++++++++--------- 1 file changed, 164 insertions(+), 89 deletions(-) diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index e1527f59f3..6ebd556626 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -588,9 +588,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", - "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -602,9 +602,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", - "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -616,9 +616,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", - "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -630,9 +630,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", - "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -644,9 +644,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", - "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -658,9 +658,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", - "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -672,9 +672,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", - "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -686,9 +686,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", - "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -700,9 +700,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", - "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -714,9 +714,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", - "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -727,10 +727,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", - "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -741,10 +741,38 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", - "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -756,9 +784,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", - "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -770,9 +798,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", - "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -784,9 +812,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", - "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -798,9 +826,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", - "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -812,9 +840,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", - "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -825,10 +853,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", - "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -840,9 +896,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", - "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -853,10 +909,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", - "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1270,9 +1340,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/acorn": { @@ -2118,13 +2188,13 @@ } }, "node_modules/rollup": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", - "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2134,26 +2204,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.1", - "@rollup/rollup-android-arm64": "4.41.1", - "@rollup/rollup-darwin-arm64": "4.41.1", - "@rollup/rollup-darwin-x64": "4.41.1", - "@rollup/rollup-freebsd-arm64": "4.41.1", - "@rollup/rollup-freebsd-x64": "4.41.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", - "@rollup/rollup-linux-arm-musleabihf": "4.41.1", - "@rollup/rollup-linux-arm64-gnu": "4.41.1", - "@rollup/rollup-linux-arm64-musl": "4.41.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-musl": "4.41.1", - "@rollup/rollup-linux-s390x-gnu": "4.41.1", - "@rollup/rollup-linux-x64-gnu": "4.41.1", - "@rollup/rollup-linux-x64-musl": "4.41.1", - "@rollup/rollup-win32-arm64-msvc": "4.41.1", - "@rollup/rollup-win32-ia32-msvc": "4.41.1", - "@rollup/rollup-win32-x64-msvc": "4.41.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, From 35886c9f72dbadc67a8eddfdf961fbf81cbce63f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:59:10 +0000 Subject: [PATCH 101/131] Bump svelte from 5.33.10 to 5.53.0 in /src/ifctester/webapp Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.33.10 to 5.53.0. - [Release notes](https://github.com/sveltejs/svelte/releases) - [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.0/packages/svelte) --- updated-dependencies: - dependency-name: svelte dependency-version: 5.53.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- src/ifctester/webapp/package-lock.json | 43 ++++++++++++++++++++------ src/ifctester/webapp/package.json | 2 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index 6ebd556626..2e136aaffa 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -24,7 +24,7 @@ "clsx": "^2.1.1", "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", - "svelte": "^5.28.1", + "svelte": "^5.53.0", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", @@ -37,6 +37,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -543,6 +544,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1345,6 +1356,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -1515,6 +1532,12 @@ "node": ">=8" } }, + "node_modules/devalue": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "license": "MIT" + }, "node_modules/engine.io-client": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", @@ -1616,9 +1639,9 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", - "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", + "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -2752,21 +2775,23 @@ } }, "node_modules/svelte": { - "version": "5.33.10", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.10.tgz", - "integrity": "sha512-/yArPQIBoQS2p86LKnvJywOXkVHeEXnFgrDPSxkEfIAEkykopYuy2bF6UUqHG4IbZlJD6OurLxJT8Kn7kTk9WA==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.0.tgz", + "integrity": "sha512-7dhHkSamGS2vtoBmIW2hRab+gl5Z60alEHZB4910ePqqJNxAWnDAxsofVmlZ2tREmWyHNE+A1nCKwICAquoD2A==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", + "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", + "devalue": "^5.6.3", "esm-env": "^1.2.1", - "esrap": "^1.4.6", + "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/src/ifctester/webapp/package.json b/src/ifctester/webapp/package.json index 4a8abd7897..b97ba2c8b5 100644 --- a/src/ifctester/webapp/package.json +++ b/src/ifctester/webapp/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", - "svelte": "^5.28.1", + "svelte": "^5.53.0", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", From 198e111a9229e02f1b2a842bebc3fe60111e9c1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:45:33 +0000 Subject: [PATCH 102/131] Bump gersemi from 0.25.4 to 0.26.0 Bumps [gersemi](https://github.com/BlankSpruce/gersemi) from 0.25.4 to 0.26.0. - [Release notes](https://github.com/BlankSpruce/gersemi/releases) - [Changelog](https://github.com/BlankSpruce/gersemi/blob/master/CHANGELOG.md) - [Commits](https://github.com/BlankSpruce/gersemi/compare/0.25.4...0.26.0) --- updated-dependencies: - dependency-name: gersemi dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index febd543cf7..b418b3db0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ dependencies = [ "black==26.1.0", "ruff==0.15.1", "poethepoet", - "gersemi==0.25.4", + "gersemi==0.26.0", ] [tool.black] From 58c69f9d35ac975b42417f397ee8afeaf802e07c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:45:29 +0000 Subject: [PATCH 103/131] Bump ruff from 0.15.1 to 0.15.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.1 to 0.15.2. - [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.1...0.15.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.2 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 b418b3db0d..4c4aeb331a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "IfcOpenShell" version = "0.0.0" dependencies = [ "black==26.1.0", - "ruff==0.15.1", + "ruff==0.15.2", "poethepoet", "gersemi==0.26.0", ] From 1c5b825d8ef05ab9d14a15dac12e9eae2f5a37c2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 11:41:56 +0500 Subject: [PATCH 104/131] build-all-win - fix missing compression for Python zip archives Same as 5ebd425, should resolve https://github.com/ifcopenshell/ifcopenshell/issues/7404 --- win/build-all-win.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win/build-all-win.py b/win/build-all-win.py index cfbad3b746..e1bd5cccdf 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -93,7 +93,7 @@ def archive_python_package(python_version: str, python_path: Path) -> None: file.unlink() zip_name = ZIP_TEMPLATE.format(package_name=f"ifcopenshell-python-{python_version_major_minor}") - with ZipFile(OUTPUT_DIR / zip_name, "w") as zipf: + with ZipFile(OUTPUT_DIR / zip_name, "w", compression=zipfile.ZIP_DEFLATED) as zipf: for file in package_path.rglob("*"): arcname = file.relative_to(site_packages) zipf.write(file, arcname=arcname) From 8834a51122a3ee539f935d40f2cf212b8584ab88 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 11:18:35 +0500 Subject: [PATCH 105/131] format cmake files --- cmake/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index af28df300c..bf48427774 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -373,11 +373,15 @@ if(ENABLE_BUILD_OPTIMIZATIONS) set(LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /OPT:NOICF") set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") - set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") + set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO + "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}" + ) set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}") - set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}") + set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO + "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}" + ) else() # GCC-like: Release should use O3 but RelWithDebInfo 02 so enforce 03. Anything other useful that could be added here? set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") From 92c979fbbfdc909845c479798d2332ad000827d4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 11:15:30 +0500 Subject: [PATCH 106/131] black . --- nix/build-all.py | 18 +-- src/bcf/bcf/v3/bcfapi.py | 4 +- src/bonsai/bonsai/bim/module/brick/data.py | 8 +- .../bonsai/bim/module/drawing/shaders.py | 10 +- .../bonsai/bim/module/structural/shader.py | 6 +- src/bonsai/bonsai/bim/ui.py | 1 - src/bonsai/bonsai/tool/brick.py | 60 +++------ src/bonsai/bonsai/tool/drawing.py | 6 +- .../classifications/brick_classifiction.py | 14 +-- src/bonsai/scripts/reregister_bonsai.py | 1 - .../_deprecated/scriptCodeAsterBonded.py | 114 ++++++------------ src/ifc2ca/_deprecated/scriptSalomeBonded.py | 6 +- .../features/steps/aggregation/en.py | 16 +-- .../examples/steps/aggregation.py | 8 +- .../ifcopenshell/__init__.py | 1 + .../ifcopenshell/express/bootstrap.py | 7 +- .../ifcopenshell/express/schema_class.py | 14 +-- .../ifcopenshell/geom/app.py | 12 +- .../ifcopenshell/util/cost.py | 6 +- .../util/scripts/validate_stub.py | 1 - .../ifcopenshell/util/selector.py | 18 +-- src/ifcopenshell-python/test/typing_tests.py | 1 - .../test/util/test_pset.py | 1 + .../recipes/ExtractPropertiesToSQLite.py | 18 +-- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 6 +- src/opencdeserver/api/app/repository/bcf.py | 14 +-- .../api/app/repository/documents.py | 21 +--- 27 files changed, 121 insertions(+), 271 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index 139e1dda2a..46b7b5c30c 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -315,24 +315,18 @@ cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA) cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA) cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.") cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA) -cecho( - """ - The used build configuration type for the dependencies. - Defaults to RelWithDebInfo if not specified.""" -) +cecho(""" - The used build configuration type for the dependencies. + Defaults to RelWithDebInfo if not specified.""") if BUILD_CFG == "MinSizeRel": cecho(" WARNING: MinSizeRel build can suffer from a significant performance loss.", RED) cecho(f"* IFCOS_NUM_BUILD_PROCS = {IFCOS_NUM_BUILD_PROCS}", MAGENTA) -cecho( - """ - How many compiler processes may be run in parallel. -""" -) +cecho(""" - How many compiler processes may be run in parallel. +""") cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA) -cecho( - """ - IFC Schemas to compile. If not provided, fallback to default provided in cmake. -""" -) +cecho(""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake. +""") dependency_tree: "dict[str, tuple[str, ...]]" = { "IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"), diff --git a/src/bcf/bcf/v3/bcfapi.py b/src/bcf/bcf/v3/bcfapi.py index 127d9e3732..3ac85b9685 100644 --- a/src/bcf/bcf/v3/bcfapi.py +++ b/src/bcf/bcf/v3/bcfapi.py @@ -34,8 +34,8 @@ client_id, client_secret = "", "" class OAuthReceiver(http.server.BaseHTTPRequestHandler): def do_GET(self) -> None: query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) - self.server.auth_code = query.get("code", [""])[0] # type:ignore - self.server.auth_state = query.get("state", [""])[0] # type:ignore + self.server.auth_code = query.get("code", [""])[0] # type: ignore + self.server.auth_state = query.get("state", [""])[0] # type: ignore self.send_response(200) self.send_header("Content-type", "text/plain") self.end_headers() diff --git a/src/bonsai/bonsai/bim/module/brick/data.py b/src/bonsai/bonsai/bim/module/brick/data.py index bae294d713..e414723594 100644 --- a/src/bonsai/bonsai/bim/module/brick/data.py +++ b/src/bonsai/bonsai/bim/module/brick/data.py @@ -63,8 +63,7 @@ class BrickschemaData: if namespace == "https://brickschema.org/schema/Brick": return [] results = [] - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX rdf: @@ -81,10 +80,7 @@ class BrickschemaData: } } GROUP BY ?object - """.replace( - "{uri}", uri - ) - ) + """.replace("{uri}", uri)) for row in query: predicate_uri = row.get("predicate") predicate_name = predicate_uri.toPython().split("#")[-1] diff --git a/src/bonsai/bonsai/bim/module/drawing/shaders.py b/src/bonsai/bonsai/bim/module/drawing/shaders.py index 2efd6ea285..774002fea3 100644 --- a/src/bonsai/bonsai/bim/module/drawing/shaders.py +++ b/src/bonsai/bonsai/bim/module/drawing/shaders.py @@ -335,12 +335,9 @@ class BaseLinesShader(BaseShader): TYPE = "LINES" - DEF_GLSL = ( - BaseShader.DEF_GLSL - + """ + DEF_GLSL = BaseShader.DEF_GLSL + """ #define GAP_SIZE {gap_size} """ - ) GEOM_GLSL = """ layout(lines) in; @@ -401,13 +398,10 @@ class DotsGizmoShader(GizmoShader): TYPE = "POINTS" - DEF_GLSL = ( - BaseShader.DEF_GLSL - + """ + DEF_GLSL = BaseShader.DEF_GLSL + """ #define CIRCLE_SEGMENTS 12 #define CIRCLE_RADIUS 8 """ - ) GEOM_GLSL = """ layout(points) in; diff --git a/src/bonsai/bonsai/bim/module/structural/shader.py b/src/bonsai/bonsai/bim/module/structural/shader.py index d6f92028d6..b9b5a5c7bc 100644 --- a/src/bonsai/bonsai/bim/module/structural/shader.py +++ b/src/bonsai/bonsai/bim/module/structural/shader.py @@ -58,15 +58,13 @@ class DecorationShader: "PLANAR LOAD", } if pattern not in valid_patterns: - raise ValueError( - """pattern must be one of: + raise ValueError("""pattern must be one of: PERPENDICULAR DISTRIBUTED FORCE PARALLEL DISTRIBUTED FORCE, DISTRIBUTED MOMENT, SINGLE FORCE, SINGLE MOMENT, - PLANAR LOAD""" - ) + PLANAR LOAD""") if "DISTRIBUTED" in pattern.upper(): shader = self.get_linear_shader(pattern) return shader diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 4cacd49e42..7bc27a20e0 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -55,7 +55,6 @@ from bonsai.bim.module.model.ui import ( from bonsai.bim.module.pset.prop import IfcProperty from bonsai.bim.prop import Attribute - if TYPE_CHECKING: from bonsai.bim.module.project.prop import BIMProjectProperties from bonsai.bim.prop import ObjProperty diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py index d585353745..7ef2c30444 100644 --- a/src/bonsai/bonsai/tool/brick.py +++ b/src/bonsai/bonsai/tool/brick.py @@ -164,17 +164,13 @@ class Brick(bonsai.core.tool.Brick): @classmethod def export_brick_attributes(cls, brick_uri: str) -> dict[str, Any]: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX rdfs: SELECT ?label { <{brick_uri}> rdfs:label ?label . } LIMIT 1 - """.replace( - "{brick_uri}", brick_uri - ) - ) + """.replace("{brick_uri}", brick_uri)) name = None for row in query: name = str(row.get("label")) @@ -218,18 +214,14 @@ class Brick(bonsai.core.tool.Brick): @classmethod def get_brickifc_project(cls) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX ref: SELECT ?proj WHERE { ?proj a ref:ifcProject . ?proj ref:ifcProjectID "{project_globalid}" . } LIMIT 1 - """.replace( - "{project_globalid}", project.GlobalId - ) - ) + """.replace("{project_globalid}", project.GlobalId)) results = list(query) if results: return results[0][0].toPython() @@ -275,17 +267,13 @@ class Brick(bonsai.core.tool.Brick): @classmethod def get_item_class(cls, item: str) -> Union[str, None]: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: SELECT ?class WHERE { <{item}> a ?class . } LIMIT 1 - """.replace( - "{item}", item - ) - ) + """.replace("{item}", item)) for row in query: return row.get("class").toPython().split("#")[-1] @@ -308,8 +296,7 @@ class Brick(bonsai.core.tool.Brick): @classmethod def import_brick_classes(cls, brick_class: str, split_screen: bool = False) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX rdf: @@ -322,10 +309,7 @@ class Brick(bonsai.core.tool.Brick): } GROUP BY ?group ORDER BY asc(?group) - """.replace( - "{brick_class}", brick_class - ) - ) + """.replace("{brick_class}", brick_class)) props = tool.Brick.get_brick_props() if split_screen: bricks = props.split_screen_bricks @@ -342,8 +326,7 @@ class Brick(bonsai.core.tool.Brick): @classmethod def import_brick_items(cls, brick_class: str, split_screen: bool = False) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX rdf: @@ -354,10 +337,7 @@ class Brick(bonsai.core.tool.Brick): } } ORDER BY asc(?item) - """.replace( - "{brick_class}", brick_class - ) - ) + """.replace("{brick_class}", brick_class)) props = tool.Brick.get_brick_props() if split_screen: bricks = props.split_screen_bricks @@ -507,8 +487,7 @@ class BrickStore: @classmethod def load_sub_roots(cls) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: SELECT ?subRoot ?subClasses WHERE { @@ -525,8 +504,7 @@ class BrickStore: } FILTER(?subClasses > 3) } - """ - ) + """) for row in query: sub_root = row.get("subRoot").toPython().split("#")[-1] BrickStore.root_classes.append(sub_root) @@ -558,8 +536,7 @@ class BrickStore: @classmethod def load_entity_classes(cls) -> None: for root_class in BrickStore.root_classes: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: PREFIX owl: @@ -569,25 +546,20 @@ class BrickStore: ?class owl:deprecated true . } } - """.replace( - "{root_class}", root_class - ) - ) + """.replace("{root_class}", root_class)) BrickStore.entity_classes[root_class] = [] for uri in sorted([x[0].toPython() for x in query]): BrickStore.entity_classes[root_class].append(uri) @classmethod def load_relationships(cls) -> None: - query = BrickStore.graph.query( - """ + query = BrickStore.graph.query(""" PREFIX brick: PREFIX rdfs: SELECT DISTINCT ?relation WHERE { ?relation rdfs:subPropertyOf brick:Relationship . } - """ - ) + """) for uri in sorted([x[0].toPython() for x in query]): BrickStore.relationships.append(uri) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index ca13168942..b8ec26fd37 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2710,16 +2710,14 @@ class Drawing(bonsai.core.tool.Drawing): return float(value) except: pass # Perhaps it's imperial? - l = lark.Lark( - """start: feet? "-"? inches? + l = lark.Lark("""start: feet? "-"? inches? feet: NUMBER? "-"? fraction? "'" inches: NUMBER? "-"? fraction? "\\"" fraction: NUMBER "/" NUMBER %import common.NUMBER %import common.WS %ignore WS // Disregard spaces in text - """ - ) + """) try: start = l.parse(value) diff --git a/src/bonsai/scripts/classifications/brick_classifiction.py b/src/bonsai/scripts/classifications/brick_classifiction.py index 9a9d42353e..17246940c7 100644 --- a/src/bonsai/scripts/classifications/brick_classifiction.py +++ b/src/bonsai/scripts/classifications/brick_classifiction.py @@ -22,8 +22,7 @@ class Generator: } ) - query = self.schema.query( - """ + query = self.schema.query(""" PREFIX brick: PREFIX rdfs: PREFIX skos: @@ -49,8 +48,7 @@ class Generator: } } GROUP BY ?entity - """ - ) + """) # create references dictionary references = {} @@ -76,17 +74,13 @@ class Generator: ) # get all parents of the entity - query = self.schema.query( - """ + query = self.schema.query(""" PREFIX brick: PREFIX rdfs: SELECT ?parent WHERE { brick:{entity} rdfs:subClassOf ?parent . } - """.replace( - "{entity}", location.split("#")[-1] - ) - ) + """.replace("{entity}", location.split("#")[-1])) # filter parents for the brick entity for row in query: parent = row.get("parent").toPython() diff --git a/src/bonsai/scripts/reregister_bonsai.py b/src/bonsai/scripts/reregister_bonsai.py index ba9ab2ba2a..4eb3764708 100644 --- a/src/bonsai/scripts/reregister_bonsai.py +++ b/src/bonsai/scripts/reregister_bonsai.py @@ -23,7 +23,6 @@ Use operators instead of `blender --command extension remove` to ensure disable and enable occur in the same Blender session. """ - import bpy import bonsai.tool as tool diff --git a/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py b/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py index 0b1184453b..e5bfd03aed 100644 --- a/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py +++ b/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py @@ -89,27 +89,22 @@ class COMMANDFILE: f.write("# Linear Static Analysis With Self-Weight\n") - f.write( - """ + f.write(""" # STEP: INITIALIZE STUDY DEBUT( PAR_LOT = 'NON' ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: READ MED FILE mesh = LIRE_MAILLAGE( FORMAT = 'MED', UNITE = 20 ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: DEFINE MODEL model = AFFE_MODELE( MAILLAGE = mesh, @@ -118,8 +113,7 @@ model = AFFE_MODELE( TOUT = 'OUI', PHENOMENE = 'MECANIQUE', MODELISATION = '3D' - ),""" - ) + ),""") if faceGroupNames: template = """ @@ -157,12 +151,10 @@ model = AFFE_MODELE( f.write(template.format(**context)) - f.write( - """ + f.write(""" ) )\n -""" - ) +""") f.write("# STEP: DEFINE MATERIALS") @@ -195,12 +187,10 @@ model = AFFE_MODELE( f.write(template.format(**context)) - f.write( - """ + f.write(""" material = AFFE_MATERIAU( MAILLAGE = mesh, - AFFE = (""" - ) + AFFE = (""") for i, material in enumerate(materials): template = """ @@ -227,20 +217,16 @@ material = AFFE_MATERIAU( f.write(template.format(**context)) - f.write( - """ + f.write(""" ) ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: DEFINE ELEMENTS element = AFFE_CARA_ELEM( MODELE = model, - POUTRE = (""" - ) + POUTRE = (""") for profile in profiles: if profile["profileShape"] == "rectangular" and profile["profileType"] == "AREA": @@ -296,11 +282,9 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - f.write( - """ + f.write(""" ), - COQUE = (""" - ) + COQUE = (""") for el in [el for el in elements if el["geometryType"] == "surface"]: @@ -319,15 +303,11 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - f.write( - """ - ),""" - ) + f.write(""" + ),""") - f.write( - """ - ORIENTATION = (""" - ) + f.write(""" + ORIENTATION = (""") for el in [el for el in elements if el["geometryType"] == "line"]: @@ -345,21 +325,16 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - f.write( - """ - ),""" - ) + f.write(""" + ),""") - f.write( - """ + f.write(""" )\n -""" - ) +""") f.write("# STEP: DEFINE SUPPORTS AND CONSTRAINTS") - f.write( - """ + f.write(""" liaisons = AFFE_CHAR_MECA( MODELE = model, DDL_IMPO = ( @@ -372,14 +347,11 @@ liaisons = AFFE_CHAR_MECA( DRY = 0.0, DRZ = 0.0 ) - ),""" - ) + ),""") if rigidLinkGroupNames: - f.write( - """ - LIAISON_SOLIDE = (""" - ) + f.write(""" + LIAISON_SOLIDE = (""") for groupName in rigidLinkGroupNames: template = """ @@ -391,16 +363,12 @@ liaisons = AFFE_CHAR_MECA( f.write(template.format(**context)) - f.write( - """ - ),""" - ) + f.write(""" + ),""") - f.write( - """ + f.write(""" ) -""" - ) +""") template = """ # STEP: DEFINE LOAD @@ -418,8 +386,7 @@ gravLoad = AFFE_CHAR_MECA( f.write(template.format(**context)) - f.write( - """ + f.write(""" # STEP: RUN ANALYSIS res_Bld = MECA_STATIQUE( MODELE = model, @@ -434,8 +401,7 @@ res_Bld = MECA_STATIQUE( ) ) ) -""" - ) +""") # f.write( # ''' @@ -515,8 +481,7 @@ res_Bld = MECA_STATIQUE( # ''' # ) # - f.write( - """ + f.write(""" # STEP: DEFORMED SHAPE EXTRACTION IMPR_RESU( FORMAT = 'MED', @@ -527,15 +492,12 @@ IMPR_RESU( NOM_CHAM_MED = ('Bld_DISP',), # 'Bld_REAC', 'Bld_FORC' ) ) -""" - ) +""") - f.write( - """ + f.write(""" # STEP: CONCLUDE STUDY FIN() -""" - ) +""") f.close() diff --git a/src/ifc2ca/_deprecated/scriptSalomeBonded.py b/src/ifc2ca/_deprecated/scriptSalomeBonded.py index fb2bd575f9..d3b6d506c8 100644 --- a/src/ifc2ca/_deprecated/scriptSalomeBonded.py +++ b/src/ifc2ca/_deprecated/scriptSalomeBonded.py @@ -53,16 +53,16 @@ class MODEL: """Function to define a Point from a polyline (list of 1 point)""" - (x, y, z) = pl + x, y, z = pl return self.geompy.MakeVertex(x, y, z) def makeLine(self, pl): """Function to define a Line from a polyline (list of 2 points)""" - (x, y, z) = pl[0] + x, y, z = pl[0] P1 = self.geompy.MakeVertex(x, y, z) - (x, y, z) = pl[1] + x, y, z = pl[1] P2 = self.geompy.MakeVertex(x, y, z) return self.geompy.MakeLineTwoPnt(P1, P2) diff --git a/src/ifcbimtester/bimtester/features/steps/aggregation/en.py b/src/ifcbimtester/bimtester/features/steps/aggregation/en.py index a2bbe621a1..f0c4af8f15 100644 --- a/src/ifcbimtester/bimtester/features/steps/aggregation/en.py +++ b/src/ifcbimtester/bimtester/features/steps/aggregation/en.py @@ -28,12 +28,8 @@ use_step_matcher("parse") @step('There must be exactly {number} "{ifc_class}" elements') def step_impl(context, number, ifc_class): num = len(IfcStore.file.by_type(ifc_class)) - assert num == int( - number - ), "Could not find {} elements of {}. \ - Found {} element(s).".format( - number, ifc_class, num - ) + assert num == int(number), "Could not find {} elements of {}. \ + Found {} element(s).".format(number, ifc_class, num) @given('a set of (key,value) called ("{key_name}","{value_name}")') @@ -95,13 +91,9 @@ def step_impl(context, attribute_name): @then('there must be exactly a number of "{ifc_class}" equals to the number of distinct value') def step_impl(context, ifc_class): try: - context.execute_steps( - """ + context.execute_steps(""" then There must be exactly {number} "{ifc_class}" elements - """.format( - ifc_class=ifc_class, number=context.model.get_count_distinct_values() - ) - ) + """.format(ifc_class=ifc_class, number=context.model.get_count_distinct_values())) except AssertionError as error: str_error = str(error) assert False, str_error[: str_error.find("Traceback")] diff --git a/src/ifcbimtester/examples/steps/aggregation.py b/src/ifcbimtester/examples/steps/aggregation.py index ccf46b5e0d..96e0375b43 100644 --- a/src/ifcbimtester/examples/steps/aggregation.py +++ b/src/ifcbimtester/examples/steps/aggregation.py @@ -51,13 +51,9 @@ def step_impl(context, path_file): @then("there must be exactly a number of {ifc_class} equals to the number of distinct row value") def step_impl(context, ifc_class): try: - context.execute_steps( - """ + context.execute_steps(""" then There must be exactly {number} {ifc_class} elements - """.format( - ifc_class=ifc_class, number=context.model.get_count_distinct_values() - ) - ) + """.format(ifc_class=ifc_class, number=context.model.get_count_distinct_values())) except AssertionError as error: str_error = str(error) assert False, str_error[: str_error.find("Traceback")] diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index fb38426aff..998eb6e5de 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -53,6 +53,7 @@ Example: for wall in walls: print(wall.Name) """ + from __future__ import annotations import os diff --git a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py index 578e561e79..ef3c3c3ef0 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py +++ b/src/ifcopenshell-python/ifcopenshell/express/bootstrap.py @@ -217,8 +217,7 @@ for id in to_emit: statements.append("%s << %s" % (id, stmt)) if __name__ == "__main__": - print( - r""" + print(r""" # This file is generated by IfcOpenShell ifcexpressparser bootstrap.py from __future__ import annotations @@ -257,6 +256,4 @@ if __name__ == "__main__": mdl = importlib.import_module(output) mdl.Generator(m).emit() sys.stdout.write(m.schema.name) -""" - % ("\n ".join(statements)) - ) +""" % ("\n ".join(statements))) diff --git a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py index e017c03d7a..d7595ad6cb 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/schema_class.py +++ b/src/ifcopenshell-python/ifcopenshell/express/schema_class.py @@ -363,24 +363,18 @@ class EarlyBoundCodeWriter: ) ) - self.statements[self.statements.index("{factory_placeholder}")] = ( - """ + self.statements[self.statements.index("{factory_placeholder}")] = """ class %(schema_name)s_instance_factory : public IfcParse::instance_factory { virtual IfcUtil::IfcBaseClass* operator()(const IfcParse::declaration* decl, IfcEntityInstanceData&& data) const { %(instance_mapping)s } }; -""" - % locals() - ) +""" % locals() "" - self.statements[self.statements.index("{string_pool_placeholder}")] = ( - """ + self.statements[self.statements.index("{string_pool_placeholder}")] = """ const std::string strings[] = {%s}; -""" - % ",".join(map(lambda s: '"%s"s' % s, self.strings)) - ) +""" % ",".join(map(lambda s: '"%s"s' % s, self.strings)) def __str__(self): return "\n".join(self.statements) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/app.py b/src/ifcopenshell-python/ifcopenshell/geom/app.py index fa07f3f2b8..d6bab207f1 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/app.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/app.py @@ -145,8 +145,7 @@ class configuration: config.set( "snippets", "print all wall ids", - self.config_encode( - """ + self.config_encode(""" ########################################################################### # A simple script that iterates over all walls in the current model # # and prints their Globally unique IDs (GUIDS) to the console window # @@ -154,15 +153,13 @@ class configuration: for wall in model.by_type("IfcWall"): print ("wall with global id: "+str(wall.GlobalId)) -""".lstrip() - ), +""".lstrip()), ) config.set( "snippets", "print properties of current selection", - self.config_encode( - """ + self.config_encode(""" ########################################################################### # A simple script that iterates over all IfcPropertySets of the currently # # selected object and prints them to the console # @@ -180,8 +177,7 @@ if selection: for prop in relDefinesByProperties.RelatingPropertyDefinition.HasProperties: print ("{:<20} :{}".format(prop.Name,prop.NominalValue.wrappedValue)) print ("\\n") -""".lstrip() - ), +""".lstrip()), ) with open(conf_file, "w") as configfile: config.write(configfile) diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index 19172c4710..875594f1a5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -352,8 +352,7 @@ def get_cost_rate( class CostValueUnserialiser: def parse(self, formula: str): - l = lark.Lark( - """start: formula + l = lark.Lark("""start: formula formula: operand (operator operand)* operand: value | category "(" formula ")" value: NUMBER? @@ -390,8 +389,7 @@ class CostValueUnserialiser: NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text - """ - ) + """) start = l.parse(formula) return self.get_formula(start.children[0]) diff --git a/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py b/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py index 2ddf444704..5ba2466f29 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py +++ b/src/ifcopenshell-python/ifcopenshell/util/scripts/validate_stub.py @@ -25,7 +25,6 @@ Things we do check: - class hierarchy """ - import ast import difflib from pathlib import Path diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index d3bef9758f..c6ee820ab8 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -39,8 +39,7 @@ import ifcopenshell.util.shape import ifcopenshell.util.system import ifcopenshell.util.unit -filter_elements_grammar = lark.Lark( - """start: filter_group +filter_elements_grammar = lark.Lark("""start: filter_group filter_group: facet_list ("+" facet_list)* facet_list: facet ("," facet)* @@ -111,11 +110,9 @@ filter_elements_grammar = lark.Lark( NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text -""" -) +""") -get_element_grammar = lark.Lark( - """start: keys +get_element_grammar = lark.Lark("""start: keys keys: key ("." key)* key: quoted_string | regex_string | unquoted_string @@ -130,11 +127,9 @@ get_element_grammar = lark.Lark( WS: /[ \\t\\f\\r\\n]/+ %ignore WS // Disregard spaces in text - """ -) + """) -format_grammar = lark.Lark( - """start: expression +format_grammar = lark.Lark("""start: expression ?expression: add_sub ?add_sub: mul_div @@ -193,8 +188,7 @@ format_grammar = lark.Lark( NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text -""" -) +""") class FormatTransformer(lark.Transformer): diff --git a/src/ifcopenshell-python/test/typing_tests.py b/src/ifcopenshell-python/test/typing_tests.py index 5b2d789613..ccf007fa62 100644 --- a/src/ifcopenshell-python/test/typing_tests.py +++ b/src/ifcopenshell-python/test/typing_tests.py @@ -21,7 +21,6 @@ This file should produce no warnings from type checker (currently pyright). Those tests are not automatically checked and just there to make sure overloads are making sense. """ - from typing import Union from typing_extensions import assert_type diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 3b44951ec4..bec1a10ba0 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . """Run this test from src/ifcopenshell-python folder: pytest --durations=0 ifcopenshell/util/test_pset.py""" + from ifcopenshell.util import pset from ifcopenshell.util.pset import ApplicableEntity diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py index 634a8fb796..0f9dc8544d 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py @@ -55,8 +55,7 @@ class Patcher: self.c = self.db.cursor() self.file_patched = db_file - self.c.execute( - """ + self.c.execute(""" CREATE TABLE IF NOT EXISTS elements ( id integer PRIMARY KEY NOT NULL UNIQUE, global_id text, @@ -65,33 +64,28 @@ class Patcher: name text, description text ); - """ - ) + """) self.c.execute("CREATE INDEX IF NOT EXISTS idx_global_id ON elements (global_id);") self.c.execute("CREATE INDEX IF NOT EXISTS idx_ifc_class ON elements (ifc_class);") self.c.execute("CREATE INDEX IF NOT EXISTS idx_predefined_type ON elements (predefined_type);") - self.c.execute( - """ + self.c.execute(""" CREATE TABLE IF NOT EXISTS relationships ( from_id integer NOT NULL, type text, to_id integer NOT NULL ); - """ - ) + """) self.c.execute("CREATE INDEX IF NOT EXISTS idx_from_id ON relationships (from_id);") - self.c.execute( - """ + self.c.execute(""" CREATE TABLE IF NOT EXISTS properties ( element_id integer NOT NULL, set_name text, name text, value text ); - """ - ) + """) self.c.execute("CREATE INDEX IF NOT EXISTS idx_element_id ON properties (element_id);") elements = self.file.by_type("IfcObjectDefinition") diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index f7e294898a..256ffa99ce 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -349,13 +349,11 @@ class Patcher(ifcpatch.BasePatcher): assert cursor is not None row = cursor.fetchone() elif self.sql_type == "mysql": - cursor = self.c.execute( - f""" + cursor = self.c.execute(f""" SELECT 1 FROM information_schema.tables WHERE table_schema = '{self.database}' AND table_name = 'id_map' LIMIT 1; - """ - ) + """) row = self.c.fetchone() else: assert_never(self.sql_type) diff --git a/src/opencdeserver/api/app/repository/bcf.py b/src/opencdeserver/api/app/repository/bcf.py index a65c43d115..3812334e41 100644 --- a/src/opencdeserver/api/app/repository/bcf.py +++ b/src/opencdeserver/api/app/repository/bcf.py @@ -694,8 +694,7 @@ class BCFDB(MyDB): snapshot_type = "" snapshot = False set_snapshot = "" - cypher_viewpoint = ( - """ + cypher_viewpoint = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) WHERE u.username = $username AND r1.createViewpoint = True @@ -713,9 +712,7 @@ class BCFDB(MyDB): v.spaces_visible = $spaces_visible, v.space_boundaries_visible = $space_boundaries_visible, v.openings_visible = $openings_visible - """ - % set_snapshot - ) + """ % set_snapshot if viewpoint.guid is None: viewpoint.guid = uuid4() if viewpoint.orthogonal_camera is None: @@ -1450,8 +1447,7 @@ class BCFDB(MyDB): else: document_url = "" document_reference.url = "" - cypher = ( - """ + cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) WHERE u.username = $username AND r1.updateDocumentReferences = True @@ -1461,9 +1457,7 @@ class BCFDB(MyDB): SET r3.guid: $document_reference_id, %s d.description = $description - """ - % document_url - ) + """ % document_url result = tx.run( cypher, username=current_user.username, diff --git a/src/opencdeserver/api/app/repository/documents.py b/src/opencdeserver/api/app/repository/documents.py index 95c669a7bc..d03512c8e3 100644 --- a/src/opencdeserver/api/app/repository/documents.py +++ b/src/opencdeserver/api/app/repository/documents.py @@ -36,17 +36,14 @@ class DOCDB(MyDB): else: version_index_criteria = "AND d.version_index = $version_index" - cypher = ( - """ + cypher = """ MATCH (d:Document) WHERE d.document_id = $document_id %s RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ - % version_index_criteria - ) + """ % version_index_criteria result = tx.run(cypher, document_id=document_id, version_index=version_index) @@ -891,8 +888,7 @@ class DOCDB(MyDB): else: version_index_criteria = "" - cypher = ( - """ + cypher = """ MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) WHERE u.username = $username AND d.document_id = $document_id @@ -900,9 +896,7 @@ class DOCDB(MyDB): RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ - % version_index_criteria - ) + """ % version_index_criteria result = tx.run( cypher, username=current_user.username, document_id=document_id, version_index=version_index @@ -927,8 +921,7 @@ class DOCDB(MyDB): else: version_index_criteria = "" - cypher = ( - """ + cypher = """ MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) WHERE u.username = $username AND d.document_id = $document_id @@ -936,9 +929,7 @@ class DOCDB(MyDB): RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ - % version_index_criteria - ) + """ % version_index_criteria result = tx.run( cypher, username=current_user.username, document_id=document_id, version_index=version_index From f8663b5e2b5af60382c4ad397bda0ad660096605 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 14:52:46 +0500 Subject: [PATCH 107/131] Bump ifcopenshell build Just because it didn't happened for a while now and we need to test it. --- src/bonsai/Makefile | 2 +- src/ifcopenshell-python/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 38fbc61355..310d31a4b0 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -104,7 +104,7 @@ endif endif # def PLATFORM # Current build commit hash. -OLD:=e8eb5e4 +OLD:=1c5b825 .PHONY: bump bump: ifndef NEW diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 78370a46fb..303811ad48 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -69,7 +69,7 @@ PLATFORMTAG:=win_amd64 endif BINARY_VERSION:=0.8.4 -BUILD_COMMIT:=e8eb5e4 +BUILD_COMMIT:=1c5b825 IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip From 5b0511379bbf6db41cbafe8de20aef9dac46e01a Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 27 Feb 2026 11:06:45 +0100 Subject: [PATCH 108/131] IfcAxis1Placement.Axis is optional #7728 --- src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp | 11 ++++++++++- src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp | 10 +++++++++- src/ifcgeom/mapping/IfcToroidalSurface.cpp | 15 +++++---------- src/ifcgeom/mapping/mapping.cpp | 4 ++++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp b/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp index 8b2fe01272..f6a9fc861c 100644 --- a/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp +++ b/src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp @@ -43,11 +43,20 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcRevolvedAreaSolid* inst) { angle = ang; } + taxonomy::direction3::ptr axis; + if (inst->Axis()->Axis()) { + axis = taxonomy::cast(map(inst->Axis()->Axis())); + } else { + // IfcAxis1Placement.Axis is optional, and defaults to (0, 0, 1) if not provided. + axis = taxonomy::make(0, 0, 1); + } + + return taxonomy::make( matrix, taxonomy::cast(map(inst->SweptArea())), taxonomy::cast(map(inst->Axis()->Location())), - taxonomy::cast(map(inst->Axis()->Axis())), + axis, angle ); diff --git a/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp b/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp index f83c4ecf03..c5fe21b58b 100644 --- a/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp +++ b/src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp @@ -31,11 +31,19 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSurfaceOfRevolution* inst) { matrix = taxonomy::cast(map(inst->Position())); } + taxonomy::direction3::ptr axis; + if (inst->AxisPosition()->Axis()) { + axis = taxonomy::cast(map(inst->AxisPosition()->Axis())); + } else { + // IfcAxis1Placement.Axis is optional, and defaults to (0, 0, 1) if not provided. + axis = taxonomy::make(0, 0, 1); + } + return taxonomy::make( matrix, taxonomy::cast(map(inst->SweptCurve())), taxonomy::cast(map(inst->AxisPosition()->Location())), - taxonomy::cast(map(inst->AxisPosition()->Axis())), + axis, boost::none ); } diff --git a/src/ifcgeom/mapping/IfcToroidalSurface.cpp b/src/ifcgeom/mapping/IfcToroidalSurface.cpp index ce17a37dbc..d73e90bb1b 100644 --- a/src/ifcgeom/mapping/IfcToroidalSurface.cpp +++ b/src/ifcgeom/mapping/IfcToroidalSurface.cpp @@ -24,16 +24,11 @@ using namespace ifcopenshell::geometry; #ifdef SCHEMA_HAS_IfcToroidalSurface taxonomy::ptr mapping::map_impl(const IfcSchema::IfcToroidalSurface* inst) { - return nullptr; - - /* - gp_Trsf trsf; - IfcGeom::Kernel::convert(inst->Position(), trsf); - - // IfcElementarySurface.Position has unit scale factor - face = BRepBuilderAPI_MakeFace(new Geom_ToroidalSurface(gp::XOY(), inst->MajorRadius() * length_unit_, inst->MinorRadius() * length_unit_), getValue(GV_PRECISION)).Face().Moved(trsf); - return true; - */ + auto c = taxonomy::make(); + c->radius1 = inst->MajorRadius() * length_unit_; + c->radius2 = inst->MinorRadius() * length_unit_; + c->matrix = taxonomy::cast(map(inst->Position())); + return c; } #endif diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index f98bfe232d..edfdd3a13b 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -691,6 +691,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSurfaceStyle* style) { } taxonomy::ptr mapping::map(const IfcBaseInterface* inst) { + if (inst == nullptr) { + Logger::Error("Warning nullptr passed to map() function"); + return nullptr; + } auto iden = inst->as()->identity(); if (use_caching_) { std::lock_guard guard(cache_guard_); From db377e21788bbcf859f106e7d51a73b8277275c5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 15:11:10 +0500 Subject: [PATCH 109/131] Also bump binary version --- src/ifcopenshell-python/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 303811ad48..b6cfe15b17 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -68,7 +68,7 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -BINARY_VERSION:=0.8.4 +BINARY_VERSION:=0.8.5 BUILD_COMMIT:=1c5b825 IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip From 43146530b0921d8276de0ae04c57cd4dc86a8bfc Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 2 Mar 2026 21:48:51 +0100 Subject: [PATCH 110/131] arrange polygons: Alternative (unused) perimiter approach; simpler topology handling; projection-based clean-up --- src/svgfill/src/arrange_polygons.cpp | 545 ++++++++++++++++++++++++++- 1 file changed, 536 insertions(+), 9 deletions(-) diff --git a/src/svgfill/src/arrange_polygons.cpp b/src/svgfill/src/arrange_polygons.cpp index 0e77a28886..4fa20c68dc 100644 --- a/src/svgfill/src/arrange_polygons.cpp +++ b/src/svgfill/src/arrange_polygons.cpp @@ -1,4 +1,4 @@ -// #define SVGFILL_DEBUG +#define SVGFILL_DEBUG // #define SVGFILL_MAIN #ifndef SVGFILL_MAIN @@ -38,6 +38,7 @@ typedef CGAL::Exact_predicates_exact_constructions_kernel K; typedef CGAL::Polygon_2 Polygon_2; typedef CGAL::Polygon_with_holes_2 Polygon_with_holes_2; typedef K::Point_2 Point_2; +typedef K::Vector_2 Vector_2; typedef K::Segment_2 Segment_2; typedef std::vector Polygon_list; typedef CGAL::Arr_segment_traits_2 Traits_2; @@ -694,6 +695,50 @@ class SegmentLookup { return std::make_pair(input_it, closest); }; + std::vector> n_closest_input_segments(const Segment_2& e, size_t n = 2) const { + auto mid = CGAL::ORIGIN + ((e.source() - CGAL::ORIGIN) + (e.target() - CGAL::ORIGIN)) / 2; + + std::vector>::iterator> cands; + cands.reserve(64); + + auto mid3 = CGAL::Point_3(mid.x(), mid.y(), 0); + + for (int i = -1; i <= 3; ++i) { + cands.clear(); + double r = std::pow(10.0, i); + auto midbb = mid3.bbox(); + CGAL::Bbox_3 box(midbb.xmin() - r, midbb.ymin() - r, -1.0, midbb.xmax() + r, midbb.ymax() + r, +1.0); + tree_.all_intersected_primitives(box, std::back_inserter(cands)); + if (cands.size() >= n) { + break; + } + } + + if (cands.empty()) { + return {}; + } + + std::vector>> scored; + scored.reserve(cands.size()); + for (auto it : cands) { + const auto& s3 = *it; + Segment_2 s2(Point_2(s3.source().x(), s3.source().y()), Point_2(s3.target().x(), s3.target().y())); + scored.emplace_back(CGAL::squared_distance(mid, s2), s2); + } + + std::sort(scored.begin(), scored.end(), [](auto& a, auto& b) { return a.first < b.first; }); + if (scored.size() > n) { + scored.resize(n); + } + + std::vector> out; + out.reserve(scored.size()); + for (auto& p : scored) { + out.push_back(p.second); + } + return out; + } + private: using TreeTraits = CGAL::AABB_traits>::iterator>>; using Tree = CGAL::AABB_tree; @@ -934,6 +979,69 @@ void eliminate_colinear_vertices(Graph2D& G) { } } +struct Ccw_radial_sort { + Point_2 c; + explicit Ccw_radial_sort(const Point_2& center) : c(center) {} + + bool operator()(const Point_2& a, const Point_2& b) const { + const Vector_2 va = a - c; + const Vector_2 vb = b - c; + + // Only left-turn is not sufficient because we should not wrap around, + // but rather start from e.g positive x-axis and then sort CCW. + // Therefore top-half plane always comes before bottom-half plane. + const bool ua = va.y() == 0 ? va.x() > 0 : va.y() > 0; + const bool ub = vb.y() == 0 ? vb.x() > 0 : vb.y() > 0; + if (ua != ub) { + return ua; + } + + if (CGAL::collinear(c, a, b)) { + // Nearer first so that original polygon edges are likely retained + // (not sure if it matters). + return va.squared_length() < vb.squared_length(); + } + + // This is a less functor, so we return true if c,a,b is a left turn, which means that a is CCW before b + return CGAL::left_turn(c, a, b); + } +}; + +void build_radial_neighbour_map(const std::vector& polygons, double radius, std::map>& neighbour_map) { + for (auto& poly : polygons) { + for (auto it = poly.edges_begin(); it != poly.edges_end(); ++it) { + auto source = it->source(); + auto target = it->target(); + neighbour_map[source].push_back(target); + neighbour_map[target].push_back(source); + } + } + + // Box_intersection_d package to find close vertices and connect them as well + typedef CGAL::Box_intersection_d::Box_with_handle_d Box; + std::vector boxes; + for (auto& poly : polygons) { + for (auto it = poly.vertices_begin(); it != poly.vertices_end(); ++it) { + const auto pb = it->bbox(); + boxes.emplace_back( + CGAL::Bbox_2(pb.xmin() - radius, pb.ymin() - radius, pb.xmax() + radius, pb.ymax() + radius), + *it); + } + } + CGAL::box_self_intersection_d(boxes.begin(), boxes.end(), [&](const Box& a, const Box& b) { + if ((a.handle() - b.handle()).squared_length() <= (radius * radius)) { + neighbour_map[a.handle()].push_back(b.handle()); + neighbour_map[b.handle()].push_back(a.handle()); + } + }); + + // radial sort + for (auto& p : neighbour_map) { + auto& nb = p.second; + std::sort(nb.begin(), nb.end(), Ccw_radial_sort(p.first)); + } +} + void edge_slide(Graph2D& G) { std::list> edges_to_remove, edges_to_insert; @@ -1043,6 +1151,7 @@ std::list> extend_end_vertices_based_on_input( 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 @@ -1067,6 +1176,9 @@ std::list> extend_end_vertices_based_on_input( } 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); @@ -1104,11 +1216,15 @@ std::list> extend_end_vertices_based_on_input( handled_as_graph_path = true; break; } +#endif + } 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) { // else we choose to map point to the midpoint of the found two close points. @@ -1150,6 +1266,7 @@ std::list> extend_end_vertices_based_on_input( constructed_segments.push_front({avg, Q}); constructed_segments.push_front({avg, R}); } +#endif } } @@ -1238,6 +1355,372 @@ 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 { + if (a.source() != b.source()) { + return a.source() < b.source(); + } + return a.target() < b.target(); + } +}; + +void clean_noisy_paths(Arrangement_2& arr, SegmentLookup& segment_lookup) { + 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(); + }; + + std::set edges; + for (auto he = arr.edges_begin(); he != arr.edges_end(); ++he) { + auto a = he->source()->point(); + auto b = he->target()->point(); + if (a < b) { + edges.insert({a, b}); + } else { + edges.insert({b, a}); + } + } + + 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.1) / own_length; + }; + + std::map badnesses; + for (auto& e : edges) { + badnesses[e] = edge_badness(e); + } + + double thr; + { + std::vector tmp; + tmp.reserve(badnesses.size()); + for (auto& p : badnesses) { + tmp.push_back(p.second); + } + std::nth_element(tmp.begin(), tmp.begin() + tmp.size() / 2, tmp.end()); + double med = tmp[tmp.size() / 2]; + thr = 10.0 * med; + } + + std::set bad_edges; + for (auto& p : badnesses) { + if (p.second > thr) { + bad_edges.insert(p.first); + } + } + + std::map> topo, bad_topo; + + for (auto& e : edges) { + topo[e.source()].push_back(e); + topo[e.target()].push_back(e); + } + for (auto& e : bad_edges) { + bad_topo[e.source()].push_back(e); + bad_topo[e.target()].push_back(e); + } + + std::set break_vertices; + for (auto& p : bad_topo) { + const auto& v = p.first; + if (!(topo[v].size() == 2 && bad_topo[v].size() == 2)) { + break_vertices.insert(v); + } + } + + std::set seen; + std::vector> bad_paths; + + for (auto& s : break_vertices) { + for (auto& e0 : bad_topo[s]) { + if (seen.count(e0)) { + continue; + } + + Point_2 v = s; + std::vector path; + path.push_back(s); + + auto e = e0; + while (true) { + seen.insert(e); + v = other(e, v); + path.push_back(v); + + if (break_vertices.count(v)) { + break; + } + + auto& inc = bad_topo[v]; + std::vector nxt; + nxt.reserve(2); + for (auto& ee : inc) { + if (ee != e && !seen.count(ee)) { + nxt.push_back(ee); + } + } + if (nxt.size() != 1) { + break; + } + e = nxt[0]; + } + + if (path.size() > 1) { + bad_paths.push_back(std::move(path)); + } + } + } + + std::set> to_remove; + std::vector> to_insert; + + auto dirs_from = [&](const Point_2& v, const std::set& path_edges) { + std::vector ds; + for (auto& ee : topo[v]) { + if (path_edges.count(ee) || bad_edges.count(ee)) { + continue; + } + Point_2 u = other(ee, v); + ds.push_back(v - u); + } + return ds; + }; + + auto collapse_path = [&](const std::vector& path) -> std::optional { + std::set path_edges; + for (size_t i = 0; i + 1 < path.size(); ++i) { + auto* a = &path[i]; + auto* b = &path[i + 1]; + if (*a < *b) { + path_edges.insert({*a, *b}); + } else { + path_edges.insert({*b, *a}); + } + } + + const auto& a0 = path.front(); + const auto& b0 = path.back(); + + auto das = dirs_from(a0, path_edges); + auto dbs = dirs_from(b0, path_edges); + if (das.empty() || dbs.empty()) { + return std::nullopt; + } + + K::FT best_ke = std::numeric_limits::infinity(); + std::optional best_x; + + for (auto& da : das) { + for (auto& db : dbs) { + CGAL::Ray_2 r1(a0, da); + CGAL::Ray_2 r2(b0, db); + + auto x = CGAL::intersection(r1, r2); + if (x) { + if (auto* xp = variant_get>(&*x)) { + // @todo does it matter that this is squared? + auto ke = CGAL::squared_distance(*xp, a0) + CGAL::squared_distance(*xp, b0); + if (ke < best_ke) { + best_ke = ke; + best_x = *xp; + } + } + } + } + } + + return best_x; + }; + + for (auto& path : bad_paths) { + auto x = collapse_path(path); + if (!x) { + // std::cerr << "Unable to collapse path, skipping" << std::endl; + continue; + } + + double orig_length = 0.; + for (size_t i = 0; i < path.size() - 1; ++i) { + auto& a = path[i]; + auto& b = path[i + 1]; + orig_length += std::sqrt(CGAL::to_double(CGAL::squared_distance(a, b))); + } + + 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; + 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; + continue; + } + } + + for (size_t i = 0; i < path.size() - 1; ++i) { + auto& a = path[i]; + auto& b = path[i + 1]; + if (a < b) { + to_remove.insert({a, b}); + } else { + to_remove.insert({b, a}); + } + } + auto s = path.front(); + auto t = path.back(); + if (s != *x) { + to_insert.push_back({s, *x}); + } + if (t != *x) { + to_insert.push_back({t, *x}); + } + } + + /* + using Walk_pl = CGAL::Arr_walk_along_line_point_location; + Walk_pl walk_pl(arr); + + 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); + + + if ((*v)->point() != e.first) { + std::cerr << "Warning: unable to locate vertex for edge removal, skipping" << std::endl; + continue; + } + } else { + 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; + } + } + + 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) { + std::set to_remove; + std::set> to_add; + while (true) { + bool removed_this_round = false; + for (auto it = arr.vertices_begin(); it != arr.vertices_end(); ++it) { + if (it->degree() == 2) { + Arrangement_2::Halfedge_around_vertex_circulator first, curr; + first = curr = it->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); + + using SK = CGAL::Simple_cartesian; + CGAL::Cartesian_converter C{}; + + auto a = C(pts[0]); + auto b = C(it->point()); + auto c = C(pts[1]); + + SK::Vector_2 ab = b - a; + SK::Vector_2 ac = c - a; + ab /= std::sqrt(ab.squared_length()); + ac /= std::sqrt(ac.squared_length()); + + double d = ab * ac; + if (std::acos(d) < 1e-12) { + CGAL::remove_edge(arr, hes[0]); + CGAL::remove_edge(arr, hes[1]); + CGAL::insert(arr, Segment_2(pts[0], pts[1])); + removed_this_round = true; + break; + }; + } + } + if (!removed_this_round) { + break; + } + } +} + class timer { public: class entry { @@ -1266,7 +1749,7 @@ class timer { }; 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-2; + 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; @@ -1325,13 +1808,12 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v // // Inset-offset to remove tiny details that may cause enourmous spikes in offsets for (auto& r : input_polygons) { - smooth_polygon(-polygon_offset_distance / 10000., r); + smooth_polygon(polygon_offset_distance / 1000., r); } debug_output.write_polygons(input_polygons, "processed_input"); - SegmentLookup segment_lookup(input_polygons); - +#if 1 t0 = timer.start("outer perimeter"); // Find the outer perimeter using offset - union - negative offset @@ -1372,7 +1854,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v remove_close_points(fused_removed_close_points, 1.e-4); // Apply negative offset to get the outer perimeter polygon - auto inner_offset = create_and_convert_offset_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()), @@ -1380,14 +1862,37 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v -polygon_offset_distance - OUTER_PERIMITER_ADDITIONAL_INSET_AMOUNT, fused_removed_close_points); - debug_output.write_polygons(inner_offset, "outer_perimiter"); + 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"); // Subtract original polygons from outer perimeter std::vector difference_result, difference_result_subdivided; - for (auto& i : inner_offset) { + for (auto& i : outer_perimiter) { std::vector working_copy; working_copy.emplace_back(i); @@ -1433,6 +1938,8 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v debug_output.write_polygons(triangular_polygons, "triangulated_corridor"); + SegmentLookup segment_lookup(input_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) { @@ -1474,7 +1981,7 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v t0 = timer.start("topology"); - auto segments = extend_end_vertices_based_on_input(G, midpoint_to_segment, segment_to_input_facet, inner_offset, segment_lookup); + 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 @@ -1490,7 +1997,9 @@ 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(); @@ -1500,6 +2009,19 @@ void arrange_cgal_polygons(const std::vector& input_polygons_, std::v 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; @@ -1525,7 +2047,12 @@ 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 t0.stop(); From 0ce60f506197902e12fe04accaad5249028c074c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:45:12 +0000 Subject: [PATCH 111/131] Bump actions/download-artifact from 7.0.0 to 8.0.0 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7.0.0 to 8.0.0. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7.0.0...v8.0.0) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci-ifcopenshell-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-ifcopenshell-docker.yml b/.github/workflows/ci-ifcopenshell-docker.yml index e6668490c5..d887e2ef45 100644 --- a/.github/workflows/ci-ifcopenshell-docker.yml +++ b/.github/workflows/ci-ifcopenshell-docker.yml @@ -91,7 +91,7 @@ jobs: lfs: true - name: Download - uses: actions/download-artifact@v7.0.0 + uses: actions/download-artifact@v8.0.0 with: # Artifact name name: ifcos-artifacts From 5a27ec9814fc2b52e128e0ae64a30625478efce5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:45:20 +0000 Subject: [PATCH 112/131] Bump actions/upload-artifact from 6 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_osx.yml | 2 +- .github/workflows/build_pyodide.yml | 2 +- .github/workflows/build_rocky.yml | 2 +- .github/workflows/build_rocky_arm.yml | 2 +- .github/workflows/ci-ifcopenshell-docker.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index 0e15cdc830..0f6bb702e9 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -83,7 +83,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-osx-${{ matrix.arch }} path: | diff --git a/.github/workflows/build_pyodide.yml b/.github/workflows/build_pyodide.yml index cd162cd9ea..7da3bb408c 100644 --- a/.github/workflows/build_pyodide.yml +++ b/.github/workflows/build_pyodide.yml @@ -42,7 +42,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-pyodide path: | diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index 3fcd759877..710b918564 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -60,7 +60,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-rocky path: | diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index cced443ad8..b54e62ccef 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -60,7 +60,7 @@ jobs: - name: Upload Build Logs if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: build-logs-rocky-arm64 path: | diff --git a/.github/workflows/ci-ifcopenshell-docker.yml b/.github/workflows/ci-ifcopenshell-docker.yml index d887e2ef45..e10be5a18b 100644 --- a/.github/workflows/ci-ifcopenshell-docker.yml +++ b/.github/workflows/ci-ifcopenshell-docker.yml @@ -73,7 +73,7 @@ jobs: make package working-directory: build - name: Upload - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: # Artifact name name: ifcos-artifacts From 4f6051cb0a3d8b4c9d0f5fbc3655eb7fd5e062c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:45:26 +0000 Subject: [PATCH 113/131] Bump ruff from 0.15.2 to 0.15.4 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.2 to 0.15.4. - [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.2...0.15.4) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.4 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 4c4aeb331a..bdc9bf1a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "IfcOpenShell" version = "0.0.0" dependencies = [ "black==26.1.0", - "ruff==0.15.2", + "ruff==0.15.4", "poethepoet", "gersemi==0.26.0", ] From d4150e0558a49b32dc01cc8a436254268265e154 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 05:49:43 +0000 Subject: [PATCH 114/131] Bump svelte from 5.53.0 to 5.53.6 in /src/ifctester/webapp Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.0 to 5.53.6. - [Release notes](https://github.com/sveltejs/svelte/releases) - [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.6/packages/svelte) --- updated-dependencies: - dependency-name: svelte dependency-version: 5.53.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- src/ifctester/webapp/package-lock.json | 16 ++++++++-------- src/ifctester/webapp/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ifctester/webapp/package-lock.json b/src/ifctester/webapp/package-lock.json index 2e136aaffa..d94807f270 100644 --- a/src/ifctester/webapp/package-lock.json +++ b/src/ifctester/webapp/package-lock.json @@ -24,7 +24,7 @@ "clsx": "^2.1.1", "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", - "svelte": "^5.53.0", + "svelte": "^5.53.6", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", @@ -1375,9 +1375,9 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2775,9 +2775,9 @@ } }, "node_modules/svelte": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.0.tgz", - "integrity": "sha512-7dhHkSamGS2vtoBmIW2hRab+gl5Z60alEHZB4910ePqqJNxAWnDAxsofVmlZ2tREmWyHNE+A1nCKwICAquoD2A==", + "version": "5.53.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz", + "integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -2786,7 +2786,7 @@ "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", diff --git a/src/ifctester/webapp/package.json b/src/ifctester/webapp/package.json index b97ba2c8b5..bb7d58567e 100644 --- a/src/ifctester/webapp/package.json +++ b/src/ifctester/webapp/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "mode-watcher": "^1.1.0", "sass-embedded": "^1.89.0", - "svelte": "^5.53.0", + "svelte": "^5.53.6", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.0", "tailwind-variants": "^1.0.0", From d6c782aba56af812719cb409adf8ae8aaf3d611b Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 26 Feb 2026 13:05:53 +0100 Subject: [PATCH 115/131] Add newline handling with add_newline_between_words n SvgWriter for text literals --- src/bonsai/bonsai/bim/module/drawing/svgwriter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index 83566c4d0b..9b2910b9a0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -1023,7 +1023,8 @@ class SvgWriter: for text_literal in text_literals: text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element) - + if newline_at: + text = helper.add_newline_between_words(text, newline_at) text_segments = parse_markdown_it(text) if len(text_segments) == 1 and text_segments[0]["url"] is None and not text_segments[0].get("break", False): From 61cfa48c2c78e7340cf3cd368cb3d9c2654a285c Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Tue, 3 Mar 2026 01:48:03 -0600 Subject: [PATCH 116/131] docs: add BonsaiPR bleeding edge installation section (#7721) * Fix #7718: Fix FallDecorator label calculation for all slope annotation types - Fix wrong dict key type in decoration.py: DecoratorData.data["fall"] is keyed by obj.name (str) but was looked up with obj (Object), causing object_type to always be None - Apply obj.matrix_world transform to spline points before computing rise/run in both decoration.py and svgwriter.py; local coordinates have Z=0 for flat annotations, world coordinates correctly reflect elevation change - Use hypotenuse (segment_length) instead of run as the denominator for SLOPE_FRACTION label display Generated with the assistance of an AI coding tool. * docs: add BonsaiPR bleeding edge installation section Add new section to installation.rst documenting the BonsaiPR community build, including why it exists, how the automated PR-merging system works, installation steps with automated updates, manual installation, and the PR workflow for contributors. Generated with the assistance of an AI coding tool. * whoops --- .../docs/guides/development/installation.rst | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/bonsai/docs/guides/development/installation.rst b/src/bonsai/docs/guides/development/installation.rst index 3446617ab6..7355d09ca1 100644 --- a/src/bonsai/docs/guides/development/installation.rst +++ b/src/bonsai/docs/guides/development/installation.rst @@ -10,6 +10,8 @@ There are different methods of installation, depending on your situation. recommended for developers who are actively coding. 4. :ref:`guides/development/installation:Packaged installation` is recommended for those who use a package manager. +5. :ref:`guides/development/installation:BonsaiPR (Bleeding Edge) Installation` merges all open, non-draft PRs automatically. + System requirements ------------------- @@ -231,6 +233,95 @@ the `Makefile `__ in the ``dist`` target. + + +BonsaiPR (Bleeding Edge) Installation +-------------------------------------- + +**BonsaiPR** is a community-maintained build that automatically merges open pull +requests (PRs) from the IfcOpenShell repository into a single installable add-on. +It is intended for power users and testers who want to try the latest community +contributions before they are officially reviewed and merged. + +Why BonsaiPR Exists +~~~~~~~~~~~~~~~~~~~~ + +Many excellent PRs are submitted by contributors, but core maintainers have +limited time for timely reviews. As a result, PRs often sit unmerged, +contributors lose momentum, and valuable work risks being forgotten. + +BonsaiPR addresses this by providing a ``bleeding_edge`` build that merges all +open, non-draft PRs automatically. Power users can install this build to test +multiple PRs together, helping catch issues earlier and reducing the load on core +developers. + +.. warning:: + + You must enable either **Bonsai** or **BonsaiPR**, but **not both at the + same time**. Enabling both can cause conflicts or unexpected behaviour. To + switch between them, disable the active one before enabling the other. + +How It Works +~~~~~~~~~~~~~ + +On a regular basis (and whenever a PR is opened or modified), an automated +system: + +1. Clones the IfcOpenShell repository and merges all open, non-draft PRs. +2. Builds the resulting add-on for all supported platforms. +3. Publishes the result as a release on the `BonsaiPR releases page + `__. +4. The list of branches is also published on `falken10vdl's IfcOpenShell Fork + `__. + +Each release includes a full report listing which PRs were merged successfully, +which were skipped (e.g. drafts), and which failed due to conflicts with other +PRs. + +Installing BonsaiPR with Automated Updates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Open Blender and go to :menuselection:`Edit --> Preferences --> Add-ons`. + Disable **Bonsai** if it is currently enabled. + +2. Click on the **Get Extensions** tab in the left sidebar. + +3. In the top right, click the **Repositories** dropdown, then the **+ icon**, + and select **Add Remote Repository**. + +4. Enter the following URL:: + + https://raw.githubusercontent.com/falken10vdl/bonsaiPR/refs/heads/main/index.json + +5. Enable **Check for Updates on Startup**, then click **Create**. + +6. In the **Get Extensions** search bar, type ``bonsai`` and look for + **BonsaiPR**. Click **Install**. + +7. Go to :menuselection:`Edit --> Preferences --> Add-ons` and confirm that + **BonsaiPR** is enabled and **Bonsai** is disabled. + +8. Restart Blender. + +Blender will automatically check for updates to the BonsaiPR extension on +startup, so you will always have access to the latest bleeding edge build. + +Installing BonsaiPR Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you prefer to install manually, download the appropriate ``.zip`` file for +your platform from the `BonsaiPR releases page +`__: + +- **Linux (x64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-linux-x64.zip`` +- **macOS Intel (x64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-macos-x64.zip`` +- **macOS Apple Silicon (ARM64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-macos-arm64.zip`` +- **Windows (x64)**: ``bonsaiPR_py311-0.8.4-alphaYYMMDDHHMM-windows-x64.zip`` + +Then go to :menuselection:`Edit --> Preferences --> Get Extensions --> "V" Icon +(top right) --> Install from Disk` and select the downloaded zip. + + Add-on compatibility -------------------- From 1378919709945ef798f65e2a95b32ebaf3faf2dd Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:16:43 -0800 Subject: [PATCH 117/131] Fixes IfcLinearPlacement fallback position warning --- src/ifcgeom/mapping/IfcObjectPlacement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcgeom/mapping/IfcObjectPlacement.cpp b/src/ifcgeom/mapping/IfcObjectPlacement.cpp index 499d9d787b..66744d5571 100644 --- a/src/ifcgeom/mapping/IfcObjectPlacement.cpp +++ b/src/ifcgeom/mapping/IfcObjectPlacement.cpp @@ -126,7 +126,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) { if (fallback) { auto mapped_fallback = taxonomy::cast(map(fallback)); - if (mapped_fallback != result) { + if (!result->ccomponents().isApprox(mapped_fallback->ccomponents())) { Logger::Warning("Computed placement differs from fallback", inst); } } From 951ade4b578069028c0c7092e8d1f5eb36b7e356 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:50:33 -0800 Subject: [PATCH 118/131] Fix bug introduced in 65d5df78 --- .../mapping/IfcSectionedSolidHorizontal.cpp | 13 ++++++++++--- src/ifcgeom/mapping/IfcSectionedSurface.cpp | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index ad1e2e0d4c..f2556092b4 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -61,9 +61,16 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); - auto linear_placement = taxonomy::cast(map(csp)); - profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3)); - boost::optional rot(linear_placement->ccomponents().block<3,3>(0,0)); + Eigen::Vector3d po( + pbde->OffsetLateral().get_value_or(0.), + // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane + pbde->OffsetVertical().get_value_or(0.), + 0.); + + profile_offsets.push_back(po); + + auto axis2_placement_linear = taxonomy::cast(map(csp)); + boost::optional rot(axis2_placement_linear->ccomponents().block<3, 3>(0, 0)); profile_rotations.push_back(rot); } if (faces.size() != profile_offsets.size()) { diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp index b91b7b62c2..efdeda5757 100644 --- a/src/ifcgeom/mapping/IfcSectionedSurface.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp @@ -63,11 +63,18 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); - auto linear_placement = taxonomy::cast(map(csp)); - profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3)); - boost::optional rot(linear_placement->ccomponents().block<3, 3>(0, 0)); - profile_rotations.push_back(rot); - } + Eigen::Vector3d po( + pbde->OffsetLateral().get_value_or(0.), + // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane + pbde->OffsetVertical().get_value_or(0.), + 0.); + + profile_offsets.push_back(po); + + auto axis2_placement_linear = taxonomy::cast(map(csp)); + boost::optional rot(axis2_placement_linear->ccomponents().block<3, 3>(0, 0)); + profile_rotations.push_back(rot); + } #else return nullptr; #endif From 619848823cf4b313c90219d48683967e316b1406 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Mar 2026 15:41:39 +0500 Subject: [PATCH 119/131] Sort out imports --- src/bonsai/bonsai/bim/module/drawing/operator.py | 4 ++-- src/bonsai/bonsai/bim/module/project/operator.py | 8 -------- src/bonsai/bonsai/bim/module/project/ui.py | 1 - src/bonsai/bonsai/core/drawing.py | 2 +- src/bonsai/bonsai/tool/project.py | 4 ++-- src/bonsai/pyproject.toml | 8 +++++++- src/bonsai/test/tool/test_blender.py | 4 ++-- src/bonsai/test/tool/test_drawing.py | 4 ++-- src/bonsai/test/tool/test_project.py | 4 ++-- src/ifcopenshell-python/ifcopenshell/util/geolocation.py | 2 +- src/ifcpatch/ifcpatch/recipes/MergeProjects.py | 1 + 11 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 66a1482acc..582c7b062b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -18,6 +18,7 @@ import hashlib import json +import logging import multiprocessing import os import shutil @@ -39,7 +40,6 @@ from typing import ( import bmesh import bpy -import logging import ifcopenshell import ifcopenshell.api.document import ifcopenshell.api.pset @@ -58,9 +58,9 @@ from bpy_extras.io_utils import ImportHelper from lxml import etree from mathutils import Color, Vector -import bonsai.bim.import_ifc import bonsai.bim.export_ifc import bonsai.bim.handler +import bonsai.bim.import_ifc import bonsai.bim.module.drawing.sheeter as sheeter import bonsai.bim.module.drawing.svgwriter as svgwriter import bonsai.core.drawing as core diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 7bffc724e5..da3771ba9c 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -56,14 +56,6 @@ import bonsai.core.project as core import bonsai.tool as tool from bonsai.bim import export_ifc, import_ifc from bonsai.bim.ifc import IfcStore -from bonsai.bim.ui import IFCFileSelector -from bonsai.bim import import_ifc -from bonsai.bim import export_ifc -from math import radians -from pathlib import Path -from collections import defaultdict -from mathutils import Vector, Matrix -from bpy.app.handlers import persistent from bonsai.bim.module.model.decorator import FaceAreaDecorator, PolylineDecorator from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.project.data import LinksData, ProjectLibraryData diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 7029eb7227..1f00c1b2b4 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -29,7 +29,6 @@ import bonsai.tool as tool from bonsai.bim.helper import draw_attributes, prop_with_search from bonsai.bim.ifc import IfcStore from bonsai.bim.module.project.data import LinksData, ProjectData -from typing import TYPE_CHECKING if TYPE_CHECKING: from bonsai.bim.module.project.prop import ( diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 953f57e7f4..42b0f77fbb 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -19,7 +19,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Literal, Optional, Union if TYPE_CHECKING: import bpy diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 6e1c9b2186..dc47b977f8 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -18,10 +18,9 @@ from __future__ import annotations -import os import json +import os import shutil -import numpy as np from collections import defaultdict from math import radians from pathlib import Path @@ -32,6 +31,7 @@ import ifcopenshell import ifcopenshell.api.document import ifcopenshell.util.element import ifcopenshell.util.representation +import numpy as np from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES import bonsai.bim.schema diff --git a/src/bonsai/pyproject.toml b/src/bonsai/pyproject.toml index 398687a715..4ee183cba0 100644 --- a/src/bonsai/pyproject.toml +++ b/src/bonsai/pyproject.toml @@ -38,6 +38,12 @@ exclude = ["test*"] [tool.ruff] extend = "../../pyproject.toml" -lint.select = [ +lint.extend-select = [ "F401", # unused imports ] + +[tool.ruff.lint.isort] +known-first-party = [ + "test", + "bonsai", +] diff --git a/src/bonsai/test/tool/test_blender.py b/src/bonsai/test/tool/test_blender.py index 97e68b9d08..cd155b5dee 100644 --- a/src/bonsai/test/tool/test_blender.py +++ b/src/bonsai/test/tool/test_blender.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import tempfile +from pathlib import Path from typing import TYPE_CHECKING import bpy @@ -24,10 +26,8 @@ import pytest import bonsai.core.tool import bonsai.tool as tool -import tempfile from bonsai.tool.blender import Blender as subject from test.bim.bootstrap import NewFile -from pathlib import Path if TYPE_CHECKING: import bpy.stub_internal.rna_enums as rna_enums diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 3fa2e68cae..06091ff8c4 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -21,7 +21,6 @@ import xml.etree.ElementTree as ET from pathlib import Path import bpy -import pytest import ifcopenshell import ifcopenshell.api.drawing import ifcopenshell.api.group @@ -31,8 +30,9 @@ import ifcopenshell.guid import ifcopenshell.util.element import mathutils import numpy as np -from mathutils import Vector +import pytest from ifcopenshell.util.shape_builder import ShapeBuilder +from mathutils import Vector import bonsai.core.tool import bonsai.tool as tool diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 43dc61be8e..28ff090be6 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -16,10 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import json import contextlib +import json import tempfile -import numpy as np from pathlib import Path from tempfile import NamedTemporaryFile @@ -30,6 +29,7 @@ import ifcopenshell.api.document import ifcopenshell.api.root import ifcopenshell.api.unit import ifcpatch +import numpy as np from ifcpatch.recipes import Ifc2Sql import bonsai.core.tool diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index 42aa28ca51..47c8886691 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -18,7 +18,7 @@ import math from decimal import ROUND_HALF_UP, Decimal -from typing import NamedTuple, Optional, Union, Any +from typing import Any, NamedTuple, Optional, Union import numpy as np diff --git a/src/ifcpatch/ifcpatch/recipes/MergeProjects.py b/src/ifcpatch/ifcpatch/recipes/MergeProjects.py index ffb8e2e65b..4a06c3d253 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeProjects.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeProjects.py @@ -25,6 +25,7 @@ import ifcopenshell.util.geolocation import ifcopenshell.util.unit import numpy as np +import ifcpatch from ifcpatch.recipes.SetFalseOrigin import Patcher as SetFalseOrigin From 8743d5643eae51d0099fc98831f3a254a7726a23 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 3 Mar 2026 14:34:05 +0500 Subject: [PATCH 120/131] typing --- src/bonsai/bonsai/bim/module/project/data.py | 2 +- .../bonsai/bim/module/project/decorator.py | 12 ++-- .../bonsai/bim/module/project/operator.py | 64 ++++++++++++++----- src/bonsai/bonsai/bim/ui.py | 2 + src/bonsai/bonsai/tool/project.py | 11 ++-- .../recipes/ExtractPropertiesToSQLite.py | 14 ++-- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 1 - 7 files changed, 70 insertions(+), 36 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index 56a77d1880..8ecf30eef5 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -170,6 +170,6 @@ class ProjectLibraryData: class LinksData: - linked_data = {} + linked_data: dict[str, Any] = {} enable_culling = False is_loaded = False diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index 8c2ec0e08b..cb1ee4b4bd 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import Union - import bmesh import bpy import gpu @@ -54,7 +52,7 @@ class ProjectDecorator: installed = None @classmethod - def install(cls, context): + def install(cls, context: bpy.types.Context) -> None: if cls.installed: cls.uninstall() handler = cls() @@ -99,9 +97,9 @@ class ProjectDecorator: # general shader self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - selected_vertices = [] - selected_edges = [] - selected_tris = [] + selected_vertices: list[tuple[float, float, float]] = [] + selected_edges: list[tuple[int, int]] = [] + selected_tris: list[tuple[int, int, int]] = [] props = tool.Project.get_project_props() try: @@ -112,7 +110,7 @@ class ProjectDecorator: except: return - root_obj: Union[bpy.types.Object, None] = props.queried_obj_root + root_obj = props.queried_obj_root if root_obj and not (m := root_obj.matrix_world).is_identity: selected_vertices = [m @ Vector(v) for v in selected_vertices] diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index da3771ba9c..ea074a2e96 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -27,11 +27,12 @@ import traceback from collections import defaultdict from math import radians from pathlib import Path -from typing import TYPE_CHECKING, Literal, Union, get_args +from typing import TYPE_CHECKING, Any, Literal, Union, get_args import bpy import ifcopenshell import ifcopenshell.api.attribute +import ifcopenshell.api.document import ifcopenshell.api.nest import ifcopenshell.api.project import ifcopenshell.api.root @@ -1438,8 +1439,13 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Load Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Load the selected file" - link_index: bpy.props.IntProperty(name="Link Index") - use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) + + link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + link_index: int + use_cache: bool def _execute(self, context): self.link = tool.Project.get_project_props().links[self.link_index] @@ -1464,9 +1470,10 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator): empty = bpy.data.objects.new(empty_name, None) empty.instance_type = "COLLECTION" empty.instance_collection = collection - empty.matrix_world = Matrix(tool.Project.calculate_link_matrix(self.link)) + empty.matrix_world = tool.Project.calculate_link_matrix(self.link) tool.Project.set_link_empty_handle(self.link, empty) + assert bpy.context.scene bpy.context.scene.collection.objects.link(empty) self.link.is_loaded = True if tool.Ifc.get(): # For non-IFC projects, locking has no meaning @@ -1635,8 +1642,16 @@ class ToggleLinkVisibility(bpy.types.Operator): bl_label = "Toggle Link Visibility" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle visibility between SOLID and WIREFRAME" - link_index: bpy.props.IntProperty(name="Link Index") - mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) + + link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name="Visibility Mode", + items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")), + ) + + if TYPE_CHECKING: + link_index: int + mode: Literal["WIREFRAME", "VISIBLE"] def execute(self, context): props = tool.Project.get_project_props() @@ -1688,8 +1703,11 @@ class EnableEditingLink(bpy.types.Operator): def execute(self, context): link = tool.Project.get_project_props().active_link + assert link link.is_editing = True - tool.Geometry.unlock_object(tool.Project.get_link_empty_handle(link)) + obj = tool.Project.get_link_empty_handle(link) + assert obj + tool.Geometry.unlock_object(obj) return {"FINISHED"} @@ -1701,9 +1719,11 @@ class DisableEditingLink(bpy.types.Operator): def execute(self, context): link = tool.Project.get_project_props().active_link + assert link link.is_editing = False obj = tool.Project.get_link_empty_handle(link) - obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link)) + assert obj + obj.matrix_world = tool.Project.calculate_link_matrix(link) tool.Geometry.lock_object(obj) return {"FINISHED"} @@ -1716,8 +1736,10 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): link = tool.Project.get_project_props().active_link + assert link link.is_editing = False obj = tool.Project.get_link_empty_handle(link) + assert obj new_obj_matrix = obj.matrix_world filepath = Path(tool.Ifc.resolve_uri(link.filepath)) @@ -1753,7 +1775,7 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator): else: link.transformation = transformation - obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link)) + obj.matrix_world = tool.Project.calculate_link_matrix(link) tool.Geometry.lock_object(obj) @@ -2270,7 +2292,8 @@ class QueryLinkedElement(bpy.types.Operator): props = tool.Project.get_project_props() props.queried_obj = None - for area in bpy.context.screen.areas: + assert context.screen + for area in context.screen.areas: if area.type == "PROPERTIES": for region in area.regions: if region.type == "WINDOW": @@ -2278,6 +2301,7 @@ class QueryLinkedElement(bpy.types.Operator): elif area.type == "VIEW_3D": area.tag_redraw() + assert context.region and context.region_data region = context.region rv3d = context.region_data coord = (self.mouse_x, self.mouse_y) @@ -2294,18 +2318,20 @@ class QueryLinkedElement(bpy.types.Operator): guid = None guid_start_index = 0 - for i, guid_end_index in enumerate(obj["guid_ids"]): + guid_ids: list[int] = obj["guid_ids"] + for i, guid_end_index in enumerate(guid_ids): if face_index < guid_end_index: guid = obj["guids"][i] props.queried_obj = obj props.queried_obj_root = self.find_obj_root(obj, instance_matrix) - selected_tris = [] - selected_edges = [] - vert_indices = set() + selected_tris: list[tuple[int, ...]] = [] + selected_edges: list[tuple[int, ...]] = [] + vert_indices_set: set[int] = set() + assert isinstance(obj.data, bpy.types.Mesh) for polygon in obj.data.polygons[guid_start_index:guid_end_index]: - vert_indices.update(polygon.vertices) - vert_indices = list(vert_indices) + vert_indices_set.update(polygon.vertices) + vert_indices = list(vert_indices_set) vert_map = {k: v for v, k in enumerate(vert_indices)} selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices] for polygon in obj.data.polygons[guid_start_index:guid_end_index]: @@ -2319,13 +2345,14 @@ class QueryLinkedElement(bpy.types.Operator): break guid_start_index = guid_end_index + assert guid is not None self.db = sqlite3.connect(obj["db"]) self.c = self.db.cursor() self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") element = self.c.fetchone() - attributes = {} + attributes: dict[str, Any] = {} for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): if element[i + 1] is not None: attributes[attr] = element[i + 1] @@ -2415,6 +2442,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): return {"CANCELLED"} queried_obj = props.queried_obj + assert queried_obj ifc_file = tool.Ifc.get() linked_ifc_file: ifcopenshell.file @@ -2683,10 +2711,12 @@ class CreateClippingPlane(bpy.types.Operator): self.report({"INFO"}, "Maximum of six clipping planes allowed.") return {"FINISHED"} + assert context.screen for area in context.screen.areas: if area.type == "VIEW_3D": area.tag_redraw() + assert context.region and context.region_data region = context.region rv3d = context.region_data if rv3d: # Called from a 3D viewport diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 7bc27a20e0..1d0e8742dd 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -774,6 +774,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bsdd_load_preview_dictionaries: bool bsdd_load_inactive_dictionaries: bool bsdd_load_test_dictionaries: bool + bsdd_baseurl: str should_disable_undo_on_save: bool should_stream: bool should_always_cache: bool @@ -789,6 +790,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): mass_time_units_in_wizard: bool chain_filter_with_set_operations: bool save_metadata_blend_file: bool + metadata_blend_file_suffix: str decorator_font_scale: float def draw(self, context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index dc47b977f8..5e53223158 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -31,6 +31,7 @@ import ifcopenshell import ifcopenshell.api.document import ifcopenshell.util.element import ifcopenshell.util.representation +import ifcopenshell.util.shape_builder import numpy as np from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES @@ -45,7 +46,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore if TYPE_CHECKING: - from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings + from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings, Link HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"] @@ -61,20 +62,20 @@ class Project(bonsai.core.tool.Project): return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue] @classmethod - def get_link_empty_handle(cls, link) -> bpy.types.Object | None: + def get_link_empty_handle(cls, link: Link) -> bpy.types.Object | None: if tool.Ifc.get(): return tool.Ifc.get_object(tool.Ifc.get().by_id(link.ifc_definition_id)) return link.empty_handle @classmethod - def set_link_empty_handle(cls, link, empty: bpy.types.Object) -> None: + def set_link_empty_handle(cls, link: Link, empty: bpy.types.Object) -> None: if tool.Ifc.get(): tool.Ifc.link(tool.Ifc.get().by_id(link.ifc_definition_id), empty) else: link.empty_handle = empty @classmethod - def calculate_link_matrix(cls, link) -> None: + def calculate_link_matrix(cls, link: Link) -> Matrix: filepath = Path(tool.Ifc.resolve_uri(link.filepath)) with open(filepath.with_suffix(".ifc.cache.json"), "r") as f: metadata = json.load(f) @@ -99,7 +100,7 @@ class Project(bonsai.core.tool.Project): rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z") local_matrix = rot @ np.eye(4) local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")] - return np.linalg.inv(local_matrix) @ global_matrix + return Matrix(np.linalg.inv(local_matrix) @ global_matrix) @classmethod def append_all_types_from_template(cls, template: str) -> None: diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py index 0f9dc8544d..c7ca60610b 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py @@ -17,21 +17,24 @@ # along with IfcPatch. If not, see . +import logging import tempfile import ifcopenshell.util.element +import ifcpatch + try: import sqlite3 except: print("No SQLite support") -class Patcher: +class Patcher(ifcpatch.BasePatcher): def __init__( self, - file, - logger, + file: ifcopenshell.file, + logger: logging.Logger | None = None, ): """Extracts properties and relationships from a IFC-SPF model to SQLite. @@ -45,10 +48,11 @@ class Patcher: result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"}) ifcpatch.write(result, "output.sqlite") """ - self.file = file - self.logger = logger + super().__init__(file, logger) def patch(self): + import sqlite3 + tmp = tempfile.NamedTemporaryFile(delete=False) db_file = tmp.name self.db = sqlite3.connect(db_file) diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 256ffa99ce..d1405e9a33 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -125,7 +125,6 @@ class Patcher(ifcpatch.BasePatcher): ) """ super().__init__(file, logger) - self.logger = logger self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower() self.host = host self.username = username From a26dbe252af0cdc869ebe2fbf7e715525e631132 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Mar 2026 14:56:53 +0500 Subject: [PATCH 121/131] bim.append_inspected_linked_element - fix missing UNDO --- src/bonsai/bonsai/bim/module/project/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index ea074a2e96..4124257dcc 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2426,7 +2426,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): bl_idname = "bim.append_inspected_linked_element" bl_label = "Append Inspected Linked Element" bl_description = "Append inspected linked element" - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "UNDO"} def _execute(self, context): from bonsai.bim.module.project.data import LinksData From 9005333f539600ae4d3b1e2da23c58f230d270ce Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 15:17:35 +0500 Subject: [PATCH 122/131] ifcpatch MergeProjects - make `logger` arg optional --- src/ifcpatch/ifcpatch/recipes/MergeProjects.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ifcpatch/ifcpatch/recipes/MergeProjects.py b/src/ifcpatch/ifcpatch/recipes/MergeProjects.py index 4a06c3d253..9d20164424 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeProjects.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeProjects.py @@ -16,9 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcPatch. If not, see . +from collections.abc import Sequence from logging import Logger from typing import Union +import ifcpatch import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.geolocation @@ -29,8 +31,13 @@ import ifcpatch from ifcpatch.recipes.SetFalseOrigin import Patcher as SetFalseOrigin -class Patcher: - def __init__(self, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]): +class Patcher(ifcpatch.BasePatcher): + def __init__( + self, + file: ifcopenshell.file, + logger: Logger | None = None, + filepaths: Sequence[Union[str, ifcopenshell.file]] = (), + ): """Merge two or more IFC models into one Note that other than combining the two (or more) IfcProject elements into @@ -51,8 +58,7 @@ class Patcher: ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]}) """ - self.file = file - self.logger = logger + super().__init__(file, logger) self.filepaths = filepaths def patch(self): @@ -62,6 +68,8 @@ class Patcher: "replace it with a list of file/filepaths." ) self.filepaths = [self.filepaths] + if len(self.filepaths) == 0: + raise ValueError("At least one file/filepath must be provided to merge with the main model.") for filepath in self.filepaths: if isinstance(filepath, ifcopenshell.file): other = filepath From f102c7c1b44c6cc0e33c173090dc1ea15a8e2f29 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 17:05:37 +0500 Subject: [PATCH 123/131] ifcopenshell-python makefile - add note about `PYNUMBER` --- src/ifcopenshell-python/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index b6cfe15b17..90c503d145 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -76,6 +76,7 @@ IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINAR .PHONY: build-urls build-urls: @echo "You can provide one of the 4 platforms (linux64, macos64, macosm164, win64) using 'PLATFORM=xxx'." + @echo "And Python version using 'PYNUMBER=xx' (e.g. 'PYNUMBER=311')." @echo ${IOS_URL} @echo ${IFCCONVERT_URL} From 3a54e808f6f3b2bcc8eaf392854b98e8f29456ed Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 27 Feb 2026 16:55:42 +0500 Subject: [PATCH 124/131] dev_environment python - create user site packages folder if missing E.g. it might be missing if Python was just installed. Also print paths first before symlinking, making it easier to debug. --- src/ifcopenshell-python/scripts/dev_environment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/scripts/dev_environment.py b/src/ifcopenshell-python/scripts/dev_environment.py index db65020048..8058a58b92 100644 --- a/src/ifcopenshell-python/scripts/dev_environment.py +++ b/src/ifcopenshell-python/scripts/dev_environment.py @@ -11,6 +11,7 @@ import site from pathlib import Path SITE = Path(site.getusersitepackages()) +SITE.mkdir(parents=True, exist_ok=True) REPO_PATH = Path(__file__).parent.parent.parent.parent REPO_PATH_SRC = REPO_PATH / "src" assert REPO_PATH_SRC.exists(), f"'{REPO_PATH_SRC}' doesn't exist." @@ -36,8 +37,8 @@ for package, repo_package_path in packages.items(): if package_path.exists(): # I guess it's a directory. shutil.rmtree(package_path) - package_path.symlink_to(repo_package_path, True) print(f"Symlinking {package_path} -> {repo_package_path}") + package_path.symlink_to(repo_package_path, True) PACKAGE_PATH = SITE / "ifcopenshell" From a52a32919784f97966d17897702c2fc26f2403ba Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 3 Mar 2026 14:29:40 +0500 Subject: [PATCH 125/131] Update note on Blender upstream issue Fix was included in 4.5.7 (see 141496 bug in https://projects.blender.org/blender/blender/issues/141871) --- src/bonsai/bonsai/bim/module/project/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 4124257dcc..3590f79d8f 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2861,7 +2861,7 @@ class IFCFileHandlerOperator(bpy.types.Operator): def clean_up_path(path: str) -> str: # In Blender 4.5.6 there was a bug producing unncesseary double slash prefix - # breaking the paths. Issue is not present in 5.0+ and presumably will be solved in 4.5.7 too. + # breaking the paths. Issue is not present in 5.0+ and is fixed in 4.5.7. # https://projects.blender.org/blender/blender/issues/153822 if bpy.app.version == (4, 5, 6): blender_prefix = "//" From adaf33b74f3e069f898200440a844063268abefa Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Mar 2026 17:23:46 +0500 Subject: [PATCH 126/131] project.operator - reuse `ray_cast` method --- .../bonsai/bim/module/project/operator.py | 16 ++++------------ src/bonsai/bonsai/tool/blender.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 3590f79d8f..2bd731a93d 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2307,7 +2307,9 @@ class QueryLinkedElement(bpy.types.Operator): coord = (self.mouse_x, self.mouse_y) origin = region_2d_to_origin_3d(region, rv3d, coord) direction = region_2d_to_vector_3d(region, rv3d, coord) - hit, location, normal, face_index, obj, instance_matrix = self.ray_cast(context, origin, direction) + hit, location, normal, face_index, obj, instance_matrix = tool.Blender.ray_cast_scene( + context, origin, direction + ) if not hit: self.report({"INFO"}, "No object found.") return {"FINISHED"} @@ -2399,11 +2401,6 @@ class QueryLinkedElement(bpy.types.Operator): ProjectDecorator.install(bpy.context) return {"FINISHED"} - def ray_cast(self, context: bpy.types.Context, origin: Vector, direction: Vector): - depsgraph = context.evaluated_depsgraph_get() - result = context.scene.ray_cast(depsgraph, origin, direction) - return result - def find_obj_root(self, obj: bpy.types.Object, matrix: Matrix) -> Union[bpy.types.Object, None]: collections = set(obj.users_collection) for o in bpy.data.objects: @@ -2723,7 +2720,7 @@ class CreateClippingPlane(bpy.types.Operator): coord = (self.mouse_x, self.mouse_y) origin = region_2d_to_origin_3d(region, rv3d, coord) direction = region_2d_to_vector_3d(region, rv3d, coord) - hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction) + hit, location, normal, face_index, obj, matrix = tool.Blender.ray_cast_scene(context, origin, direction) if not hit: self.report({"INFO"}, "No object found.") return {"FINISHED"} @@ -2758,11 +2755,6 @@ class CreateClippingPlane(bpy.types.Operator): bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") return {"FINISHED"} - def ray_cast(self, context, origin, direction): - depsgraph = context.evaluated_depsgraph_get() - result = context.scene.ray_cast(depsgraph, origin, direction) - return result - def invoke(self, context, event): self.mouse_x = event.mouse_region_x self.mouse_y = event.mouse_region_y diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 9c01ce8c20..06082ebea2 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -2184,3 +2184,20 @@ class Blender(bonsai.core.tool.Blender): for f in files if (Path(directory) / f.name).is_file() ] + + @classmethod + def ray_cast_scene( + cls, + context: bpy.types.Context, + origin: Vector, + direction: Vector, + ) -> tuple[bool, Vector, Vector, int, bpy.types.Object, Matrix]: + depsgraph = context.evaluated_depsgraph_get() + assert context.scene + # `matrix` is just `obj.matrix_world`. + result = context.scene.ray_cast( + depsgraph, + origin, + direction, + ) + return result From ecc82a52f5b5884e86e4d0749e9d824e39416b86 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Mar 2026 17:57:34 +0500 Subject: [PATCH 127/131] ExtractPropertiesToSQLite - add typing for created columns --- .../bonsai/bim/module/project/operator.py | 21 ++++-- .../recipes/ExtractPropertiesToSQLite.py | 74 +++++++++++++------ 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 2bd731a93d..d5dc0f0a5b 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2287,6 +2287,11 @@ class QueryLinkedElement(bpy.types.Operator): region_2d_to_origin_3d, region_2d_to_vector_3d, ) + from ifcpatch.recipes.ExtractPropertiesToSQLite import ( + ElementRow, + PropertyRow, + RelationshipRow, + ) LinksData.linked_data = {} props = tool.Project.get_project_props() @@ -2352,7 +2357,7 @@ class QueryLinkedElement(bpy.types.Operator): self.c = self.db.cursor() self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") - element = self.c.fetchone() + element = ElementRow(*self.c.fetchone()) attributes: dict[str, Any] = {} for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): @@ -2360,14 +2365,14 @@ class QueryLinkedElement(bpy.types.Operator): attributes[attr] = element[i + 1] self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],)) - rows = self.c.fetchall() + rows = [PropertyRow(*row) for row in self.c.fetchall()] - properties = {} + properties: defaultdict[str, dict[str, str]] = defaultdict(dict) for row in rows: - properties.setdefault(row[1], {})[row[2]] = row[3] + properties[row.pset_name][row.name] = row.value self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],)) - relationships = self.c.fetchall() + relationships = [RelationshipRow(*row) for row in self.c.fetchall()] relating_type_id = None @@ -2375,12 +2380,12 @@ class QueryLinkedElement(bpy.types.Operator): if relationship[1] == "IfcRelDefinesByType": relating_type_id = relationship[2] - type_properties = {} + type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict) if relating_type_id is not None: self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,)) - rows = self.c.fetchall() + rows = [PropertyRow(*row) for row in self.c.fetchall()] for row in rows: - type_properties.setdefault(row[1], {})[row[2]] = row[3] + type_properties[row.pset_name][row.name] = row.value LinksData.linked_data = { "attributes": attributes, diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py index c7ca60610b..c91467036f 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py @@ -19,17 +19,40 @@ import logging import tempfile +from typing import NamedTuple import ifcopenshell.util.element import ifcpatch try: - import sqlite3 + import sqlite3 # noqa: F401 except: print("No SQLite support") +class ElementRow(NamedTuple): + element_id: int + guid: str + class_: str + predefined_type: str | None + name: str | None + description: str | None + + +class PropertyRow(NamedTuple): + element_id: int + pset_name: str + name: str + value: str + + +class RelationshipRow(NamedTuple): + element_id: int + rel_ifc_class: str + to_id: int + + class Patcher(ifcpatch.BasePatcher): def __init__( self, @@ -94,20 +117,21 @@ class Patcher(ifcpatch.BasePatcher): elements = self.file.by_type("IfcObjectDefinition") - rows = [] - properties = [] - relationships = [] + rows: list[ElementRow] = [] + properties: list[PropertyRow] = [] + relationships: list[RelationshipRow] = [] id_map = {e.id(): i for i, e in enumerate(elements)} + for i, element in enumerate(elements): rows.append( - [ + ElementRow( i, element[0], # IfcRoot.GlobalId element.is_a(), ifcopenshell.util.element.get_predefined_type(element), element[2], # IfcRoot.Name element[3], # IfcRoot.Description - ] + ) ) psets = ifcopenshell.util.element.get_psets(element, should_inherit=False) for pset_name, pset_data in psets.items(): @@ -118,49 +142,55 @@ class Patcher(ifcpatch.BasePatcher): value = "True" if value else "False" elif not isinstance(value, str): value = str(value) - properties.append([i, pset_name, prop_name, value]) + properties.append(PropertyRow(i, pset_name, prop_name, value)) material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) if material: name = getattr(material, "Name", getattr(material, "LayerSetName", None)) or "Unnamed" - properties.append([i, "IFC Material", "Name", name]) - properties.append([i, "IFC Material", "Class", material.is_a()]) + properties.append(PropertyRow(i, "IFC Material", "Name", name)) + properties.append(PropertyRow(i, "IFC Material", "Class", material.is_a())) if material.is_a("IfcMaterial"): materials = [] elif material.is_a("IfcMaterialLayerSet"): for idx, item in enumerate(material.MaterialLayers or []): material = item.Material - properties.append([i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)]) - properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name]) + properties.append( + PropertyRow(i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)) + ) + properties.append(PropertyRow(i, "IFC Material", f"Layer {idx + 1} Material", material.Name)) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category]) + properties.append(PropertyRow(i, "IFC Material", f"Layer {idx + 1} Category", category)) elif material.is_a("IfcMaterialProfileSet"): for idx, item in enumerate(material.MaterialProfiles or []): material = item.Material - properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name]) - properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name]) + properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Name", item.Name)) + properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Material", material.Name)) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category]) + properties.append(PropertyRow(i, "IFC Material", f"Profile {idx + 1} Category", category)) elif material.is_a("IfcMaterialConstituentSet"): for idx, item in enumerate(material.MaterialConstituents or []): material = item.Material - properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name]) - properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.Name]) + properties.append(PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Name", item.Name)) + properties.append( + PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Material", material.Name) + ) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Constituent {idx + 1} Category", category]) + properties.append( + PropertyRow(i, "IFC Material", f"Constituent {idx + 1} Category", category) + ) elif material.is_a("IfcMaterialList"): for idx, material in enumerate(material.Materials): - properties.append([i, "IFC Material", f"Material {idx + 1} Name", material.Name]) + properties.append(PropertyRow(i, "IFC Material", f"Material {idx + 1} Name", material.Name)) if category := getattr(material, "Category", None): - properties.append([i, "IFC Material", f"Material {idx + 1} Category", category]) + properties.append(PropertyRow(i, "IFC Material", f"Material {idx + 1} Category", category)) layers = ifcopenshell.util.element.get_layers(self.file, element) for idx, layer in enumerate(layers): - properties.append([i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name]) + properties.append(PropertyRow(i, "IFC Presentation Layer Assignment", f"Layer {idx + 1}", layer.Name)) relating_type = ifcopenshell.util.element.get_type(element) if relating_type and relating_type != element: - relationships.append([i, "IfcRelDefinesByType", id_map[relating_type.id()]]) + relationships.append(RelationshipRow(i, "IfcRelDefinesByType", id_map[relating_type.id()])) self.c.executemany("INSERT INTO elements VALUES (?, ?, ?, ?, ?, ?);", rows) self.c.executemany("INSERT INTO properties VALUES (?, ?, ?, ?);", properties) From 9d0c172a532c74fbd92b1adc4fa40ef98d9adc3a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Mar 2026 15:26:27 +0500 Subject: [PATCH 128/131] bim.select_linked_model_element Refactored methods for accessing objects in linked models and added a simple operator to select object in linked model by providing guid. A quick demo - https://files.catbox.moe/sjjw37.mp4 --- .../bonsai/bim/module/project/__init__.py | 1 + .../bonsai/bim/module/project/operator.py | 156 +++++----------- src/bonsai/bonsai/bim/module/project/ui.py | 1 + src/bonsai/bonsai/tool/project.py | 174 +++++++++++++++++- 4 files changed, 219 insertions(+), 113 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index f4f4261eb5..81eae4902a 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -66,6 +66,7 @@ classes = ( operator.RewindLibrary, operator.SaveLibraryFile, operator.SelectLibraryFile, + operator.SelectLinkedModelElement, operator.SelectLinkHandle, operator.ToggleFilterCategories, operator.ToggleLinkSelectability, diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index d5dc0f0a5b..d56ee9dfaa 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -27,7 +27,7 @@ import traceback from collections import defaultdict from math import radians from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Union, get_args +from typing import TYPE_CHECKING, Literal, Union, get_args import bpy import ifcopenshell @@ -49,6 +49,10 @@ import ifcopenshell.util.unit import numpy as np from bpy.app.handlers import persistent from bpy_extras.io_utils import ExportHelper, ImportHelper +from bpy_extras.view3d_utils import ( + region_2d_to_origin_3d, + region_2d_to_vector_3d, +) from mathutils import Matrix, Vector import bonsai.bim.handler @@ -1797,6 +1801,43 @@ class SelectLinkHandle(bpy.types.Operator): return {"FINISHED"} +class SelectLinkedModelElement(bpy.types.Operator): + bl_idname = "bim.select_linked_model_element" + bl_label = "Select Linked Model Element" + 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] + + if TYPE_CHECKING: + guid: str + + def invoke(self, context, event): + assert context.window_manager + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + guid = self.guid.strip() + if not guid: + self.report({"ERROR"}, "GlobalId is not provided.") + return {"CANCELLED"} + + props = tool.Project.get_project_props() + active_link = props.active_link + assert active_link is not None + assert active_link.is_loaded + + guid_obj = tool.Project.Link.get_obj_by_guid(active_link, guid) + if not guid_obj: + filepath = active_link.filepath + self.report({"INFO"}, f"Element with GlobalId '{guid}' not found in the linked model at '{filepath}'.") + return {"CANCELLED"} + + tool.Project.Link.select_linked_element(context, guid_obj, guid) + self.report({"INFO"}, f"Element with GlobalId '{guid}' is selected.") + return {"FINISHED"} + + class ExportIFC(bpy.types.Operator, ExportHelper): bl_idname = "bim.save_project" bl_label = "Save IFC" @@ -2278,21 +2319,10 @@ class QueryLinkedElement(bpy.types.Operator): @classmethod def poll(cls, context): + assert context.area return context.area.type == "VIEW_3D" - def execute(self, context): - import sqlite3 - - from bpy_extras.view3d_utils import ( - region_2d_to_origin_3d, - region_2d_to_vector_3d, - ) - from ifcpatch.recipes.ExtractPropertiesToSQLite import ( - ElementRow, - PropertyRow, - RelationshipRow, - ) - + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: LinksData.linked_data = {} props = tool.Project.get_project_props() props.queried_obj = None @@ -2319,105 +2349,18 @@ class QueryLinkedElement(bpy.types.Operator): self.report({"INFO"}, "No object found.") return {"FINISHED"} - if "guids" not in obj: + if not tool.Project.Link.is_linked_element(obj): self.report({"INFO"}, "Object is not a linked IFC element.") return {"FINISHED"} - guid = None - guid_start_index = 0 - guid_ids: list[int] = obj["guid_ids"] - for i, guid_end_index in enumerate(guid_ids): - if face_index < guid_end_index: - guid = obj["guids"][i] - props.queried_obj = obj - props.queried_obj_root = self.find_obj_root(obj, instance_matrix) - - selected_tris: list[tuple[int, ...]] = [] - selected_edges: list[tuple[int, ...]] = [] - vert_indices_set: set[int] = set() - assert isinstance(obj.data, bpy.types.Mesh) - for polygon in obj.data.polygons[guid_start_index:guid_end_index]: - vert_indices_set.update(polygon.vertices) - vert_indices = list(vert_indices_set) - vert_map = {k: v for v, k in enumerate(vert_indices)} - selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices] - for polygon in obj.data.polygons[guid_start_index:guid_end_index]: - selected_tris.append(tuple(vert_map[v] for v in polygon.vertices)) - selected_edges.extend(tuple([vert_map[vi] for vi in e] for e in polygon.edge_keys)) - - obj["selected_vertices"] = selected_vertices - obj["selected_edges"] = selected_edges - obj["selected_tris"] = selected_tris - - break - guid_start_index = guid_end_index - + guid = tool.Project.Link.get_guid_by_face_index(obj, face_index) assert guid is not None - self.db = sqlite3.connect(obj["db"]) - self.c = self.db.cursor() - - self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") - element = ElementRow(*self.c.fetchone()) - - attributes: dict[str, Any] = {} - for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): - if element[i + 1] is not None: - attributes[attr] = element[i + 1] - - self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],)) - rows = [PropertyRow(*row) for row in self.c.fetchall()] - - properties: defaultdict[str, dict[str, str]] = defaultdict(dict) - for row in rows: - properties[row.pset_name][row.name] = row.value - - self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],)) - relationships = [RelationshipRow(*row) for row in self.c.fetchall()] - - relating_type_id = None - - for relationship in relationships: - if relationship[1] == "IfcRelDefinesByType": - relating_type_id = relationship[2] - - type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict) - if relating_type_id is not None: - self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,)) - rows = [PropertyRow(*row) for row in self.c.fetchall()] - for row in rows: - type_properties[row.pset_name][row.name] = row.value - - LinksData.linked_data = { - "attributes": attributes, - "properties": [(k, properties[k]) for k in sorted(properties.keys())], - "type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())], - } - self.db.close() - - for area in bpy.context.screen.areas: - if area.type == "PROPERTIES": - for region in area.regions: - if region.type == "WINDOW": - region.tag_redraw() - elif area.type == "VIEW_3D": - area.tag_redraw() + tool.Project.Link.select_linked_element(context, obj, guid) self.report({"INFO"}, f"Loaded data for {guid}") ProjectDecorator.install(bpy.context) return {"FINISHED"} - def find_obj_root(self, obj: bpy.types.Object, matrix: Matrix) -> Union[bpy.types.Object, None]: - collections = set(obj.users_collection) - for o in bpy.data.objects: - if ( - o.type != "EMPTY" - or o.instance_type != "COLLECTION" - or o.instance_collection not in collections - or not np.allclose(matrix, o.matrix_world, atol=1e-4) - ): - continue - return o - def invoke(self, context, event): self.mouse_x = event.mouse_region_x self.mouse_y = event.mouse_region_y @@ -2702,11 +2645,6 @@ class CreateClippingPlane(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - from bpy_extras.view3d_utils import ( - region_2d_to_origin_3d, - region_2d_to_vector_3d, - ) - # Clean up deleted planes props = tool.Project.get_project_props() if len(props.clipping_planes) > 5: diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 1f00c1b2b4..d86dd3cf5b 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -490,6 +490,7 @@ class BIM_PT_links(Panel): row.operator("bim.disable_editing_link", text="", icon="CANCEL") else: row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL") + row.operator("bim.select_linked_model_element", icon="VIEWZOOM", text="") row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 5e53223158..b9a7a68a0c 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -24,7 +24,7 @@ import shutil from collections import defaultdict from math import radians from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple, Optional +from typing import TYPE_CHECKING, Any, NamedTuple, Optional import bpy import ifcopenshell @@ -34,6 +34,7 @@ import ifcopenshell.util.representation import ifcopenshell.util.shape_builder import numpy as np from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES +from mathutils import Matrix import bonsai.bim.schema import bonsai.core.aggregate @@ -46,7 +47,11 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore if TYPE_CHECKING: - from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings, Link + from bonsai.bim.module.project.prop import ( + BIMProjectProperties, + Link, + MeasureToolSettings, + ) HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"] @@ -497,8 +502,8 @@ class Project(bonsai.core.tool.Project): ) @classmethod - def get_clipping_planes_normals(cls): - normals = [] + def get_clipping_planes_normals(cls) -> list[tuple[Vector, Vector]]: + normals: list[tuple[Vector, Vector]] = [] for clipping_plane in tool.Project.get_project_props().clipping_planes: plane = clipping_plane.obj if not plane or not plane.data: @@ -507,6 +512,7 @@ class Project(bonsai.core.tool.Project): if plane.mode == "EDIT": continue # A profile decorator or something else is used here. + assert isinstance(plane.data, bpy.types.Mesh) v1 = plane.matrix_world @ plane.data.vertices[0].co v2 = plane.matrix_world @ plane.data.vertices[1].co v3 = plane.matrix_world @ plane.data.vertices[2].co @@ -589,3 +595,163 @@ class Project(bonsai.core.tool.Project): ifc_file = tool.Ifc.get() ifcopenshell.api.document.remove_information(ifc_file, information=doc) + + class Link: + """Tools for working with linked models.""" + + @classmethod + def is_linked_element(cls, obj: bpy.types.Object) -> bool: + return "guids" in obj + + @classmethod + def get_obj_by_guid(cls, link: Link, guid: str) -> bpy.types.Object | None: + assert link.is_loaded + + handle = tool.Project.get_link_empty_handle(link) + assert handle + col = handle.instance_collection + assert col + + guid_obj = None + for obj in col.objects: + obj_guids: list[str] = obj["guids"] + if guid in obj_guids: + guid_obj = obj + break + + return guid_obj + + @classmethod + def get_guid_by_face_index(cls, obj: bpy.types.Object, face_index: int) -> str | None: + guids: list[str] = obj["guids"] + guid_ids: list[int] = obj["guid_ids"] + for guid, guid_end_index in zip(guids, guid_ids): + if face_index < guid_end_index: + return guid + + @classmethod + def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None: + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + obj_guids: list[str] = obj["guids"] + obj_guid_ids: list[int] = obj["guid_ids"] + + index = obj_guids.index(guid) + guid_end_index = obj_guid_ids[index] + if index > 0: + guid_start_index = obj_guid_ids[index - 1] + else: + guid_start_index = 0 + guid_polygons = mesh.polygons[guid_start_index:guid_end_index] + + selected_tris: list[tuple[int, ...]] = [] + selected_edges: list[tuple[int, ...]] = [] + + # Restart verts indices for our polygons. + guid_vertices_set: set[int] = set() + for polygon in guid_polygons: + guid_vertices_set.update(polygon.vertices) + vert_map = {k: v for v, k in enumerate(guid_vertices_set)} + + selected_vertices = [obj.matrix_world @ mesh.vertices[vi].co for vi in vert_map] + for polygon in guid_polygons: + selected_tris.append(tuple(vert_map[vi] for vi in polygon.vertices)) + selected_edges.extend(tuple([vert_map[vi] for vi in e]) for e in polygon.edge_keys) + + obj["selected_vertices"] = selected_vertices + obj["selected_edges"] = selected_edges + obj["selected_tris"] = selected_tris + + @classmethod + def select_linked_element( + cls, + context: bpy.types.Context, + obj: bpy.types.Object, + guid: str, + instance_matrix: Matrix | None = None, + ) -> None: + import sqlite3 + + from ifcpatch.recipes.ExtractPropertiesToSQLite import ( + ElementRow, + PropertyRow, + RelationshipRow, + ) + + from bonsai.bim.module.project.data import LinksData + from bonsai.bim.module.project.decorator import ProjectDecorator + + # Not sure if there's a difference between `instance_matrix` coming from `ray_cast` + # and usual `matrix_world`, maybe we can just get it from object always. + if instance_matrix is None: + instance_matrix = obj.matrix_world + + props = tool.Project.get_project_props() + props.queried_obj = obj + props.queried_obj_root = cls.find_obj_root(obj, instance_matrix) + + cls.select_linked_element_geom(obj, guid) + db = sqlite3.connect(obj["db"]) + c = db.cursor() + + c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") + element = ElementRow(*c.fetchone()) + + attributes: dict[str, Any] = {} + for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): + if element[i + 1] is not None: + attributes[attr] = element[i + 1] + + c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],)) + rows = [PropertyRow(*row) for row in c.fetchall()] + + properties: defaultdict[str, dict[str, str]] = defaultdict(dict) + for row in rows: + properties[row.pset_name][row.name] = row.value + + c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],)) + relationships = [RelationshipRow(*row) for row in c.fetchall()] + + relating_type_id = None + + for relationship in relationships: + if relationship[1] == "IfcRelDefinesByType": + relating_type_id = relationship[2] + + type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict) + if relating_type_id is not None: + c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,)) + rows = [PropertyRow(*row) for row in c.fetchall()] + for row in rows: + type_properties[row.pset_name][row.name] = row.value + + LinksData.linked_data = { + "attributes": attributes, + "properties": [(k, properties[k]) for k in sorted(properties.keys())], + "type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())], + } + db.close() + + assert context.screen + for area in context.screen.areas: + if area.type == "PROPERTIES": + for region in area.regions: + if region.type == "WINDOW": + region.tag_redraw() + elif area.type == "VIEW_3D": + area.tag_redraw() + + ProjectDecorator.install(context) + + @classmethod + def find_obj_root(cls, obj: bpy.types.Object, matrix: Matrix) -> bpy.types.Object | None: + collections = set(obj.users_collection) + for o in bpy.data.objects: + if ( + o.type != "EMPTY" + or o.instance_type != "COLLECTION" + or o.instance_collection not in collections + or not np.allclose(matrix, o.matrix_world, atol=1e-4) + ): + continue + return o From 3a59425a6453292c209f0698e4741c2050e1ea8c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Mar 2026 16:59:15 +0500 Subject: [PATCH 129/131] Linked IFC models - hotkey to hide selected geometry Demo - https://files.catbox.moe/aok74w.mp4 --- .../bonsai/bim/module/project/__init__.py | 1 + .../bonsai/bim/module/project/decorator.py | 20 +-- .../bonsai/bim/module/project/operator.py | 49 +++++- src/bonsai/bonsai/bim/module/project/prop.py | 2 + .../bonsai/bim/module/project/workspace.py | 14 +- src/bonsai/bonsai/tool/blender.py | 14 +- src/bonsai/bonsai/tool/project.py | 149 ++++++++++++++++-- .../ifcopenshell/api/project/append_asset.py | 2 +- 8 files changed, 219 insertions(+), 32 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 81eae4902a..db642c18c2 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -46,6 +46,7 @@ classes = ( operator.EnableEditingLink, operator.ExportIFC, operator.FlipClippingPlane, + operator.HideQueriedLinkedElement, operator.IFCFileHandlerOperator, operator.ImageScalingTool, operator.LinkIfc, diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index cb1ee4b4bd..72090a769b 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -97,26 +97,20 @@ class ProjectDecorator: # general shader self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - selected_vertices: list[tuple[float, float, float]] = [] - selected_edges: list[tuple[int, int]] = [] - selected_tris: list[tuple[int, int, int]] = [] - props = tool.Project.get_project_props() - try: - obj = props.queried_obj - selected_vertices = obj["selected_vertices"] - selected_edges = obj["selected_edges"] - selected_tris = obj["selected_tris"] - except: + obj = props.queried_obj + if obj is None: return + geom = tool.Project.Link.get_selected_geometry(obj) + selected_vertices = geom.selected_vertices root_obj = props.queried_obj_root if root_obj and not (m := root_obj.matrix_world).is_identity: selected_vertices = [m @ Vector(v) for v in selected_vertices] - if selected_edges: - self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges) - self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris) + if geom.selected_edges: + self.draw_batch("LINES", selected_vertices, selected_elements_color, geom.selected_edges) + self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), geom.selected_tris) class ClippingPlaneDecorator: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index d56ee9dfaa..a0768e0cf5 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -53,7 +53,7 @@ from bpy_extras.view3d_utils import ( region_2d_to_origin_3d, region_2d_to_vector_3d, ) -from mathutils import Matrix, Vector +from mathutils import Vector import bonsai.bim.handler import bonsai.bim.helper @@ -2367,6 +2367,53 @@ class QueryLinkedElement(bpy.types.Operator): return self.execute(context) +class HideQueriedLinkedElement(bpy.types.Operator): + bl_idname = "bim.hide_queried_linked_element" + bl_label = "Hide Queried Linked Element" + bl_description = ( + "Hide geometry for currently queried linked element.\n\n" + "ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n" + "(Not Yet Implemented) SHIFT+Click to hide everything but currently queried element." + ) + bl_options = {"REGISTER", "UNDO"} + + unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + unhide_all: bool + + def invoke(self, context, event): + self.unhide_all = event.alt + return self.execute(context) + + def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Project.get_project_props() + + if self.unhide_all: + return self.run_unhide_all() + + obj = props.queried_obj + if not obj: + self.report({"INFO"}, "No object is queried to hide.") + return {"FINISHED"} + guid = props.queried_guid + tool.Project.Link.hide_linked_element(obj, guid) + tool.Project.Link.deselect_queried_linked_element() + + self.report({"INFO"}, "Queried object is now hidden.") + return {"FINISHED"} + + def run_unhide_all(self) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Project.get_project_props() + link = props.active_link + if not link: + self.report({"INFO"}, "No linked model is currently selected.") + return {"FINISHED"} + tool.Project.Link.unhide_all_elements(link) + self.report({"INFO"}, "All linked model geometry is unhidden.") + return {"FINISHED"} + + class AppendInspectedLinkedElement(AppendLibraryElement): bl_idname = "bim.append_inspected_linked_element" bl_label = "Append Inspected Linked Element" diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 57b153428b..5524f7efa7 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -452,6 +452,7 @@ class BIMProjectProperties(PropertyGroup): ) queried_obj: bpy.props.PointerProperty(type=bpy.types.Object) queried_obj_root: bpy.props.PointerProperty(type=bpy.types.Object) + queried_guid: bpy.props.StringProperty() clipping_planes: bpy.props.CollectionProperty(type=ObjProperty) clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5) edited_objs: bpy.props.CollectionProperty(type=EditedObj) @@ -550,6 +551,7 @@ class BIMProjectProperties(PropertyGroup): should_save_metadata_for_this_file: bool queried_obj: Union[bpy.types.Object, None] queried_obj_root: Union[bpy.types.Object, None] + queried_guid: str clipping_planes: bpy.types.bpy_prop_collection_idprop[ObjProperty] clipping_planes_active_index: int edited_objs: bpy.types.bpy_prop_collection_idprop[EditedObj] diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index dca65f72e6..5437d1270b 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -40,9 +40,11 @@ class ExploreTool(bpy.types.WorkSpaceTool): ("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}), ("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}), ("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}), + ("bim.explore_hotkey", {"type": "H", "value": "PRESS"}, {"properties": [("hotkey", "H")]}), + ("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}), ) - def draw_settings(context, layout, ws_tool): + def draw_settings(context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool) -> None: row = layout.row(align=True) row.label(text="Query Object", icon="MOUSE_RMB") row = layout.row(align=True) @@ -61,6 +63,9 @@ class ExploreTool(bpy.types.WorkSpaceTool): row.label(text="", icon="EVENT_ALT") row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C") + row = layout.row(align=True) + row.operator("bim.hide_queried_linked_element", text="Hide Queried Element", icon="EVENT_H") + prop = tool.Project.get_measure_tool_settings() row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -86,6 +91,7 @@ class ExploreHotkey(bpy.types.Operator): bl_idname = "bim.explore_hotkey" bl_label = "" bl_options = {"REGISTER", "UNDO", "INTERNAL"} + hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() @@ -137,3 +143,9 @@ class ExploreHotkey(bpy.types.Operator): return bpy.ops.bim.image_scaling_tool("INVOKE_DEFAULT") + + def hotkey_H(self) -> None: + bpy.ops.bim.hide_queried_linked_element() + + def hotkey_A_H(self) -> None: + bpy.ops.bim.hide_queried_linked_element(unhide_all=True) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 06082ebea2..205e592a35 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -2192,12 +2192,24 @@ class Blender(bonsai.core.tool.Blender): origin: Vector, direction: Vector, ) -> tuple[bool, Vector, Vector, int, bpy.types.Object, Matrix]: + """ + + The returned matrix is just ``obj.matrix_world``. + The returned object is not evaluated by the current depsgraph, + e.g. if object is modified by the depsgraph (e.g. by modifiers) + object has to be evaluated first (`obj.evaluated_get(depsgraph)`). + """ depsgraph = context.evaluated_depsgraph_get() assert context.scene - # `matrix` is just `obj.matrix_world`. result = context.scene.ray_cast( depsgraph, origin, direction, ) return result + + @classmethod + def depsgraph_evaluate(cls, obj: bpy.types.Object) -> bpy.types.Object: + depsgraph = bpy.context.evaluated_depsgraph_get() + evaluated_obj = obj.evaluated_get(depsgraph) + return evaluated_obj diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index b9a7a68a0c..d92bf19cf5 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -33,6 +33,7 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.shape_builder import numpy as np +import numpy.typing as npt from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES from mathutils import Matrix @@ -621,28 +622,103 @@ class Project(bonsai.core.tool.Project): return guid_obj + @classmethod + def get_linked_element_guid_ids(cls, obj: bpy.types.Object, *, skip_hidden: bool) -> npt.NDArray[np.int64]: + obj_guid_ids: npt.NDArray[np.int64] = np.array(obj["guid_ids"]) + + if not skip_hidden: + return obj_guid_ids + + # 'hidden_indices' is needed, because otherwise we can't make sense of 'guid_ids', + # since part of the geometry is hidden. + obj_hidden_indices: list[int] = list(obj.get("hidden_indices") or []) + + if not obj_hidden_indices: + return obj_guid_ids + + # Skip hidden geometry indices. + hidden_indices_mask = np.zeros(len(obj_guid_ids), dtype=bool) + hidden_indices_mask[obj_hidden_indices] = True + deltas = np.diff(obj_guid_ids, prepend=0) + deltas[~hidden_indices_mask] = 0 + obj_guid_ids -= np.cumsum(deltas) + return obj_guid_ids + @classmethod def get_guid_by_face_index(cls, obj: bpy.types.Object, face_index: int) -> str | None: guids: list[str] = obj["guids"] - guid_ids: list[int] = obj["guid_ids"] + guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=True) for guid, guid_end_index in zip(guids, guid_ids): if face_index < guid_end_index: return guid + @classmethod + def get_linked_element_geom_slice(cls, obj: bpy.types.Object, guid: str) -> slice[int, int]: + """ + Get slice for ``obj.data.polygons`` for the provided ``guid``. + """ + obj_guids: list[str] = obj["guids"] + + # Just to be safe. + obj_hidden_indices: list[int] = obj.get("hidden_indices") or [] + index = obj_guids.index(guid) + if index in obj_hidden_indices: + assert False, "Unexpected. Why would you need the geometry for the hidden element?" + obj_guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=False) + guid_end_index = obj_guid_ids[index] + guid_start_index = index and obj_guid_ids[index - 1] + return slice(guid_start_index, guid_end_index) + + @classmethod + def hide_linked_element(cls, obj: bpy.types.Object, guid: str) -> None: + verts = tool.Project.Link.get_linked_element_verts(obj, guid) + + # `MeshPolygon.hide` works only in EDIT mode, + # so we use vertex groups + Mask modifier. + MODIFIER_VG_NAME = "BBIM_HIDE_LINKED_GEOMETRY" + + vertex_groups = obj.vertex_groups + vertex_group = vertex_groups.get(MODIFIER_VG_NAME) + if vertex_group is None: + vertex_group = vertex_groups.new(name=MODIFIER_VG_NAME) + + modifiers = obj.modifiers + modifier = modifiers.get(MODIFIER_VG_NAME) + if modifier is None: + modifier = modifiers.new(MODIFIER_VG_NAME, "MASK") + assert isinstance(modifier, bpy.types.MaskModifier) + modifier.vertex_group = MODIFIER_VG_NAME + modifier.invert_vertex_group = True + + vertex_group.add(verts, 1.0, "REPLACE") + + hidden_indices: list[int] = list(obj.get("hidden_indices") or []) + guid_ids: list[str] = obj["guids"] + index = guid_ids.index(guid) + hidden_indices.append(index) + obj["hidden_indices"] = hidden_indices + + @classmethod + def unhide_all_elements(cls, link: Link) -> None: + obj = tool.Project.get_link_empty_handle(link) + assert obj + col = obj.instance_collection + assert col + + for obj_ in col.objects: + if "hidden_indices" not in obj_: + continue + obj_.vertex_groups.clear() + obj_.modifiers.clear() + del obj_["hidden_indices"] + @classmethod def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None: + slice_ = cls.get_linked_element_geom_slice(obj, guid) + mesh = obj.data assert isinstance(mesh, bpy.types.Mesh) - obj_guids: list[str] = obj["guids"] - obj_guid_ids: list[int] = obj["guid_ids"] - - index = obj_guids.index(guid) - guid_end_index = obj_guid_ids[index] - if index > 0: - guid_start_index = obj_guid_ids[index - 1] - else: - guid_start_index = 0 - guid_polygons = mesh.polygons[guid_start_index:guid_end_index] + guid_polygons = mesh.polygons[slice_] selected_tris: list[tuple[int, ...]] = [] selected_edges: list[tuple[int, ...]] = [] @@ -662,6 +738,19 @@ class Project(bonsai.core.tool.Project): obj["selected_edges"] = selected_edges obj["selected_tris"] = selected_tris + @classmethod + def get_linked_element_verts(cls, obj: bpy.types.Object, guid: str) -> set[int]: + slice_ = cls.get_linked_element_geom_slice(obj, guid) + + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + guid_polygons = mesh.polygons[slice_] + + guid_vertices_set: set[int] = set() + for polygon in guid_polygons: + guid_vertices_set.update(polygon.vertices) + return guid_vertices_set + @classmethod def select_linked_element( cls, @@ -686,10 +775,8 @@ class Project(bonsai.core.tool.Project): if instance_matrix is None: instance_matrix = obj.matrix_world - props = tool.Project.get_project_props() - props.queried_obj = obj - props.queried_obj_root = cls.find_obj_root(obj, instance_matrix) - + cls.deselect_queried_linked_element() + cls.set_queried_linked_element(obj, guid, instance_matrix) cls.select_linked_element_geom(obj, guid) db = sqlite3.connect(obj["db"]) c = db.cursor() @@ -743,6 +830,25 @@ class Project(bonsai.core.tool.Project): ProjectDecorator.install(context) + @classmethod + def set_queried_linked_element(cls, obj: bpy.types.Object, guid: str, instance_matrix: Matrix) -> None: + props = tool.Project.get_project_props() + props.queried_obj = obj + props.queried_obj_root = cls.find_obj_root(obj, instance_matrix) + props.queried_guid = guid + + @classmethod + def deselect_queried_linked_element(cls) -> None: + props = tool.Project.get_project_props() + obj = props.queried_obj + props.property_unset("queried_obj") + props.property_unset("queried_obj_root") + props.property_unset("queried_guid") + + if obj is not None: + for field in cls.SelectedGeometry._fields: + del obj[field] + @classmethod def find_obj_root(cls, obj: bpy.types.Object, matrix: Matrix) -> bpy.types.Object | None: collections = set(obj.users_collection) @@ -755,3 +861,16 @@ class Project(bonsai.core.tool.Project): ): continue return o + + class SelectedGeometry(NamedTuple): + selected_vertices: list[tuple[float, float, float]] + selected_edges: list[tuple[int, int]] + selected_tris: list[tuple[int, int, int]] + + @classmethod + def get_selected_geometry(cls, obj: bpy.types.Object) -> SelectedGeometry: + return cls.SelectedGeometry( + obj["selected_vertices"], + obj["selected_edges"], + obj["selected_tris"], + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 0a5aee0e1c..0f09bd4991 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -40,7 +40,7 @@ APPENDABLE_ASSET = Literal[ "IfcProfileDef", "IfcPresentationStyle", ] -APPENDABLE_ASSET_TYPES = get_args(APPENDABLE_ASSET) +APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET) MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet") From f9486be17226ad2851646294bb68860423b19c56 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 6 Mar 2026 19:04:54 +0500 Subject: [PATCH 130/131] get_linked_element_geom_slice - add tests --- src/bonsai/test/tool/test_project.py | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 28ff090be6..c6d1c410de 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -21,6 +21,7 @@ import json import tempfile from pathlib import Path from tempfile import NamedTemporaryFile +from typing import cast import bpy import ifcopenshell @@ -399,3 +400,50 @@ class TestLoadingIfcSqlite(NewFile): for element_name in elements_without_meshes: assert element_name in bpy.data.objects assert not bpy.data.objects[element_name].data + + +class TestGettingLinkedElementGeomSlice: + def __init__(self): + self.test_get_first_element() + self.test_get_middle_element() + self.test_skip_hidden_first_element() + self.test_skip_hidden_middle_element() + self.test_handle_hidden_non_first_element() + + TEST_OBJ = { + "guids": ["aaa", "bbb", "ccc"], + "guid_ids": [5, 10, 15], + } + + def test_get_first_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa") + assert range(15)[slice_] == range(5) + + def test_get_middle_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb") + assert range(15)[slice_] == range(5, 10) + + def test_skip_hidden_first_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = obj | {"hidden_indices": [0]} + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb") + assert range(15)[slice_] == range(5) + + def test_skip_hidden_middle_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = obj | {"hidden_indices": [1]} + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "ccc") + assert range(15)[slice_] == range(5, 10) + + def test_handle_hidden_non_first_element(self): + obj = TestGettingLinkedElementGeomSlice.TEST_OBJ + obj = obj | {"hidden_indices": [1]} + obj = cast(bpy.types.Object, obj) + slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa") + assert range(15)[slice_] == range(5) From 0469a9528b1466b79dc45bf4dae4dc06d3607c3d Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 6 Mar 2026 16:34:12 +0100 Subject: [PATCH 131/131] use basic casts to prevent needless item upgrades #7738 --- src/ifcgeom/taxonomy.cpp | 4 ++-- src/ifcgeom/taxonomy.h | 20 +++++++------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index 7d509db8cb..2ce115dfd1 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -543,8 +543,8 @@ piecewise_function::const_ptr offset_function::get_offset() const { return offse ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) { auto flat = make(); ifcopenshell::geometry::visit(deep, [&flat](taxonomy::ptr i) { - flat->children.push_back(taxonomy::cast(clone(i))); - }); + flat->children.push_back(std::static_pointer_cast(clone(i))); + }); return flat; } diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index ab8a222aee..5d70d4490c 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -1622,25 +1622,19 @@ typedef item const* ptr; for (auto& i : deep->children) { // @todo Sad... now that we have templated collection members, // we can't generally use collection_base anymore as a cast target. - if (auto s = taxonomy::dcast(i)) { + if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); - } - else if (auto s = taxonomy::dcast(i)) { + } else if (auto s = std::dynamic_pointer_cast(i)) { visit(s, fn); } else {