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
//! P — Plane. Ported from the retired `PlaneFeature`.
//!
//! Produces no solid; it registers ONE named scene FRAME under `${id}` (the
//! selectable mesh name is the featureID) so a later sketch
//! can resolve its plane by name, fully headless. The frame is:
//! - a `datum`/`face` reference's (origin, normal); or
//! - the `orientation` (XY/XZ/YZ) base normal;
//! offset along the normal by `offset_distance`, then run through the worldUp
//! convention ([`Frame::from_origin_normal`]) — the same basis a sketch would
//! derive when it references this plane.
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{FeatureContext, FeatureResult, Frame};
use crate::Vec3;
use serde_json::Value;
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
let offset = match ctx.param("offset_distance") {
Some(Value::Null) | None => 0.0,
Some(_) => ctx.number("offset_distance")?,
};
// A `datum` reference (a datum/construction PLANE or a FACE) takes priority
// over `orientation`, matching `PlaneFeature.createPlaneMesh` (`datum ? … : …`).
// The shared plane resolver treats a datum plane and a planar face identically.
let (mut origin, normal) = match common::first_reference_name(ctx.param("datum")) {
Some(name) => match common::resolve_plane_reference(ctx, &name) {
Some(plane_ref) => {
let frame = plane_ref.frame()?;
(frame.origin, frame.z_axis)
}
None => {
// Unresolved reference — register no frame; the caller repairs later.
let mut result =
FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
result.unresolved.push(name);
return Ok(result);
}
},
None => (Vec3::new(0.0, 0.0, 0.0), orientation_normal(ctx.param("orientation"))),
};
let unit = normal.normalized()?;
origin = origin.add(unit.scale(offset));
let frame = Frame::from_origin_normal(origin, normal)?;
let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
result.frames.push((ctx.id.clone(), frame));
Ok(result)
}
/// The base plane normal for an `orientation` (matching `#basisFromOrientation`:
/// XY→+Z, XZ (`rotX π/2`)→−Y, YZ (`rotY π/2`)→+X).
fn orientation_normal(param: Option<&Value>) -> Vec3 {
match param
.and_then(|v| v.as_str())
.map(|text| text.trim().to_uppercase())
.as_deref()
{
Some("XZ") => Vec3::new(0.0, -1.0, 0.0),
Some("YZ") => Vec3::new(1.0, 0.0, 0.0),
_ => Vec3::new(0.0, 0.0, 1.0),
}
}
/// Context-bar applicability ([`crate::feature_pipeline::context_offer`]):
/// a selected FACE or datum/construction PLANE can seat the plane (`datum`) — the
/// build path resolves both (an offset plane FROM a datum plane), so the offer
/// must appear for a plane selection too, not just a face.
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.faces > 0 || probe.planes > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "P",
"shortName": "P",
"longName": "Plane",
"displayBuilder": true,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the plane feature"
},
"datum": {
"type": "reference_selection",
"selectionFilter": [
"PLANE",
"FACE"
],
"multiple": false,
"default_value": null,
"hint": "Optional reference plane or face"
},
"orientation": {
"type": "options",
"options": [
"XY",
"XZ",
"YZ"
],
"default_value": "XY",
"hint": "Plane orientation"
},
"offset_distance": {
"type": "number",
"default_value": 0,
"hint": "Plane offset distance"
}
}
})
}
// BREP private tests: 27ffd9874b26bcc7