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
//! D — Datum. Ported from the retired `DatiumFeature`.
//!
//! Produces no solid; it registers the three orthogonal base planes
//! `${id}:XY` / `${id}:XZ` / `${id}:YZ` as named scene FRAMES so a later sketch can
//! resolve its plane by name, fully headless. The whole datum
//! carries an optional TRS `transform` (position + `rotationEuler` in DEGREES;
//! scale is orientation-neutral for a plane and dropped, matching the retired
//! quaternion path).
//!
//! Child plane normals (local, before the group TRS): XY has mesh normal +Z; XZ
//! (`rotation.x = π/2`) → −Y;
//! YZ (`rotation.y = π/2`) → +X. The sketch frame is then `Frame::from_origin_normal`
//! (the worldUp convention) applied to each (group-origin, world-normal).
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 transform = ctx.param("transform");
let position = read_vec3(ctx, transform, "position", [0.0, 0.0, 0.0])?;
let rotation_deg = read_vec3(ctx, transform, "rotationEuler", [0.0, 0.0, 0.0])?;
let rotation = [
rotation_deg[0].to_radians(),
rotation_deg[1].to_radians(),
rotation_deg[2].to_radians(),
];
let origin = Vec3::new(position[0], position[1], position[2]);
let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
for (suffix, local_normal) in [
("XY", Vec3::new(0.0, 0.0, 1.0)),
("XZ", Vec3::new(0.0, -1.0, 0.0)),
("YZ", Vec3::new(1.0, 0.0, 0.0)),
] {
// Rotate the WHOLE plane frame rigidly by the datum rotation — not just
// the normal. Derive the canonical in-plane axes from the UNROTATED
// local normal (worldUp convention, `from_origin_normal`), THEN rotate
// x/y/z together. This preserves the identity conventions AND makes all
// three planes share ONE rigid rotation about the common origin, so they
// track the transform gizmo's triad. (Previously only the normal was
// rotated and x/y were re-derived from worldUp per-plane, discarding the
// datum's roll and desyncing the quads at three different rotate angles.)
let base = Frame::from_origin_normal(origin, local_normal)?;
let frame = Frame {
origin,
x_axis: rotate_euler_xyz(base.x_axis, rotation),
y_axis: rotate_euler_xyz(base.y_axis, rotation),
z_axis: rotate_euler_xyz(base.z_axis, rotation),
};
result.frames.push((format!("{}:{suffix}", ctx.id), frame));
}
Ok(result)
}
/// Read a `[x, y, z]` component vector from a transform sub-field; each component
/// may be a number or an expression string (evaluated against the shared env).
fn read_vec3(
ctx: &FeatureContext,
transform: Option<&Value>,
key: &str,
default: [f64; 3],
) -> Result<[f64; 3], String> {
let array = transform.and_then(|t| t.get(key)).and_then(|v| v.as_array());
let mut out = default;
if let Some(array) = array {
for (index, slot) in out.iter_mut().enumerate() {
if let Some(value) = array.get(index) {
*slot = match value {
Value::Number(number) => number.as_f64().unwrap_or(*slot),
Value::String(source) if !source.trim().is_empty() => ctx
.env
.eval(source)
.map_err(|error| format!("datum: transform `{key}`: {error}"))?,
_ => *slot,
};
}
}
}
Ok(out)
}
/// Apply an intrinsic XYZ Euler rotation (default `XYZ` order) to a vector — the
/// exact rotation-matrix composition for Euler order `XYZ`, so datum orientation
/// byte-matches the editor.
pub fn rotate_euler_xyz(v: Vec3, euler: [f64; 3]) -> Vec3 {
let (c1, s1) = (euler[0].cos(), euler[0].sin());
let (c2, s2) = (euler[1].cos(), euler[1].sin());
let (c3, s3) = (euler[2].cos(), euler[2].sin());
// Column-major basis image for Euler order 'XYZ'.
let m00 = c2 * c3;
let m01 = -c2 * s3;
let m02 = s2;
let m10 = c1 * s3 + c3 * s1 * s2;
let m11 = c1 * c3 - s1 * s2 * s3;
let m12 = -c2 * s1;
let m20 = s1 * s3 - c1 * c3 * s2;
let m21 = c3 * s1 + c1 * s2 * s3;
let m22 = c1 * c2;
Vec3::new(
m00 * v.x + m01 * v.y + m02 * v.z,
m10 * v.x + m11 * v.y + m12 * v.z,
m20 * v.x + m21 * v.y + m22 * v.z,
)
}
/// 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": "D",
"shortName": "D",
"longName": "Datium",
"displayBuilder": true,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the datium feature"
},
"transform": {
"type": "transform",
"default_value": {
"position": [
0,
0,
0
],
"rotationEuler": [
0,
0,
0
],
"scale": [
1,
1,
1
]
},
"hint": "Position and rotation. Scale is ignored — a plane has no scale (it is orientation-neutral), and the Scale row the form draws for this widget shape has no effect here."
}
}
})
}
// BREP private tests: 2f7e787ea1841cac