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
//! P.CY — Primitive Cylinder. Ported from the retired `Primitive Cylinder` feature.
//!
//! The cylinder is aligned along +Y with its base at `y=0` and top at `y=height`
//! (built from base `(0,0,0)`, axis `(0,1,0)`).
//!
//! Name fidelity (contract rule 2): the solid is named `${featureID}`; its three
//! faces are `${featureID}` + `_S, _B, _T` (Side, Bottom, Top). This ORDER matches
//! `make_cylinder_brep`'s shell face order `[side_face, bottom_face, top_face]`
//! (topology.rs:1666, verified by geometry: `_B` cap is at `y=0`, `_T` cap at
//! `y=height`).
use crate::feature_pipeline::features::{common, transform_bake};
use crate::feature_pipeline::{FeatureContext, FeatureResult};
use crate::{make_cylinder_brep, Vec3};
/// `make_cylinder_brep` face order → face-name suffix (Side, Bottom, Top).
const FACE_SUFFIX: [&str; 3] = ["_S", "_B", "_T"];
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
// radius is a magnitude (|radius|); height is DIRECTIONAL — a negative height
// extends the cylinder to the -Y side (built with the magnitude, then spun a
// half turn below). Diverges from the old `Math.abs` on height.
let radius = ctx.number("radius")?.abs();
let height = ctx.number("height")?;
let mut solid = make_cylinder_brep(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
radius,
height.abs(),
)?;
// Stamp the three face names in `make_cylinder_brep` order.
let faces = &mut solid
.shells
.get_mut(0)
.ok_or("cylinder builder produced no shell")?
.faces;
if faces.len() != FACE_SUFFIX.len() {
return Err(format!(
"cylinder builder produced {} faces, expected 3",
faces.len()
));
}
for (face, suffix) in faces.iter_mut().zip(FACE_SUFFIX) {
face.name = Some(format!("{}{}", ctx.id, suffix));
}
// A negative height spins the +Y cylinder a half turn onto the -Y side
// (proper rotation → names + winding preserved).
let solid = if height < 0.0 {
common::rotate_half_turn_about_x(solid)?
} else {
solid
};
// Bake the `transform` param BEFORE the boolean; the bake preserves the
// stamped names (geometry-only rewrite).
let solid = transform_bake::apply(ctx, solid)?;
Ok(common::finalize_solid(ctx, solid, &ctx.id))
}
/// Context-bar applicability ([`crate::feature_pipeline::context_offer`]):
/// never offered from a selection (no reference inputs).
pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
false
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "P.CY",
"shortName": "P.CY",
"longName": "Primitive Cylinder",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "Unique identifier for the feature"
},
"radius": {
"type": "number",
"default_value": 5,
"hint": "Radius of the cylinder"
},
"height": {
"type": "number",
"default_value": 10,
"hint": "Height of the cylinder along Y-axis"
},
"transform": common::primitive_transform_schema(),
"boolean": common::optional_boolean_schema()
}
})
}
// BREP private tests: 1d6cfa8e0e951f90