use crate::feature_pipeline::wire_harness::Attachment;
use crate::feature_pipeline::{FeatureContext, FeatureResult, SceneMap};
use crate::{make_line, NurbsCurve, Vec3, Vec4};
use serde_json::Value;
const DEGENERATE_EPS: f64 = 1e-12;
pub(crate) struct SplinePoint {
pub(crate) position: Vec3,
pub(crate) direction: Vec3,
pub(crate) forward: f64,
pub(crate) backward: f64,
}
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
let base = if ctx.id.is_empty() { "Spline" } else { ctx.id.as_str() };
let (anchors, unresolved) = parse_spline_points(ctx.persistent, ctx.scene);
result.unresolved = unresolved;
match build_chain(&anchors, bend_radius(ctx)) {
Ok(chain) if !chain.is_empty() => {
result.paths.push((base.to_string(), chain.clone()));
result.paths.push((format!("{base}:SplineEdge"), chain));
}
Ok(_) => {} Err(error) => return ctx.fail(format!("spline: {error}")),
}
for (index, anchor) in anchors.iter().enumerate() {
let name = format!("{base}:P{index}");
result
.points
.push((name.clone(), crate::feature_pipeline::ScenePoint::model(anchor.position)));
if let Ok(direction) = anchor.direction.normalized() {
result.axes.push((
name,
crate::feature_pipeline::Axis {
point: anchor.position,
direction,
},
));
}
}
result
}
pub(crate) fn parse_spline_points(persistent: &Value, scene: &SceneMap) -> (Vec<SplinePoint>, Vec<String>) {
let raw = persistent
.get("spline")
.and_then(|spline| spline.get("points"))
.and_then(Value::as_array);
let mut unresolved = Vec::new();
let points = match raw {
Some(list) if list.len() >= 2 => list
.iter()
.map(|point| parse_point(point, scene, &mut unresolved))
.collect(),
_ => vec![
SplinePoint {
position: Vec3::new(0.0, 0.0, 0.0),
direction: Vec3::new(1.0, 0.0, 0.0),
forward: 1.0,
backward: 1.0,
},
SplinePoint {
position: Vec3::new(5.0, 0.0, 0.0),
direction: Vec3::new(1.0, 0.0, 0.0),
forward: 1.0,
backward: 1.0,
},
],
};
(points, unresolved)
}
fn parse_point(point: &Value, scene: &SceneMap, unresolved: &mut Vec<String>) -> SplinePoint {
if let Some(attachment) = Attachment::parse(point.get("attachment")) {
match scene.resolve_port(&attachment.port_ref) {
Some(port) => {
return SplinePoint {
position: port.point,
direction: port.side_direction(attachment.side),
forward: port.extension,
backward: port.extension,
};
}
None => unresolved.push(attachment.port_ref.clone()),
}
}
let mut direction = rotation_x_axis(point.get("rotation"));
if point
.get("flipDirection")
.and_then(Value::as_bool)
.unwrap_or(false)
{
direction = direction.scale(-1.0);
}
SplinePoint {
position: read_vec3(point.get("position")),
direction,
forward: distance_or_default(point.get("forwardDistance")),
backward: distance_or_default(point.get("backwardDistance")),
}
}
fn read_vec3(value: Option<&Value>) -> Vec3 {
let component = |index: usize| -> f64 {
value
.and_then(Value::as_array)
.and_then(|array| array.get(index))
.and_then(|entry| match entry {
Value::Number(number) => number.as_f64(),
Value::String(text) => text.trim().parse::<f64>().ok(),
_ => None,
})
.filter(|number| number.is_finite())
.unwrap_or(0.0)
};
Vec3::new(component(0), component(1), component(2))
}
fn rotation_x_axis(value: Option<&Value>) -> Vec3 {
let identity = Vec3::new(1.0, 0.0, 0.0);
let Some(array) = value.and_then(Value::as_array) else {
return identity;
};
if array.len() != 9 {
return identity;
}
let axis: Vec<f64> = array
.iter()
.take(3)
.filter_map(Value::as_f64)
.filter(|number| number.is_finite())
.collect();
match axis.as_slice() {
[x, y, z] => Vec3::new(*x, *y, *z),
_ => identity,
}
}
fn distance_or_default(value: Option<&Value>) -> f64 {
value
.and_then(Value::as_f64)
.map(|number| number.max(0.0))
.unwrap_or(1.0)
}
fn bend_radius(ctx: &FeatureContext) -> f64 {
match ctx.param("bendRadius") {
None | Some(Value::Null) => 1.0,
Some(_) => match ctx.number("bendRadius") {
Ok(value) if value.is_finite() => value.clamp(0.1, 5.0),
_ => 1.0,
},
}
}
fn build_chain(points: &[SplinePoint], bend: f64) -> Result<Vec<NurbsCurve>, String> {
let mut chain: Vec<NurbsCurve> = Vec::new();
for pair in points.windows(2) {
let (a, b) = (&pair[0], &pair[1]);
let forward_ext = a.position.add(a.direction.scale(a.forward));
let backward_ext = b.position.sub(b.direction.scale(b.backward));
push_line(&mut chain, a.position, forward_ext)?;
push_hermite_span(&mut chain, a, b, forward_ext, backward_ext, bend)?;
push_line(&mut chain, backward_ext, b.position)?;
}
Ok(chain)
}
fn push_line(chain: &mut Vec<NurbsCurve>, start: Vec3, end: Vec3) -> Result<(), String> {
if end.sub(start).length() <= DEGENERATE_EPS {
return Ok(()); }
chain.push(make_line(start, end)?);
Ok(())
}
fn push_hermite_span(
chain: &mut Vec<NurbsCurve>,
a: &SplinePoint,
b: &SplinePoint,
p0: Vec3,
p1: Vec3,
bend: f64,
) -> Result<(), String> {
let ext_distance = p0.sub(p1).length();
let avg_ext_distance = (a.forward + b.backward) * 0.5;
let tangent_scale = (ext_distance * 0.3).max(avg_ext_distance * 0.5) * bend;
let t0 = normalize_or_zero(p0.sub(a.position)).scale(tangent_scale);
let t1 = normalize_or_zero(b.position.sub(p1)).scale(tangent_scale);
if ext_distance <= DEGENERATE_EPS
&& t0.length() <= DEGENERATE_EPS
&& t1.length() <= DEGENERATE_EPS
{
return Ok(()); }
let controls = vec![
Vec4::from_point(p0, 1.0),
Vec4::from_point(p0.add(t0.scale(1.0 / 3.0)), 1.0),
Vec4::from_point(p1.sub(t1.scale(1.0 / 3.0)), 1.0),
Vec4::from_point(p1, 1.0),
];
chain.push(NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
controls,
)?);
Ok(())
}
fn normalize_or_zero(vector: Vec3) -> Vec3 {
let length = vector.length();
if length == 0.0 {
vector
} else {
vector.scale(1.0 / length)
}
}
pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
false
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "SP",
"shortName": "SP",
"longName": "Spline",
"displayBuilder": true,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the spline feature"
},
"bendRadius": {
"type": "number",
"default_value": 1,
"label": "Bend Radius",
"hint": "Controls the smoothness of curve transitions. Lower values create sharper bends, higher values create smoother curves.",
"min": 0.1,
"step": 0.5
},
"splinePoints": {
"type": "spline_points",
"label": "Anchors",
"hint": "The through-points, edited in the anchor editor: position, travel direction, forward/backward straight run, and an optional port attachment (side A or B)."
}
}
})
}