Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-05-19 07:06:22 +02:00
15 changed files with 425 additions and 105 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
uv tool install ruff uv tool install ruff
uv tool install black uv tool install black
uv tool install poethepoet uv tool install poethepoet
uv tool install ty uv tool install ty==0.0.34
# black doesn't catch all syntax errors, so we check them explicitly. # black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors - name: Check syntax errors
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pyparsing
pip install src/bcf --no-deps pip install src/bcf --no-deps
pip install pytest-xdist==3.8.0 pip install pytest-xdist==3.8.0
+1 -2
View File
@@ -126,9 +126,8 @@ ssl._create_default_https_context = ssl._create_unverified_context
import time import time
from collections.abc import Generator, Sequence from collections.abc import Generator, Sequence
from pathlib import Path from pathlib import Path
from urllib.request import urlretrieve
from typing import Literal, Union from typing import Literal, Union
from urllib.request import urlretrieve
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
@@ -313,7 +313,7 @@ def format_distance(
if not feet and not add_inches: if not feet and not add_inches:
tx_dist += str(feet) + "'" tx_dist += str(feet) + "'"
if not feet and add_inches: if not feet and add_inches and unit_length != "INCHES":
if value < 0: if value < 0:
tx_dist += "-0' - " tx_dist += "-0' - "
else: else:
+5 -1
View File
@@ -468,7 +468,11 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
profiles = extrusion.SweptArea.Profiles if extrusion.SweptArea.is_a("IfcCompositeProfileDef") else [extrusion.SweptArea] profiles = (
extrusion.SweptArea.Profiles
if extrusion.SweptArea.is_a("IfcCompositeProfileDef")
else [extrusion.SweptArea]
)
for profile in profiles: for profile in profiles:
coord_list = builder.get_polyline_coords(profile.OuterCurve) coord_list = builder.get_polyline_coords(profile.OuterCurve)
coord_list = [ coord_list = [
+3 -7
View File
@@ -17,20 +17,20 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import test.bim.bootstrap
import ifcopenshell.api.cost import ifcopenshell.api.cost
import bonsai.core.tool import bonsai.core.tool
import bonsai.tool as tool import bonsai.tool as tool
import test.bim.bootstrap import test.bim.bootstrap
from bonsai.tool.cost import Cost as subject
from test.bim.bootstrap import NewFile from test.bim.bootstrap import NewFile
from bonsai.tool.cost import Cost as subject
class TestImplementsTool(NewFile): class TestImplementsTool(NewFile):
def test_run(self): def test_run(self):
assert isinstance(subject(), bonsai.core.tool.Cost) assert isinstance(subject(), bonsai.core.tool.Cost)
class TestDisableEditingCostItemParent(NewFile): class TestDisableEditingCostItemParent(NewFile):
def test_avoid_recursion_error(newfile, monkeypatch): def test_avoid_recursion_error(newfile, monkeypatch):
class DummyProps: class DummyProps:
@@ -39,11 +39,7 @@ class TestDisableEditingCostItemParent(NewFile):
self.active_cost_item_id = 5 self.active_cost_item_id = 5
props = DummyProps() props = DummyProps()
monkeypatch.setattr( monkeypatch.setattr("bonsai.tool.Cost.get_cost_props", lambda: props)
"bonsai.tool.Cost.get_cost_props",
lambda: props
)
subject.disable_editing_cost_item_parent() subject.disable_editing_cost_item_parent()
assert props.active_cost_item_id == 0 assert props.active_cost_item_id == 0
assert props.change_cost_item_parent is not False assert props.change_cost_item_parent is not False
+2
View File
@@ -562,6 +562,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcMaterial* material) {
} }
// Check if it's failed or just some unsupported case. // Check if it's failed or just some unsupported case.
if (failed_on_purpose_.find(styled_item) == failed_on_purpose_.end()) { if (failed_on_purpose_.find(styled_item) == failed_on_purpose_.end()) {
failed_on_purpose_.insert(material);
return nullptr; return nullptr;
} }
Logger::Warning("Skipping unsupported material style for material: ", material); Logger::Warning("Skipping unsupported material style for material: ", material);
@@ -569,6 +570,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcMaterial* material) {
} }
// When material does not have a representation we don't create a style from it // When material does not have a representation we don't create a style from it
failed_on_purpose_.insert(material);
return nullptr; return nullptr;
/* /*
+4 -1
View File
@@ -538,7 +538,10 @@ def main(
*(tup for i, tup in enumerate(zip(path_objects, section_polies, polies)) if has_relevant_zone(i)) *(tup for i, tup in enumerate(zip(path_objects, section_polies, polies)) if has_relevant_zone(i))
) )
arranged = W.arrange_polygons(*filter(None, (ARRANGE_POLYGON_SETTINGS,)), polies) arranged = W.arrange_polygons(
*filter(None, (ARRANGE_POLYGON_SETTINGS,)),
polies, # ty: ignore[too-many-positional-arguments]
)
svg_data_3 = W.polygons_to_svg(arranged, False) svg_data_3 = W.polygons_to_svg(arranged, False)
dom3 = parseString(svg_data_3) dom3 = parseString(svg_data_3)
svg3 = dom3.childNodes[0] svg3 = dom3.childNodes[0]
@@ -420,7 +420,15 @@ class SchemaClass(codegen.Base):
if isinstance(type, nodes.AggregationType): if isinstance(type, nodes.AggregationType):
aggr_type = type.aggregate_type aggr_type = type.aggregate_type
make_bound = lambda b: -1 if b == "?" else int(b)
def make_bound(b):
# `?` and non-literal bounds (attribute references, arithmetic expressions) collapse to -1.
#
try:
return int(b)
except (TypeError, ValueError):
return -1
bound1, bound2 = map(make_bound, (type.bounds.lower, type.bounds.upper)) bound1, bound2 = map(make_bound, (type.bounds.lower, type.bounds.upper))
decl_type = get_declared_type(type.type, emitted_names) decl_type = get_declared_type(type.type, emitted_names)
return x.aggregation_type(aggr_type, bound1, bound2, decl_type) return x.aggregation_type(aggr_type, bound1, bound2, decl_type)
@@ -547,7 +555,16 @@ class SchemaClass(codegen.Base):
inv_attrs = [] inv_attrs = []
for attr in type.inverse: for attr in type.inverse:
if attr.bounds: if attr.bounds:
make_bound = lambda b: -1 if b == "?" else int(b)
def make_bound(b):
# `?` and non-literal bounds (attribute references, arithmetic
# expressions) collapse to -1 (unbounded) — the C++ runtime has
# no third state for "dynamic cardinality".
try:
return int(b)
except (TypeError, ValueError):
return -1
bound1, bound2 = map(make_bound, (attr.bounds.lower, attr.bounds.upper)) bound1, bound2 = map(make_bound, (attr.bounds.lower, attr.bounds.upper))
else: else:
bound1, bound2 = -1, -1 bound1, bound2 = -1, -1
@@ -1695,7 +1695,7 @@ class type_declaration(declaration):
class uninitialized_tag: ... class uninitialized_tag: ...
def arrange_polygons(polygons): ... def arrange_polygons(settings, polygons): ...
def clear_schemas(): ... def clear_schemas(): ...
def construct_iterator(geometry_library, settings, file, num_threads): ... def construct_iterator(geometry_library, settings, file, num_threads): ...
def construct_iterator_with_include_exclude(geometry_library, settings, file, elems, include, num_threads): ... def construct_iterator_with_include_exclude(geometry_library, settings, file, elems, include, num_threads): ...
+1
View File
@@ -21,6 +21,7 @@ dependencies = [
"isodate", "isodate",
"python-dateutil", "python-dateutil",
"lark", "lark",
"pyparsing",
"typing-extensions", "typing-extensions",
] ]
@@ -0,0 +1,74 @@
import os
import sys
import tempfile
import unittest
import ifcopenshell.express
sys.path.insert(0, os.path.dirname(ifcopenshell.express.__file__))
def _parse(schema_text):
with tempfile.NamedTemporaryFile(mode="w", suffix=".exp", delete=False) as f:
f.write(schema_text)
path = f.name
try:
return ifcopenshell.express.parse(path)
finally:
os.unlink(path)
cache = path + ".cache.dat"
if os.path.exists(cache):
os.unlink(cache)
class TestAggregateBounds(unittest.TestCase):
def test_literal_bounds_preserved(self):
"""After loading [1;3] -> (1, 3)?"""
s = _parse("SCHEMA t; ENTITY E; v : ARRAY [1:3] OF REAL; END_ENTITY; END_SCHEMA;")
agg = (
next(d for d in s.schema.declarations() if d.name() == "E")
.attributes()[0]
.type_of_attribute()
.as_aggregation_type()
)
self.assertEqual((agg.bound1(), agg.bound2()), (1, 3))
s.disown()
def test_unbounded_marker(self):
"""[0:?] -> (0, -1)?"""
s = _parse("SCHEMA t; ENTITY E; v : LIST [0:?] OF REAL; END_ENTITY; END_SCHEMA;")
agg = (
next(d for d in s.schema.declarations() if d.name() == "E")
.attributes()[0]
.type_of_attribute()
.as_aggregation_type()
)
# import pdb; pdb.set_trace()
self.assertEqual((agg.bound1(), agg.bound2()), (0, -1))
s.disown()
def test_voxel_grid_with_dynamic_bound_loads(self):
"""
Array that is an expression : [1:dim_x*dim_y*dim_z]
Parsing must not crash, Bbund must be (1, -1)
"""
s = _parse("""
SCHEMA t;
TYPE IfcBoolean = BOOLEAN; END_TYPE;
ENTITY IfcVoxelHolder;
NumberOfVoxelsX : INTEGER;
NumberOfVoxelsY : INTEGER;
NumberOfVoxelsZ : INTEGER;
Voxels : ARRAY [1:NumberOfVoxelsX*NumberOfVoxelsY*NumberOfVoxelsZ] OF IfcBoolean;
END_ENTITY;
END_SCHEMA;
""")
holder = next(d for d in s.schema.declarations() if d.name() == "IfcVoxelHolder")
voxels = holder.attributes()[-1].type_of_attribute().as_aggregation_type()
self.assertEqual((voxels.bound1(), voxels.bound2()), (1, -1))
s.disown()
if __name__ == "__main__":
unittest.main()
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import ifcopenshell.api.control import ifcopenshell.api.control
import ifcopenshell.api.cost import ifcopenshell.api.cost
@@ -25,6 +24,7 @@ import ifcopenshell.api.root
import ifcopenshell.util.cost as subject import ifcopenshell.util.cost as subject
class TestGetCostItemForProduct(test.bootstrap.IFC4): class TestGetCostItemForProduct(test.bootstrap.IFC4):
def test_run(self): def test_run(self):
model = self.file model = self.file
@@ -40,7 +40,7 @@ class TestGetCostItemForProduct(test.bootstrap.IFC4):
cost_schedule = ifcopenshell.api.cost.add_cost_schedule(model) cost_schedule = ifcopenshell.api.cost.add_cost_schedule(model)
item1 = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=cost_schedule) item1 = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=cost_schedule)
ifcopenshell.api.control.assign_control(model, related_objects=[element], relating_control=item1) ifcopenshell.api.control.assign_control(model, related_objects=[element], relating_control=item1)
ifcopenshell.api.cost.remove_cost_item(model, cost_item = item1) ifcopenshell.api.cost.remove_cost_item(model, cost_item=item1)
assert list(subject.get_cost_items_for_product(element)) == [] assert list(subject.get_cost_items_for_product(element)) == []
def test_no_assigned_cost_items(self): def test_no_assigned_cost_items(self):
@@ -49,4 +49,3 @@ class TestGetCostItemForProduct(test.bootstrap.IFC4):
cost_schedule = ifcopenshell.api.cost.add_cost_schedule(model) cost_schedule = ifcopenshell.api.cost.add_cost_schedule(model)
item1 = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=cost_schedule) item1 = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=cost_schedule)
assert list(subject.get_cost_items_for_product(element)) == [] assert list(subject.get_cost_items_for_product(element)) == []
@@ -33,7 +33,7 @@ class Patcher(ifcpatch.BasePatcher):
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Union[Logger, None] = None, logger: Union[Logger, None] = None,
query: str = "IfcWall", query: str = "IfcWall",
assume_asset_uniqueness_by_name: bool = True, assume_asset_uniqueness_by_name: bool = False,
): ):
"""Extract certain elements into a new model """Extract certain elements into a new model
+309 -84
View File
@@ -267,11 +267,12 @@ void clean_polygon(Polygon_2& poly) {
void smooth_polygon(double factor, Polygon_2& poly) { void smooth_polygon(double factor, Polygon_2& poly) {
auto ps = create_and_convert_offset_polygon(-factor, poly); auto ps = create_and_convert_offset_polygon(-factor, poly);
if (ps.size() == 1) { auto it = std::max_element(ps.begin(), ps.end(), [&](const auto& p, const auto& q) { return p.area() < q.area(); });
auto r2 = ps.front(); if (it != ps.end()) {
ps = create_and_convert_offset_polygon(+factor, r2); auto qs = create_and_convert_offset_polygon(+factor, *it);
if (ps.size() == 1) { auto jt = std::max_element(qs.begin(), qs.end(), [&](const auto& p, const auto& q) { return p.area() < q.area(); });
poly = ps.front(); if (jt != qs.end()) {
poly = *jt;
} }
} }
} }
@@ -884,8 +885,7 @@ Polygon_with_holes_2 subdivide_polygon_on_same_input(SegmentLookup& segment_look
std::tuple< std::tuple<
std::map<Point_2, std::vector<Point_2>>, std::map<Point_2, std::vector<Point_2>>,
std::map<Point_2, std::pair<Point_2, Point_2>>, std::map<Point_2, std::pair<Point_2, Point_2>>,
std::map<std::pair<Point_2, Point_2>, std::vector<const CGAL::Polygon_2<K>*>>, std::map<std::pair<Point_2, Point_2>, std::vector<const CGAL::Polygon_2<K>*>>
std::map<Point_2, double>
> >
build_line_graph(const std::vector<Polygon_2>& input_polygons, const std::map<Point_2, SegmentLookup::PolygonIt>& point_lookup, const std::vector<Polygon_2>& triangular_polygons) build_line_graph(const std::vector<Polygon_2>& input_polygons, const std::map<Point_2, SegmentLookup::PolygonIt>& point_lookup, const std::vector<Polygon_2>& triangular_polygons)
{ {
@@ -896,7 +896,9 @@ build_line_graph(const std::vector<Polygon_2>& input_polygons, const std::map<Po
std::map<std::pair<Point_2, Point_2>, Point_2> segment_to_midpoint; std::map<std::pair<Point_2, Point_2>, Point_2> segment_to_midpoint;
std::map<Point_2, std::pair<Point_2, Point_2>> midpoint_to_segment; std::map<Point_2, std::pair<Point_2, Point_2>> midpoint_to_segment;
std::map<const CGAL::Polygon_2<K>*, std::vector<std::pair<Point_2, Point_2>>> facet_to_segment; std::map<const CGAL::Polygon_2<K>*, std::vector<std::pair<Point_2, Point_2>>> facet_to_segment;
std::map<Point_2, double> midpoint_to_edge_length;
// std::map<Point_2, double> midpoint_to_edge_length;
for (auto& tri : triangular_polygons) { for (auto& tri : triangular_polygons) {
for (size_t i = 0; i < 3; ++i) { for (size_t i = 0; i < 3; ++i) {
@@ -929,7 +931,7 @@ build_line_graph(const std::vector<Polygon_2>& input_polygons, const std::map<Po
if (p1index->second != input_polygons.end() && p2index->second != input_polygons.end() && p1index->second != p2index->second) { if (p1index->second != input_polygons.end() && p2index->second != input_polygons.end() && p1index->second != p2index->second) {
segment_to_midpoint[p.first] = center; segment_to_midpoint[p.first] = center;
midpoint_to_segment[center] = p.first; midpoint_to_segment[center] = p.first;
midpoint_to_edge_length[center] = std::sqrt(CGAL::to_double(CGAL::squared_distance(p.first.first, p.first.second))); // midpoint_to_edge_length[center] = std::sqrt(CGAL::to_double(CGAL::squared_distance(p.first.first, p.first.second)));
} }
} }
@@ -949,7 +951,7 @@ build_line_graph(const std::vector<Polygon_2>& input_polygons, const std::map<Po
} }
} }
return {line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length}; return {line_graph, midpoint_to_segment, segment_to_input_facet}; // } , midpoint_to_edge_length};
} }
using DPoint = CGAL::Simple_cartesian<double>::Point_2; using DPoint = CGAL::Simple_cartesian<double>::Point_2;
@@ -958,8 +960,8 @@ using DBox = std::array<DPoint, 2>;
struct CenterLineGraphData { struct CenterLineGraphData {
std::vector<Point_2> points; std::vector<Point_2> points;
std::vector<std::optional<std::pair<Point_2, Point_2>>> orig_segments;
std::vector<DPoint> points_double; std::vector<DPoint> points_double;
std::vector<double> widths;
std::vector<std::pair<size_t, size_t>> edges; std::vector<std::pair<size_t, size_t>> edges;
std::vector<std::vector<size_t>> incident_edges; std::vector<std::vector<size_t>> incident_edges;
}; };
@@ -1085,9 +1087,48 @@ bool aabb_overlap(const DBox& a, const DBox& b, double eps = 1.e-9) {
a[1].y() + eps >= b[0].y(); a[1].y() + eps >= b[0].y();
} }
std::pair<double, double> projected_interval_on_axis(const std::array<DPoint, 4>& points, const DDir& axis_u) {
auto u = unit(axis_u);
auto t0 = (points.front() - CGAL::ORIGIN) * u;
auto interval = std::make_pair(t0, t0);
for (auto& p : points) {
auto t = (p - CGAL::ORIGIN) * u;
interval.first = std::min(interval.first, t);
interval.second = std::max(interval.second, t);
}
return interval;
}
bool intervals_overlap(const std::pair<double, double>& a, const std::pair<double, double>& b, double eps = 1.e-9) {
return a.first <= b.second + eps && b.first <= a.second + eps;
}
bool obb_overlap(const std::array<DPoint, 4>& a, const std::array<DPoint, 4>& b, double eps = 1.e-9) {
auto has_separating_axis = [&](const std::array<DPoint, 4>& points) {
for (size_t i = 0; i < points.size(); ++i) {
auto edge = points[(i + 1) % points.size()] - points[i];
auto axis = unit(perpendicular(edge));
if (axis.squared_length() < 1.e-18) {
continue;
}
if (!intervals_overlap(projected_interval_on_axis(a, axis), projected_interval_on_axis(b, axis), eps)) {
return true;
}
}
return false;
};
return !has_separating_axis(a) && !has_separating_axis(b);
}
template <typename T, typename U>
bool obb_overlap(const T& a, const U& b, double eps = 1.e-9) {
return obb_overlap(a.corners, b.corners, eps);
}
CenterLineGraphData make_center_line_graph_data( CenterLineGraphData make_center_line_graph_data(
const std::map<Point_2, std::vector<Point_2>>& line_graph, const std::map<Point_2, std::vector<Point_2>>& line_graph,
const std::map<Point_2, double>& midpoint_to_edge_length) const std::map<Point_2, std::pair<Point_2, Point_2>>& midpoint_to_segment)
{ {
CenterLineGraphData graph; CenterLineGraphData graph;
std::map<Point_2, size_t> point_to_index; std::map<Point_2, size_t> point_to_index;
@@ -1100,9 +1141,13 @@ CenterLineGraphData make_center_line_graph_data(
auto i = graph.points.size(); auto i = graph.points.size();
point_to_index[p] = i; point_to_index[p] = i;
graph.points.push_back(p); graph.points.push_back(p);
auto mit = midpoint_to_segment.find(p);
if (mit == midpoint_to_segment.end()) {
graph.orig_segments.emplace_back();
} else {
graph.orig_segments.emplace_back(mit->second);
}
graph.points_double.push_back(to_double_point(p)); graph.points_double.push_back(to_double_point(p));
auto wt = midpoint_to_edge_length.find(p);
graph.widths.push_back(wt == midpoint_to_edge_length.end() ? 0. : wt->second);
graph.incident_edges.emplace_back(); graph.incident_edges.emplace_back();
return i; return i;
}; };
@@ -1136,7 +1181,41 @@ CenterLineGraphData make_center_line_graph_data(
} }
double segment_width(const CenterLineGraphData& graph, const std::pair<size_t, size_t>& edge) { double segment_width(const CenterLineGraphData& graph, const std::pair<size_t, size_t>& edge) {
return 0.5 * (graph.widths[edge.first] + graph.widths[edge.second]); auto s1 = graph.orig_segments[edge.first];
auto s2 = graph.orig_segments[edge.second];
if (!s1 || !s2) {
throw std::runtime_error("!!!");
}
// A line segment between two points is expected to span a triangle, which means that one of the
// segment points ought to be shared.
Point_2 refpoint;
if (s1->first == s2->first) {
refpoint = s1->first;
} else if (s1->second == s2->first) {
refpoint = s1->second;
} else if (s1->first == s2->second) {
refpoint = s1->first;
} else if (s1->second == s2->second) {
refpoint = s1->second;
} else {
throw std::runtime_error("!!!!!");
}
auto p1 = graph.points_double[edge.first];
auto p2 = graph.points_double[edge.second];
auto v = p2 - p1;
if (v.squared_length() < 1.e-9) {
throw std::runtime_error("!!!!!!!");
}
v /= std::sqrt(v.squared_length());
auto n = perpendicular(v);
auto P = to_double_point(refpoint);
auto l = CGAL::abs((P - p1) * n);
return 2 * l;
} }
bool edge_supports_same_line( bool edge_supports_same_line(
@@ -1227,6 +1306,7 @@ std::vector<LineRun> runs_from_graph(const CenterLineGraphData& graph, double an
auto len = std::sqrt(d.squared_length()); auto len = std::sqrt(d.squared_length());
total_length += len; total_length += len;
weighted_width_sum += len * segment_width(graph, edge); weighted_width_sum += len * segment_width(graph, edge);
// std::cout << " l: " << len << " w: " << segment_width(graph, edge) << " p1: " << graph.points_double[edge.first] << " p2: " << graph.points_double[edge.second] << std::endl;
} }
auto run_direction = direction_sum.squared_length() < 1.e-18 ? ref : unit(direction_sum); auto run_direction = direction_sum.squared_length() < 1.e-18 ? ref : unit(direction_sum);
@@ -1249,6 +1329,8 @@ std::vector<LineRun> runs_from_graph(const CenterLineGraphData& graph, double an
auto avg_width = total_length < 1.e-9 ? segment_width(graph, seed_edge) : weighted_width_sum / total_length; auto avg_width = total_length < 1.e-9 ? segment_width(graph, seed_edge) : weighted_width_sum / total_length;
// std::cout << "avg_width: " << avg_width << std::endl;
runs.push_back({ runs.push_back({
graph.points[start_index], graph.points[start_index],
graph.points[end_index], graph.points[end_index],
@@ -1375,9 +1457,19 @@ std::pair<double, double> merge_score(const MergedBoxRecord& a, const MergedBoxR
} }
bool clusters_can_merge(const BoxCluster& a, const BoxCluster& b, double angle_tol_deg = 5., double axis_overlap_ratio_limit = 0.5) { bool clusters_can_merge(const BoxCluster& a, const BoxCluster& b, double angle_tol_deg = 5., double axis_overlap_ratio_limit = 0.5) {
auto min_width = a.box.avg_width < b.box.avg_width ? a.box.avg_width : b.box.avg_width;
auto max_width = a.box.avg_width > b.box.avg_width ? a.box.avg_width : b.box.avg_width;
if (min_width > 1.e-9) {
if (max_width / min_width > 5) {
return false;
}
}
if (!aabb_overlap(a.box.bbox, b.box.bbox)) { if (!aabb_overlap(a.box.bbox, b.box.bbox)) {
return false; return false;
} }
if (!obb_overlap(a.box, b.box)) {
return false;
}
if (angle_between_dirs_deg(a.box.direction, b.box.direction) > angle_tol_deg) { if (angle_between_dirs_deg(a.box.direction, b.box.direction) > angle_tol_deg) {
return false; return false;
} }
@@ -1427,6 +1519,7 @@ std::vector<MergedBoxRecord> merge_intersecting_parallel_boxes_iterative(const s
std::vector<size_t> members = clusters[i].members; std::vector<size_t> members = clusters[i].members;
members.insert(members.end(), clusters[j].members.begin(), clusters[j].members.end()); members.insert(members.end(), clusters[j].members.begin(), clusters[j].members.end());
auto merged = BoxCluster{members, merge_cluster_to_box(members, records)}; auto merged = BoxCluster{members, merge_cluster_to_box(members, records)};
// std::cout << "Result width: " << merged.box.avg_width << "; from " << clusters[i].box.avg_width << " & " << clusters[j].box.avg_width << std::endl;
std::vector<BoxCluster> next_clusters; std::vector<BoxCluster> next_clusters;
next_clusters.reserve(clusters.size() - 1); next_clusters.reserve(clusters.size() - 1);
@@ -1506,6 +1599,7 @@ double point_to_oriented_box_distance(const DPoint& p, const MergedBoxRecord& bo
} }
std::map<Point_2, std::vector<Point_2>> snap_points_to_box_axes( std::map<Point_2, std::vector<Point_2>> snap_points_to_box_axes(
DebugWriter& debug,
const CenterLineGraphData& graph, const CenterLineGraphData& graph,
const std::vector<MergedBoxRecord>& boxes, const std::vector<MergedBoxRecord>& boxes,
const K::FT& max_projection_distance) { const K::FT& max_projection_distance) {
@@ -1548,16 +1642,34 @@ std::map<Point_2, std::vector<Point_2>> snap_points_to_box_axes(
auto& c2 = containing[1]; auto& c2 = containing[1];
if (angle_between_dirs_deg(boxes[c1.box_index].direction, boxes[c2.box_index].direction) > 8.) { if (angle_between_dirs_deg(boxes[c1.box_index].direction, boxes[c2.box_index].direction) > 8.) {
if (auto x = intersect_infinite_lines_exact(boxes[c1.box_index], boxes[c2.box_index])) { if (auto x = intersect_infinite_lines_exact(boxes[c1.box_index], boxes[c2.box_index])) {
snapped_points[i] = *x; auto seg = CGAL::Segment_2<K>(graph.points[i], *x);
continue; bool intersects_with_other_box_axis = false;
for (size_t j = 0; j < boxes.size(); ++j) {
if (j == c1.box_index || j == c2.box_index) {
continue;
}
auto& box = boxes[j];
auto box_seg = CGAL::Segment_2<K>(box.exact_start, box.exact_end);
if (CGAL::do_intersect(seg, box_seg)) {
intersects_with_other_box_axis = true;
break;
}
}
if (!intersects_with_other_box_axis) {
snapped_points[i] = *x;
debug.write_segment(graph.points[i], *x, "snap_candidate_1");
continue;
}
} }
} }
snapped_points[i] = c1.projection; snapped_points[i] = (c1.projection - graph.points[i]).squared_length() < (c2.projection - graph.points[i]).squared_length() ? c1.projection : c2.projection;
debug.write_segment(graph.points[i], snapped_points[i], "snap_candidate_2");
continue; continue;
} }
if (containing.size() == 1) { if (containing.size() == 1) {
snapped_points[i] = containing[0].projection; snapped_points[i] = containing[0].projection;
debug.write_segment(graph.points[i], containing[0].projection, "snap_candidate_3");
continue; continue;
} }
@@ -1570,6 +1682,7 @@ std::map<Point_2, std::vector<Point_2>> snap_points_to_box_axes(
if ((graph.points[i] - best.projection).squared_length() < (max_projection_distance * max_projection_distance)) { if ((graph.points[i] - best.projection).squared_length() < (max_projection_distance * max_projection_distance)) {
snapped_points[i] = best.projection; snapped_points[i] = best.projection;
debug.write_segment(graph.points[i], best.projection, "snap_candidate_4");
} else { } else {
snapped_points[i] = graph.points[i]; snapped_points[i] = graph.points[i];
std::cout << "Warning: snapping distance exceeding distance: " << std::sqrt(CGAL::to_double((snapped_points[i] - best.projection).squared_length())) << " > " << max_projection_distance << std::endl; std::cout << "Warning: snapping distance exceeding distance: " << std::sqrt(CGAL::to_double((snapped_points[i] - best.projection).squared_length())) << " > " << max_projection_distance << std::endl;
@@ -1597,9 +1710,9 @@ std::map<Point_2, std::vector<Point_2>> snap_points_to_box_axes(
Graph2D<K> join_segment_runs( Graph2D<K> join_segment_runs(
DebugWriter& debug, DebugWriter& debug,
const std::map<Point_2, std::vector<Point_2>>& line_graph, const std::map<Point_2, std::vector<Point_2>>& line_graph,
const std::map<Point_2, double>& midpoint_to_edge_length, const std::map<Point_2, std::pair<Point_2, Point_2>>& midpoint_to_segment,
const K::FT& max_projection_distance) { const K::FT& max_projection_distance) {
auto graph = make_center_line_graph_data(line_graph, midpoint_to_edge_length); auto graph = make_center_line_graph_data(line_graph, midpoint_to_segment);
auto runs = runs_from_graph(graph); auto runs = runs_from_graph(graph);
runs.erase(std::remove_if(runs.begin(), runs.end(), [](const LineRun& run) { runs.erase(std::remove_if(runs.begin(), runs.end(), [](const LineRun& run) {
return run.vertex_count <= 5; return run.vertex_count <= 5;
@@ -1629,7 +1742,7 @@ Graph2D<K> join_segment_runs(
} }
debug.write_polygons(run_polygons, "merged_boxes"); debug.write_polygons(run_polygons, "merged_boxes");
auto snapped_graph = snap_points_to_box_axes(graph, boxes, max_projection_distance); auto snapped_graph = snap_points_to_box_axes(debug, graph, boxes, max_projection_distance);
return Graph2D<K>(snapped_graph); return Graph2D<K>(snapped_graph);
} }
@@ -2123,61 +2236,139 @@ std::list<std::pair<Point_2, Point_2>> extend_end_vertices_based_on_input(
std::list<std::pair<Point_2, Point_2>> std::list<std::pair<Point_2, Point_2>>
extend_end_vertices_based_on_input_simple( extend_end_vertices_based_on_input_simple(
DebugWriter& debug_output,
const Graph2D<K>& G, const Graph2D<K>& G,
const Polygon_list& outer_perimiter, const Polygon_list& outer_perimiter,
const K::FT& max_projection_distance) const K::FT& max_projection_distance, int pass)
{ {
auto max_intersection_distance = max_projection_distance / 4; auto max_intersection_distance = max_projection_distance / 4;
std::list<std::pair<Point_2, Point_2>> constructed_segments;
for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) { using ValidationSegmentList = std::list<CGAL::Segment_3<K>>;
if (it->second.size() == 1) { using ValidationSegmentIt = ValidationSegmentList::iterator;
auto& M = it->first; using ValidationTreeTraits = CGAL::AABB_traits<K, CGAL::AABB_segment_primitive<K, ValidationSegmentIt>>;
using ValidationTree = CGAL::AABB_tree<ValidationTreeTraits>;
for (auto& bnd : outer_perimiter) { const auto& to_3d = [](const Point_2& p) {
// if point M is contained in bnd interior: return CGAL::Point_3<K>(p.x(), p.y(), 0);
// if (!bnd.has_on_unbounded_side(M)) { };
if (bnd.has_on_bounded_side(M)) {
auto& incoming = *it->second.begin();
// create ray incoming -> M
CGAL::Ray_2<K> ray(incoming, M - incoming);
// intersect ray with boundary const auto& to_2d = [](const CGAL::Point_3<K>& p) {
boost::optional<CGAL::Segment_2<K>> closest_segment; return CGAL::Point_2<K>(p.x(), p.y());
boost::optional<CGAL::Point_2<K>> closest_intersection_point; };
K::FT sq_distance_along_ray = std::numeric_limits<double>::infinity();
for (auto jt = bnd.edges_begin(); jt != bnd.edges_end(); ++jt) { ValidationSegmentList validation_segments;
const auto& seg = *jt; for (auto it = G.edges_begin(); it != G.edges_end(); ++it) {
auto x = CGAL::intersection(ray, seg); if (it->first != it->second) {
if (x) { validation_segments.emplace_back(to_3d(it->first), to_3d(it->second));
if (auto* xp = variant_get<CGAL::Point_2<K>>(&*x)) { }
auto dist = ((*xp) - M).squared_length(); }
if (dist < sq_distance_along_ray) {
if (dist < (max_intersection_distance * max_intersection_distance)) { ValidationTree validation_tree(validation_segments.begin(), validation_segments.end());
const auto has_intersection = [&](const Segment_2& candidate) {
// @nb still disabled.
return false;
std::vector<ValidationSegmentIt> intersected_segments;
validation_tree.all_intersected_primitives(CGAL::Segment_3<K>(to_3d(candidate.source()), to_3d(candidate.target())), std::back_inserter(intersected_segments));
for (auto it : intersected_segments) {
auto existing = CGAL::Segment_2<K>(to_2d(it->source()), to_2d(it->target()));
auto intersection = CGAL::intersection(candidate, existing);
if (!intersection) {
continue;
}
if (auto* point = variant_get<Point_2>(&*intersection)) {
const bool candidate_endpoint = *point == candidate.source() || *point == candidate.target();
const bool existing_endpoint = *point == existing.source() || *point == existing.target();
if (candidate_endpoint && existing_endpoint) {
continue;
}
}
return true;
}
return false;
};
const auto& process_point = [&](const Point_2& M, const Point_2& incoming) {
bool within_any_perimeter = false;
for (auto& bnd : outer_perimiter) {
// if point M is contained in bnd interior:
// if (!bnd.has_on_unbounded_side(M)) {
if (bnd.has_on_bounded_side(M)) {
within_any_perimeter = true;
// create ray incoming -> M
CGAL::Ray_2<K> ray(incoming, M - incoming);
// intersect ray with boundary
boost::optional<CGAL::Segment_2<K>> closest_segment;
boost::optional<CGAL::Point_2<K>> closest_intersection_point;
K::FT sq_distance_along_ray = std::numeric_limits<double>::infinity();
for (auto jt = bnd.edges_begin(); jt != bnd.edges_end(); ++jt) {
const auto& seg = *jt;
auto x = CGAL::intersection(ray, seg);
if (x) {
if (auto* xp = variant_get<CGAL::Point_2<K>>(&*x)) {
auto dist = ((*xp) - M).squared_length();
if (dist < sq_distance_along_ray) {
if (dist < (max_intersection_distance * max_intersection_distance)) {
if (has_intersection(CGAL::Segment_2<K>(M, *xp))) {
debug_output.write_segment(M, *xp, "exterior_extension_intersection");
} else {
closest_segment = seg; closest_segment = seg;
closest_intersection_point = *xp; closest_intersection_point = *xp;
sq_distance_along_ray = dist; sq_distance_along_ray = dist;
} else { }
} else {
}
}
}
}
}
if (closest_intersection_point) {
return closest_intersection_point;
// constructed_segments.push_front({M, *closest_intersection_point});
} else {
// Loop over boundary segments, and project point onto it, take the closest
K::FT closest_distance = std::numeric_limits<double>::infinity();
boost::optional<CGAL::Point_2<K>> closest_point;
for (auto& poly : outer_perimiter) {
for (auto jt = poly.edges_begin(); jt != poly.edges_end(); ++jt) {
auto seg = *jt;
auto Pp = seg.supporting_line().projection(M);
if (seg.has_on(Pp)) {
auto d = CGAL::squared_distance(Pp, M);
if (d < (max_projection_distance * max_projection_distance)) {
if (d < closest_distance) {
if (has_intersection(CGAL::Segment_2<K>(M, Pp))) {
debug_output.write_segment(M, Pp, "exterior_projection_intersection");
} else {
closest_distance = d;
closest_point = Pp;
}
} }
} }
} }
} }
} }
if (closest_intersection_point) { if (closest_point) {
constructed_segments.push_front({M, *closest_intersection_point}); return closest_point;
// constructed_segments.push_front({M, *closest_point});
} else { } else {
// Loop over boundary segments, and project point onto it, take the closest
K::FT closest_distance = std::numeric_limits<double>::infinity();
boost::optional<CGAL::Point_2<K>> closest_point;
for (auto& poly : outer_perimiter) { for (auto& poly : outer_perimiter) {
for (auto jt = poly.edges_begin(); jt != poly.edges_end(); ++jt) { for (auto it = poly.begin(); it != poly.end(); ++it) {
auto seg = *jt; auto Pp = *it;
auto Pp = seg.supporting_line().projection(M); auto d = CGAL::squared_distance(Pp, M);
if (seg.has_on(Pp)) { if (d < (max_projection_distance * max_projection_distance)) {
auto d = CGAL::squared_distance(Pp, M); if (has_intersection(CGAL::Segment_2<K>(M, Pp))) {
if (d < (max_projection_distance * max_projection_distance)) { debug_output.write_segment(M, Pp, "exterior_nearby_intersection");
} else {
if (d < closest_distance) { if (d < closest_distance) {
closest_distance = d; closest_distance = d;
closest_point = Pp; closest_point = Pp;
@@ -2188,32 +2379,58 @@ extend_end_vertices_based_on_input_simple(
} }
if (closest_point) { if (closest_point) {
constructed_segments.push_front({M, *closest_point}); return closest_point;
} else { } else {
for (auto& poly : outer_perimiter) {
for (auto it = poly.begin(); it != poly.end(); ++it) {
auto Pp = *it;
auto d = CGAL::squared_distance(Pp, M);
if (d < (max_projection_distance * max_projection_distance)) {
if (d < closest_distance) {
closest_distance = d;
closest_point = Pp;
}
}
}
}
if (closest_point) {
constructed_segments.push_front({M, *closest_point});
} else {
std::cout << "Unable to find projection or intersection point for interior boundary (" << M.x() << " " << M.y() << ")" << std::endl;
}
} }
} }
} }
} else if (bnd.has_on_boundary(M)) {
return boost::optional<Point_2>{M};
} }
} }
if (within_any_perimeter) {
std::cout << "Within boundary but still no solution given" << std::endl;
} else {
std::cout << "Outside of all boundaries" << std::endl;
}
return boost::optional<Point_2>{};
};
using solution_length_point_incoming = std::tuple<K::FT, Point_2, Point_2>;
std::vector<solution_length_point_incoming> solutions;
for (auto it = G.vertices_begin(); it != G.vertices_end(); ++it) {
if (it->second.size() == 1) {
auto& M = it->first;
if (auto result = process_point(M, *it->second.begin())) {
if (*result == M) {
std::cout << "Point already on perimeter (" << M.x() << " " << M.y() << ")" << std::endl;
continue;
}
auto d = (M - *result).squared_length();
solutions.emplace_back(d, M, *it->second.begin());
} else {
std::cout << "Unable to find projection or intersection point for interior boundary pass " << pass << " [round 1] (" << M.x() << " " << M.y() << ")" << std::endl;
}
}
}
std::sort(solutions.begin(), solutions.end());
std::list<std::pair<Point_2, Point_2>> constructed_segments;
for (auto& [d, point, incoming] : solutions) {
if (auto result = process_point(point, incoming)) {
constructed_segments.push_front({point, *result});
debug_output.write_segment(point, *result, "exterior_constructed_segment");
auto d = CGAL::squared_distance(point, *result);
std::cout << "Distance: " << std::sqrt(CGAL::to_double(d)) << std::endl;
validation_segments.emplace_back(to_3d(point), to_3d(*result));
auto inserted_it = std::prev(validation_segments.end());
validation_tree.insert(inserted_it, validation_segments.end());
} else {
std::cout << "Unable to find projection or intersection point for interior boundary pass " << pass << " [round 2] (" << point.x() << " " << point.y() << ")" << std::endl;
}
} }
return constructed_segments; return constructed_segments;
@@ -3179,6 +3396,14 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std
std::swap(input_polygons, split_polygons); std::swap(input_polygons, split_polygons);
} }
// before overlap elimition we can (and should) still smooth
/*
* @todo
for (auto& r : input_polygons) {
smooth_polygon(polygon_offset_distance / 100., r);
}
*/
t0.stop(); t0.stop();
t0 = timer.start("overlap elimination"); t0 = timer.start("overlap elimination");
@@ -3332,7 +3557,7 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std
debug_output.write_polygons(triangular_polygons, "triangulated_corridor"); debug_output.write_polygons(triangular_polygons, "triangulated_corridor");
auto [line_graph, midpoint_to_segment, segment_to_input_facet, midpoint_to_edge_length] = build_line_graph(input_polygons, point_lookup, triangular_polygons); auto [line_graph, midpoint_to_segment, segment_to_input_facet] = build_line_graph(input_polygons, point_lookup, triangular_polygons);
for (auto& p : line_graph) { for (auto& p : line_graph) {
for (auto& q : p.second) { for (auto& q : p.second) {
debug_output.write_segment(p.first, q, "network_1"); debug_output.write_segment(p.first, q, "network_1");
@@ -3372,17 +3597,17 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std
Graph2D<K> G2(line_graph); Graph2D<K> G2(line_graph);
G = G2.weld_vertices(); G = G2.weld_vertices();
for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { for (auto it = G.edges_begin(); it != G.edges_end(); ++it) {
debug_output.write_segment(it->first, it->second, "network_2"); debug_output.write_segment(it->first, it->second, "network_b_2");
} }
eliminate_colinear_vertices(G); eliminate_colinear_vertices(G);
edge_slide(G); edge_slide(G);
for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { for (auto it = G.edges_begin(); it != G.edges_end(); ++it) {
debug_output.write_segment(it->first, it->second, "network_3"); debug_output.write_segment(it->first, it->second, "network_b_3");
} }
}; };
if (settings.line_cleaning_algo == 0) { if (settings.line_cleaning_algo == 0) {
G = join_segment_runs(debug_output, line_graph, midpoint_to_edge_length, subdivision_length * 4); G = join_segment_runs(debug_output, line_graph, midpoint_to_segment, subdivision_length * 4);
Arrangement_2 arr; Arrangement_2 arr;
G.to_arrangement(arr); G.to_arrangement(arr);
Graph2D<K> G2; Graph2D<K> G2;
@@ -3390,7 +3615,7 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std
eliminate_colinear_vertices(G2); eliminate_colinear_vertices(G2);
G = G2; G = G2;
for (auto it = G.edges_begin(); it != G.edges_end(); ++it) { for (auto it = G.edges_begin(); it != G.edges_end(); ++it) {
debug_output.write_segment(it->first, it->second, "network_2"); debug_output.write_segment(it->first, it->second, "network_a_2");
} }
} else { } else {
apply_line_cleaning_algo_1(); apply_line_cleaning_algo_1();
@@ -3404,8 +3629,8 @@ void arrange_cgal_polygons(svgfill::arrange_polygon_settings settings, const std
bool fallback_to_line_cleaning_algo_1 = false; bool fallback_to_line_cleaning_algo_1 = false;
if (settings.line_cleaning_algo == 0) { if (settings.line_cleaning_algo == 0) {
segments1 = extend_end_vertices_based_on_input_simple(G, outer_perimiter, subdivision_length * 16); segments1 = extend_end_vertices_based_on_input_simple(debug_output, G, outer_perimiter, subdivision_length * 16, 0);
segments2 = extend_end_vertices_based_on_input_simple(G_orig, outer_perimiter, subdivision_length * 16); segments2 = extend_end_vertices_based_on_input_simple(debug_output, G_orig, outer_perimiter, subdivision_length * 16, 1);
Arrangement_2 arr_clean; Arrangement_2 arr_clean;
G.to_arrangement(arr_clean); G.to_arrangement(arr_clean);