Merge pull request #1688 from YsDirector/pr/d3-spline-api

Create and serialize splines through automation

# Conflicts:
#	src/app/control/entities.rs
This commit is contained in:
Hakan Seven committed 2026-10-11 23:26:06 +03:00
commit cdf8e60fbf
3 files changed
+191 -12

No files matched your search

+21
View File
@@ -284,6 +284,27 @@ pub(crate) fn entity_json(e: &codec::EntityType, detail: &str) -> Value {
map.insert("start_angle".into(), json!(a.start_angle));
map.insert("end_angle".into(), json!(a.end_angle));
}
E::Spline(spline) => {
map.insert("degree".into(), json!(spline.degree));
map.insert("knots".into(), json!(spline.knots));
map.insert(
"control_points".into(),
json!(spline
.control_points
.iter()
.map(|point| v3(*point))
.collect::<Vec<_>>()),
);
map.insert(
"fit_points".into(),
json!(spline
.fit_points
.iter()
.map(|point| v3(*point))
.collect::<Vec<_>>()),
);
map.insert("weights".into(), json!(spline.weights));
}
E::Point(p) => {
map.insert("location".into(), v3(p.location));
}
+139 -12
View File
@@ -8,23 +8,84 @@ use crate::command::EntityTransform;
type Parsed<T> = Result<T, Value>;
/// `[x,y]` or `[x,y,z]` (missing z = 0) from `req[key]`.
fn point_field(req: &Value, key: &str) -> Parsed<codec::types::Vector3> {
let values = req[key]
.as_array()
/// Parse `[x,y]` or `[x,y,z]` without silently turning a malformed
/// coordinate into zero.
fn point_value(value: Option<&Value>, label: &str, code: &str) -> Parsed<codec::types::Vector3> {
let values = value
.and_then(Value::as_array)
.filter(|v| (2..=3).contains(&v.len()))
.ok_or_else(|| failure("invalid_point", format!("{key} must be [x,y] or [x,y,z]")))?;
.ok_or_else(|| failure(code, format!("{label} must be [x,y] or [x,y,z]")))?;
let coord = |i: usize| {
values[i]
.as_f64()
.filter(|v| v.is_finite())
.ok_or_else(|| failure("invalid_point", format!("{key} needs finite coordinates")))
.ok_or_else(|| failure(code, format!("{label} needs finite coordinates")))
};
Ok(codec::types::Vector3::new(
coord(0)?,
coord(1)?,
values.get(2).and_then(Value::as_f64).unwrap_or(0.0),
))
let z = values
.get(2)
.map(|value| {
value
.as_f64()
.filter(|v| v.is_finite())
.ok_or_else(|| failure(code, format!("{label} needs finite coordinates")))
})
.transpose()?
.unwrap_or(0.0);
Ok(codec::types::Vector3::new(coord(0)?, coord(1)?, z))
}
/// `[x,y]` or `[x,y,z]` (missing z = 0) from `req[key]`.
fn point_field(req: &Value, key: &str) -> Parsed<codec::types::Vector3> {
point_value(req.get(key), key, "invalid_point")
}
fn point_list_field(
spec: &Value,
key: &str,
minimum: usize,
) -> Parsed<Vec<codec::types::Vector3>> {
let points = spec[key]
.as_array()
.filter(|points| points.len() >= minimum)
.ok_or_else(|| {
failure(
"invalid_spline_points",
format!("{key} needs at least {minimum} points"),
)
})?;
points
.iter()
.enumerate()
.map(|(index, point)| {
point_value(
Some(point),
&format!("{key}[{index}]"),
"invalid_spline_points",
)
})
.collect()
}
fn finite_number_list_field(spec: &Value, key: &str) -> Parsed<Vec<f64>> {
let values = spec[key]
.as_array()
.filter(|values| !values.is_empty())
.ok_or_else(|| failure("invalid_spline_values", format!("{key} needs a non-empty array")))?;
values
.iter()
.enumerate()
.map(|(index, value)| {
value
.as_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| {
failure(
"invalid_spline_values",
format!("{key}[{index}] must be finite"),
)
})
})
.collect()
}
fn deg_field(req: &Value, key: &str) -> f64 {
@@ -132,6 +193,72 @@ fn build_entity(spec: &Value) -> Parsed<codec::EntityType> {
deg_field(spec, "end_angle_deg"),
))
}
"SPLINE" => {
let control_points = if spec.get("control_points").is_some() {
point_list_field(spec, "control_points", 2)?
} else {
Vec::new()
};
let fit_points = if spec.get("fit_points").is_some() {
point_list_field(spec, "fit_points", 2)?
} else {
Vec::new()
};
if control_points.is_empty() && fit_points.is_empty() {
return Err(failure(
"invalid_spline_points",
"spline needs control_points or fit_points (at least 2 points)",
));
}
let degree = match spec.get("degree") {
None | Some(Value::Null) => 3,
Some(value) => value.as_i64().unwrap_or(3),
};
let mut spline = if control_points.is_empty() {
Spline::from_fit_points(fit_points.clone())
} else {
Spline::from_control_points(degree as i32, control_points)
};
spline.degree = degree as i32;
if !fit_points.is_empty() {
spline.fit_points = fit_points;
}
if spec.get("knots").is_some() {
if spline.control_points.is_empty() {
return Err(failure(
"invalid_spline_values",
"spline knots require control_points",
));
}
spline.knots = finite_number_list_field(spec, "knots")?;
}
if spec.get("weights").is_some() {
if spline.control_points.is_empty() {
return Err(failure(
"invalid_spline_values",
"spline weights require control_points",
));
}
spline.weights = finite_number_list_field(spec, "weights")?;
spline.flags.rational = true;
}
if let Some(value) = spec["closed"].as_bool() {
spline.flags.closed = value;
}
if let Some(value) = spec["periodic"].as_bool() {
spline.flags.periodic = value;
}
if let Some(value) = spec["rational"].as_bool() {
spline.flags.rational = value;
}
if let Some(value) = spec["planar"].as_bool() {
spline.flags.planar = value;
}
if let Some(value) = spec["linear"].as_bool() {
spline.flags.linear = value;
}
EntityType::Spline(spline)
}
"LWPOLYLINE" | "POLYLINE" => {
let vertices = spec["vertices"]
.as_array()
@@ -264,7 +391,7 @@ fn build_entity(spec: &Value) -> Parsed<codec::EntityType> {
return Err(failure(
"unknown_entity_type",
format!(
"Unknown entity type '{other}'. Supported: Line, Circle, Arc, LwPolyline, Point, Text, MText, Insert, Solid, Hatch"
"Unknown entity type '{other}'. Supported: Line, Circle, Arc, Spline, LwPolyline, Point, Text, MText, Insert, Solid, Hatch"
),
))
}
+31
View File
@@ -432,6 +432,37 @@ fn plot_per_page_writes_one_pdf_per_layout() {
}
}
#[test]
fn entities_create_supports_control_and_fit_point_splines() {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let control = mutate(
&mut app,
r#"{"protocol":1,"op":"entities_create","request_id":"spline-control","document_id":{doc},"entities":[
{"type":"Spline","degree":2,"knots":[0,0,0,1,1,1],"control_points":[[0,0,0],[5,5,0],[10,0,0]],"weights":[1,2,1],"layer":"CURVES"}
]}"#,
);
assert_eq!(control["ok"], true, "{control}");
let fit = mutate(
&mut app,
r#"{"protocol":1,"op":"entities_create","request_id":"spline-fit","document_id":{doc},"entities":[
{"type":"Spline","degree":3,"fit_points":[[20,0],[25,5],[30,0]]}
]}"#,
);
assert_eq!(fit["ok"], true, "{fit}");
let query = app.automation_op(r#"{"op":"query","type":"Spline","detail":"geometry"}"#);
assert_eq!(query["count"], 2, "{query}");
let control_entity = query["entities"]
.as_array()
.unwrap()
.iter()
.find(|entity| entity["layer"] == "CURVES")
.unwrap();
assert_eq!(control_entity["degree"], 2, "{control_entity}");
assert_eq!(control_entity["control_points"].as_array().unwrap().len(), 3);
assert_eq!(control_entity["weights"], json!([1.0, 2.0, 1.0]));
}
#[test]
fn hatch_boundary_is_flattened_in_query() {
let mut app = OpenCADStudio::new_for_test();