1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Stepped union of two coaxial prisms, as constant-section bands.
//!
//! [`boolean_prisms_exact`](crate::boolean_exact::boolean_prisms_exact)
//! builds a stepped union as ONE exact solid, ledges included (#120, ADR
//! 0072). This module is the older, lighter answer to the same question:
//! cut the union at every height where an operand starts or stops, and
//! return the bands. Within a band the active operand set is constant, so
//! each band is a genuine prism whose section is the planar union of
//! whatever is active there.
//!
//! Use it when bands are what you want (per-storey quantities, say); use
//! the boolean when you want the solid. The two agree on volume, which the
//! tests check.
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Frame2, Scalar, Tolerance, Vec2};
use axiolid_overlay::{overlay, FillRule, OverlayInput, OverlayOperation, Polygon, Ring};
use crate::boolean_exact::{unsupported, Prism};
use crate::BACKEND_ID;
/// One constant-section slab of a stepped result.
#[derive(Debug, Clone, PartialEq)]
pub struct Band {
/// Cross-section rings: outer first, then holes.
pub rings: Vec<Vec<axiolid_core::Point2>>,
/// Base height of this slab.
pub bottom: Scalar,
/// Top height of this slab.
pub top: Scalar,
}
/// Decompose a coaxial union into constant-section bands, bottom to top.
///
/// Returns one [`Band`] per height interval over which the set of active
/// operands does not change. A union whose operands span the same height
/// yields exactly one band, which is the case
/// [`boolean_prisms_exact`](crate::boolean_exact::boolean_prisms_exact)
/// already handles.
///
/// Operands that do not touch are refused: their union is two separate
/// solids, and a band stack describes one.
pub fn union_prisms_stepped(
subject: &Prism,
tool: &Prism,
tolerance: Tolerance,
) -> GeomResult<Vec<Band>> {
if tool.bottom > subject.top + tolerance.linear()
|| subject.bottom > tool.top + tolerance.linear()
{
return Err(unsupported(
"stepped union of prisms that do not meet along the axis",
));
}
// Every height where an operand starts or stops is a potential
// section change. Heights closer than tolerance are the SAME cut:
// keeping both would emit a zero-thickness band that no solid can
// represent.
let mut cuts = vec![subject.bottom, subject.top, tool.bottom, tool.top];
cuts.sort_by(|a, b| a.total_cmp(b));
cuts.dedup_by(|a, b| tolerance.eq(*a, *b));
let mut bands = Vec::with_capacity(cuts.len().saturating_sub(1));
for pair in cuts.windows(2) {
let (bottom, top) = (pair[0], pair[1]);
// The midpoint decides membership: it is interior to the band, so
// it cannot land on a boundary and give an ambiguous answer.
let middle = 0.5 * (bottom + top);
let in_subject = middle > subject.bottom && middle < subject.top;
let in_tool = middle > tool.bottom && middle < tool.top;
let rings = match (in_subject, in_tool) {
(false, false) => continue,
(true, false) => subject.rings.clone(),
(false, true) => tool.rings.clone(),
// Both active: the band's section is their planar union, which
// is exactly the overlay the single-prism path performs.
(true, true) => section_union(subject, tool, tolerance)?,
};
bands.push(Band { rings, bottom, top });
}
if bands.is_empty() {
return Err(GeomError::Degenerate(
"stepped union has no band of positive height".to_owned(),
));
}
Ok(bands)
}
/// Planar union of the two cross-sections.
fn section_union(
subject: &Prism,
tool: &Prism,
tolerance: Tolerance,
) -> GeomResult<Vec<Vec<axiolid_core::Point2>>> {
let frame = Frame2 {
origin: Vec2::ZERO,
x: Vec2::X,
y: Vec2::Y,
};
let result = overlay(
&OverlayInput {
frame,
polygons: to_polygons(subject),
},
&OverlayInput {
frame,
polygons: to_polygons(tool),
},
OverlayOperation::Union,
FillRule::NonZero,
tolerance,
)
.map_err(|error| GeomError::BackendContractViolation {
backend: BACKEND_ID,
detail: format!("stepped union cross-section overlay failed: {error:?}"),
})?;
if result.polygons.len() != 1 {
return Err(unsupported(
"stepped union band with a disconnected cross-section",
));
}
let polygon = &result.polygons[0];
let mut rings = Vec::with_capacity(1 + polygon.holes.len());
rings.push(polygon.outer.points.clone());
for hole in &polygon.holes {
rings.push(hole.points.clone());
}
Ok(rings)
}
fn to_polygons(prism: &Prism) -> Vec<Polygon> {
let mut rings = prism.rings.iter();
let outer = Ring {
points: rings.next().cloned().unwrap_or_default(),
};
let holes = rings.map(|r| Ring { points: r.clone() }).collect();
vec![Polygon { outer, holes }]
}