forked from 1b-agent/opencadstudio-upstream
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:
commit
cdf8e60fbf
3 files changed
+191
-12
No files matched your search
@@ -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
@@ -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"
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user