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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Chamfer contract and single-edge exact reference implementation (#68).
//!
//! The oracle is the removed material, computed from the input parameters
//! rather than recorded from a run: chamfering a corner by distance d cuts a
//! right triangle of legs d, so the solid loses exactly d^2/2 * depth. That
//! is checked against the tessellated volume of the result, so a chamfer that
//! silently did nothing -- the worst failure, because the caller believes the
//! feature was applied -- fails loudly.
use axiolid_construct::feature::{chamfer_extruded_profile, EdgeSelector, FeatureSize};
use axiolid_contracts::GeomError;
use axiolid_core::{Point2, Tolerance, Vec3};
use axiolid_measure::exact_properties;
use axiolid_profile::{Profile, RectangleProfile};
fn rect(x: f64, y: f64) -> Profile {
Profile::Rectangle(RectangleProfile {
x,
y,
thickness: None,
outer_radius: None,
inner_radius: None,
})
}
/// The chamfered solid loses exactly the triangular prism the cut removes.
#[test]
fn a_chamfer_removes_the_expected_volume() {
let (x, y, depth, distance) = (4.0, 6.0, 2.0, 1.0);
let chamfered = chamfer_extruded_profile(
&rect(x, y),
Vec3::Z,
depth,
// The +x/+y corner of a 4 x 6 rectangle sits at (2, 3).
EdgeSelector::NearestCorner(Point2::new(2.0, 3.0)),
FeatureSize::ConstantDistance(distance),
Tolerance::METRE,
)
.expect("a constant-distance chamfer on a straight edge is constructible");
// A chamfered rectangle is a pentagon: the corner vertex is replaced by
// two. Five side walls plus two caps.
assert_eq!(
chamfered.topology().faces().len(),
7,
"chamfering one corner of a box yields a pentagonal prism"
);
// Every face is planar: the chamfer introduces no curved geometry.
assert!(
chamfered
.surfaces()
.iter()
.all(|s| matches!(s, axiolid_surface::Surface::Plane(_))),
"a chamfer must not introduce curved surfaces"
);
}
/// The chamfer's cross-section area matches the oracle.
///
/// Volume is derived from the solid's own vertices via the shoelace formula
/// on its distinct cross-section, so it measures what was BUILT rather than
/// what was requested. A no-op chamfer returns the full box area and fails.
#[test]
fn the_chamfered_cross_section_loses_the_triangle() {
let (x, y, depth, distance) = (4.0, 6.0, 2.0, 1.0);
let chamfered = chamfer_extruded_profile(
&rect(x, y),
Vec3::Z,
depth,
EdgeSelector::NearestCorner(Point2::new(2.0, 3.0)),
FeatureSize::ConstantDistance(distance),
Tolerance::METRE,
)
.expect("constructible");
// Recover the base cross-section: vertices at z = 0, deduplicated.
let mut base: Vec<(f64, f64)> = chamfered
.topology()
.vertices()
.iter()
.filter(|v| v.position.z.abs() < 1e-9)
.map(|v| (v.position.x, v.position.y))
.collect();
base.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9);
assert_eq!(
base.len(),
5,
"a chamfered rectangle is a pentagon: {base:?}"
);
// Volume comes from the measurement provider, not a local sum. The
// cross-section count above already pins the topology; this pins the
// magnitude against an oracle derived from the inputs.
let measured = exact_properties(&chamfered, Tolerance::METRE)
.expect("a chamfered prism is a closed planar solid");
// Oracle from the inputs: full rectangle minus the right triangle of
// legs `distance` that the cut removes, extruded through `depth`.
let expected_area = x * y - distance * distance / 2.0;
let expected_volume = expected_area * depth;
assert!(
(measured.signed_volume - expected_volume).abs() < 1e-9,
"expected volume {expected_volume}, measured {} (a no-op chamfer would give {})",
measured.signed_volume,
x * y * depth
);
}
/// A fillet is refused, not approximated by a many-segment chamfer.
///
/// A constant-radius blend needs a cylindrical wall stitched into the prism.
/// Substituting a polyline would be indistinguishable to the caller, which is
/// exactly the silent approximation the kernel exists to avoid.
#[test]
fn a_fillet_is_refused_rather_than_approximated() {
let error = chamfer_extruded_profile(
&rect(4.0, 6.0),
Vec3::Z,
2.0,
EdgeSelector::NearestCorner(Point2::new(2.0, 3.0)),
FeatureSize::ConstantRadius(1.0),
Tolerance::METRE,
)
.expect_err("no cylindrical-wall assembly exists yet");
assert!(
matches!(
error,
GeomError::UnsupportedInput {
input: "constant-radius fillet on an extruded solid",
..
}
),
"the refusal must name the missing capability, got {error:?}"
);
}
/// A chamfer consuming a whole edge changes the topology, so it is refused.
#[test]
fn an_oversized_chamfer_is_refused() {
// Distance 2.0 on a 4-wide rectangle: the two chamfers would meet.
let error = chamfer_extruded_profile(
&rect(4.0, 6.0),
Vec3::Z,
2.0,
EdgeSelector::NearestCorner(Point2::new(2.0, 3.0)),
FeatureSize::ConstantDistance(2.0),
Tolerance::METRE,
)
.expect_err("the chamfer consumes an entire edge");
assert!(matches!(error, GeomError::Degenerate(_)), "got {error:?}");
}
/// An oblique extrusion does not keep the vertical edges vertical.
#[test]
fn an_oblique_extrusion_is_refused() {
let error = chamfer_extruded_profile(
&rect(4.0, 6.0),
Vec3::new(1.0, 0.0, 1.0),
2.0,
EdgeSelector::NearestCorner(Point2::new(2.0, 3.0)),
FeatureSize::ConstantDistance(1.0),
Tolerance::METRE,
)
.expect_err("an oblique extrusion has no vertical edges to chamfer");
assert!(matches!(
error,
GeomError::UnsupportedInput {
input: "chamfer on an oblique extrusion",
..
}
));
}