use super::*;
use crate::feature_pipeline::ScenePoint;
use super::chain::{chain_segments, emit_chain, plan_length};
use super::frame::{pre_evaluate_expressions, resolve_frame};
use super::geometry::{circle_loop, ellipse_loop, Loop, SegKind, Segment, SegmentIds};
use super::project::projected_reference_paths;
use super::regions::{assemble_loops, classify_regions, emit_loop};
struct SketchOutput {
profile: Option<SketchProfile>,
unresolved: Vec<String>,
axes: Vec<(String, Axis)>,
path: Option<Vec<NurbsCurve>>,
path_segment_names: Option<Vec<Option<String>>>,
per_geometry: Vec<(String, Vec<NurbsCurve>)>,
projected: Vec<(String, Vec<NurbsCurve>)>,
frame: Frame,
points: Vec<(String, ScenePoint)>,
}
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(output) => {
let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
if let Some(profile) = output.profile {
result.profiles.push((ctx.id.clone(), profile));
}
if let Some(path) = output.path {
result.paths.push((ctx.id.clone(), path));
}
if let Some(names) = output.path_segment_names {
result.path_segment_names.push((ctx.id.clone(), names));
}
for entry in output.per_geometry {
result.paths.push(entry);
}
for entry in output.projected {
result.paths.push(entry);
}
result.frames.push((ctx.id.clone(), output.frame));
result.axes = output.axes;
result.points = output.points;
result.unresolved = output.unresolved;
result
}
Err(error) => ctx.fail(error),
}
}
fn point_key(value: &Value) -> String {
value.to_string()
}
fn build(ctx: &FeatureContext) -> Result<SketchOutput, String> {
let (frame, mut unresolved) = resolve_frame(ctx)?;
let projected = projected_reference_paths(ctx, &frame, &mut unresolved)?;
let mut sketch = match ctx.persistent.get("sketch") {
Some(value) if value.is_object() => value.clone(),
Some(value) => {
return Err(format!(
"sketch: `persistentData.sketch` must be an object, got {value}"
));
}
None => {
return Ok(SketchOutput {
profile: None,
unresolved,
axes: Vec::new(),
path: None,
path_segment_names: None,
per_geometry: Vec::new(),
projected,
frame,
points: Vec::new(),
});
}
};
pre_evaluate_expressions(ctx, &mut sketch)?;
pin_origin_point(&mut sketch, &frame);
let request = SolveSketchRequest {
sketch,
iterations: Some(2000),
remove_implied_duplicates: true,
tolerance: None,
distance_slide_threshold_ratio: None,
distance_slide_step_ratio: None,
distance_slide_min_step: None,
polish: Some(true),
};
let solved = solve_sketch(&request).map_err(|error| format!("sketch: solve failed: {error}"))?;
let solved_sketch = solved
.get("sketch")
.ok_or("sketch: solver returned no sketch")?;
let mut points: std::collections::HashMap<String, [f64; 2]> = std::collections::HashMap::new();
if let Some(array) = solved_sketch.get("points").and_then(|p| p.as_array()) {
for point in array {
let (Some(id), Some(x), Some(y)) = (
point.get("id"),
point.get("x").and_then(|v| v.as_f64()),
point.get("y").and_then(|v| v.as_f64()),
) else {
continue;
};
points.insert(point_key(id), [x, y]);
}
}
let point_of = |id: &Value| -> Option<[f64; 2]> { points.get(&point_key(id)).copied() };
let mut published_points: Vec<(String, ScenePoint)> = Vec::new();
if let Some(array) = solved_sketch.get("points").and_then(|p| p.as_array()) {
for point in array {
let (Some(id), Some(x), Some(y)) = (
point.get("id"),
point.get("x").and_then(|v| v.as_f64()),
point.get("y").and_then(|v| v.as_f64()),
) else {
continue;
};
if is_sketch_origin_point_id(id) {
continue;
}
let construction = point.get("construction").and_then(|v| v.as_bool()) == Some(true);
published_points.push((
format!("{}:P{}", ctx.id, id_display(id)),
ScenePoint { position: frame.to_3d(x, y), construction },
));
}
}
let mut segments: Vec<Segment> = Vec::new();
let mut whole_loops: Vec<Loop> = Vec::new();
let mut axes: Vec<(String, Axis)> = Vec::new();
if let Some(geometries) = solved_sketch.get("geometries").and_then(|g| g.as_array()) {
for geometry in geometries {
let geom_type = geometry.get("type").and_then(|v| v.as_str()).unwrap_or("");
let ids = geometry.get("points").and_then(|p| p.as_array());
let edge_name = geometry
.get("id")
.map(|id| format!("{}:G{}", ctx.id, id_display(id)));
let segment_ids = SegmentIds {
gid: geometry.get("id").and_then(numeric_id),
loop_id: geometry.get("loopId").and_then(numeric_id),
};
if geom_type == "line" {
if let Some(ids) = ids {
if ids.len() == 2 {
if let (Some(a), Some(b)) = (point_of(&ids[0]), point_of(&ids[1])) {
let point = frame.to_3d(a[0], a[1]);
if let Ok(direction) = frame.to_3d(b[0], b[1]).sub(point).normalized() {
if let Some(name) = edge_name.clone() {
axes.push((name, Axis { point, direction }));
}
}
}
}
}
}
if geometry.get("construction").and_then(|v| v.as_bool()) == Some(true) {
continue;
}
match (geom_type, ids) {
("line", Some(ids)) if ids.len() == 2 => {
let (Some(a), Some(b)) = (point_of(&ids[0]), point_of(&ids[1])) else {
continue;
};
segments.push(Segment {
a,
b,
kind: SegKind::Line,
name: edge_name,
ids: segment_ids,
});
}
("arc", Some(ids)) if ids.len() == 3 => {
let (Some(center), Some(start), Some(end)) =
(point_of(&ids[0]), point_of(&ids[1]), point_of(&ids[2]))
else {
continue;
};
let radius = (start[0] - center[0]).hypot(start[1] - center[1]);
if radius <= JOIN_TOL {
return Err("sketch: degenerate arc (zero radius)".into());
}
let start_angle = (start[1] - center[1]).atan2(start[0] - center[0]);
let end_angle = (end[1] - center[1]).atan2(end[0] - center[0]);
let mut sweep = (end_angle - start_angle).rem_euclid(TAU);
if sweep < 1e-6 {
sweep = TAU;
}
segments.push(Segment {
a: start,
b: end,
kind: SegKind::Arc {
center,
radius,
start_angle,
sweep,
},
name: edge_name,
ids: segment_ids,
});
}
("circle", Some(ids)) if ids.len() == 2 => {
let (Some(center), Some(rim)) = (point_of(&ids[0]), point_of(&ids[1])) else {
continue;
};
let radius = (rim[0] - center[0]).hypot(rim[1] - center[1]);
if radius <= JOIN_TOL {
continue;
}
whole_loops.push(circle_loop(center, radius, edge_name, segment_ids));
}
("ellipse", Some(ids)) if ids.len() >= 3 => {
let (Some(center), Some(major_end), Some(minor_end)) =
(point_of(&ids[0]), point_of(&ids[1]), point_of(&ids[2]))
else {
continue;
};
let mut ux = major_end[0] - center[0];
let mut uy = major_end[1] - center[1];
let a = ux.hypot(uy);
if a <= JOIN_TOL {
return Err("sketch: degenerate ellipse (zero-length major axis)".into());
}
ux /= a;
uy /= a;
let (px, py) = (-uy, ux);
let b = ((minor_end[0] - center[0]) * px + (minor_end[1] - center[1]) * py)
.abs();
if b <= JOIN_TOL {
return Err(
"sketch: degenerate ellipse (minor axis end collinear with the major axis)"
.into(),
);
}
whole_loops.push(ellipse_loop(
center,
[a * ux, a * uy],
[b * px, b * py],
edge_name,
segment_ids,
));
}
("bezier", Some(ids)) if ids.len() >= 4 && (ids.len() - 1) % 3 == 0 => {
let mut controls = Vec::with_capacity(ids.len());
for id in ids {
let Some(point) = point_of(id) else {
return Err("sketch: bezier references a missing point".into());
};
controls.push(point);
}
let a = controls[0];
let b = *controls.last().unwrap();
segments.push(Segment {
a,
b,
kind: SegKind::Bezier { controls },
name: edge_name,
ids: segment_ids,
});
}
("line" | "arc" | "circle" | "bezier" | "ellipse", _) => {
return Err(format!("sketch: malformed '{geom_type}' geometry"));
}
(other, _) => {
return Err(format!(
"sketch: profile geometry type '{other}' is not yet migrated to the Rust pipeline"
));
}
}
}
}
let mut per_geometry: Vec<(String, Vec<NurbsCurve>)> = Vec::new();
for segment in &segments {
if let Some(name) = &segment.name {
per_geometry.push((name.clone(), vec![segment.to_curve(&frame, false)?]));
}
}
let mut loop_plans: Vec<Vec<(usize, bool)>> = Vec::new();
let mut open_plans: Vec<Vec<(usize, bool)>> = Vec::new();
for chain in chain_segments(&segments) {
if chain.closed {
if chain.entries.len() < 2 {
return Err("sketch: degenerate profile loop (< 2 edges)".into());
}
loop_plans.push(chain.entries);
} else {
open_plans.push(chain.entries);
}
}
let path_plan: Option<(&Vec<(usize, bool)>, bool)> = if let Some(plan) = open_plans
.iter()
.max_by(|a, b| plan_length(a, &segments).total_cmp(&plan_length(b, &segments)))
{
Some((plan, false))
} else if loop_plans.len() == 1 {
Some((&loop_plans[0], true))
} else {
None
};
let mut path = None;
let mut path_segment_names = None;
if let Some((plan, closed)) = path_plan {
path = Some(emit_chain(plan, &segments, &frame, closed)?);
path_segment_names = Some(
plan.iter()
.map(|&(index, _)| segments[index].name.clone())
.collect(),
);
}
let profile = build_profile(segments, loop_plans, whole_loops, &frame)?;
Ok(SketchOutput {
profile,
unresolved,
axes,
path,
path_segment_names,
per_geometry,
projected,
frame,
points: published_points,
})
}
fn build_profile(
segments: Vec<Segment>,
loop_plans: Vec<Vec<(usize, bool)>>,
mut whole_loops: Vec<Loop>,
frame: &Frame,
) -> Result<Option<SketchProfile>, String> {
let mut loops = assemble_loops(segments, loop_plans);
loops.append(&mut whole_loops);
if loops.is_empty() {
return Ok(None);
}
super::loop_ids::resolve(&mut loops);
let mut regions: Vec<Vec<ProfileLoop>> = Vec::new();
for region in classify_regions(loops)? {
let mut emitted = Vec::with_capacity(region.len());
for loop_data in ®ion {
emitted.push(emit_loop(loop_data, frame)?);
}
regions.push(emitted);
}
Ok(Some(SketchProfile {
origin: frame.origin,
x_axis: frame.x_axis,
y_axis: frame.y_axis,
z_axis: frame.z_axis,
regions,
}))
}
fn pin_origin_point(sketch: &mut Value, frame: &Frame) {
let Some(points) = sketch.get_mut("points").and_then(|p| p.as_array_mut()) else {
return;
};
let offset = Vec3::new(0.0, 0.0, 0.0).sub(frame.origin);
for point in points {
if !point.get("id").is_some_and(is_sketch_origin_point_id) {
continue;
}
point["x"] = Value::from(offset.dot(frame.x_axis));
point["y"] = Value::from(offset.dot(frame.y_axis));
point["fixed"] = Value::Bool(true);
point["construction"] = Value::Bool(true);
point["externalReference"] = Value::Bool(true);
}
}
fn numeric_id(value: &Value) -> Option<u64> {
match value {
Value::Number(number) => number.as_u64(),
Value::String(text) => text.parse().ok(),
_ => None,
}
}
fn id_display(value: &Value) -> String {
match value {
Value::Number(number) => {
if let Some(int) = number.as_i64() {
int.to_string()
} else if let Some(uint) = number.as_u64() {
uint.to_string()
} else {
number.to_string()
}
}
Value::String(text) => text.clone(),
other => other.to_string(),
}
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "S",
"shortName": "S",
"longName": "Sketch",
"ribbonPath": "Home/Reference/Sketch",
"commandSize": "Large",
"displayBuilder": true,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the sketch feature"
},
"sketchPlane": {
"type": "reference_selection",
"selectionFilter": [
"PLANE",
"FACE"
],
"multiple": false,
"default_value": null,
"hint": "Select the plane or face for the sketch",
"selectionValidationMessage": "Cannot use this sketch's own profile face as its sketch plane."
},
"projectedEdges": {
"type": "reference_selection",
"selectionFilter": [
"EDGE"
],
"multiple": true,
"default_value": null,
"hint": "Model edges projected onto the sketch plane as read-only reference geometry"
},
"editSketch": {
"type": "button",
"label": "Edit Sketch",
"default_value": null,
"hint": "Launch the 2D sketch editor"
}
}
})
}