Files
OneCAD/crates/engine/src/cmd/sketch_3d.rs
T
bherbruckandClaude Opus 5.5 296683229d Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)
* WIP deep rename (not for push)

* WIP2

* WIP3

* WIP4 wxs fix

* rustfmt

* Rename the remaining identifiers in merged crash-report code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:17:01 -04:00

152 lines
7.1 KiB
Rust

//! 3D sketch drawing: with the sketch's 3D Sketch option on (Sketch Palette), lines, splines and
//! points may leave the sketch plane. They are kept as drawn 3D curves (wires with fit points):
//! drawn and snapped to, movable point by point, and usable as sweep and pipe paths.
use serde_json::{Value, json};
use solidcraft_geom::Vec3;
use solidcraft_sketch::WireSpace;
use super::sketch::{edit, target_sketch};
use super::{CommandSpec, in_sketch};
use crate::params::{bad, req_vec3, vec3};
use crate::{Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.line3d", "3D Line", line3d).enabled(in_sketch).params(concat!(
"points: [[x,y,z]…] world points (2 or more): one 3D line per segment, joined end to end (needs 3D Sketch on)",
created_doc!()
)),
CommandSpec::new("sketch.spline3d", "3D Spline", spline3d)
.enabled(in_sketch)
.params(concat!("points: [[x,y,z]…] world fit points (2 or more): a smooth 3D curve through them (needs 3D Sketch on)", created_doc!())),
CommandSpec::new("sketch.point3d", "3D Point", point3d)
.enabled(in_sketch)
.params(concat!("point: [x,y,z] world point (needs 3D Sketch on)", created_doc!())),
CommandSpec::new("sketch.move3d", "Move 3D Point", move3d)
.enabled(in_sketch)
.params("wire: drawn 3D curve or point id, index?: fit point (default 0), to: [x,y,z] | by: [dx,dy,dz]; points joined to it move too"),
];
fn points(cmd: &str, p: &Value, min: usize) -> Result<Vec<Vec3>> {
let a = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`points` must list [x,y,z] points"))?;
if a.len() < min || a.len() > solidcraft_sketch::MAX_FIT_POINTS {
return Err(bad(cmd, format!("`points` needs {min} to {} points", solidcraft_sketch::MAX_FIT_POINTS)));
}
a.iter().map(|v| vec3(v).ok_or_else(|| bad(cmd, "a point must be [x, y, z]"))).collect()
}
fn needs_3d(sk: &solidcraft_sketch::Sketch, cmd: &str) -> Result<()> {
if sk.view.three_d { Ok(()) } else { Err(bad(cmd, "turn on 3D Sketch (sketch.options sketch_3d: true) to draw off the sketch plane")) }
}
/// World command inputs belong to the target sketch's owner, which may differ from the
/// active component when a script supplies an explicit sketch.
fn inverse_owner_frame(s: &Session, p: &Value, cmd: &str) -> Result<solidcraft_doc::Mat> {
let id = target_sketch(s, p, cmd)?;
let owner = s.doc.feature(id).map(|f| f.component).ok_or_else(|| bad(cmd, "target sketch is missing"))?;
if owner != 0 && owner != s.active_component {
if s.doc.occurrences.len() > 100_000 {
return Err(bad(cmd, "sketch owner placement expansion exceeds supported bounds"));
}
let mut children: std::collections::BTreeMap<u64, Vec<&solidcraft_doc::Occurrence>> = std::collections::BTreeMap::new();
for occurrence in &s.doc.occurrences {
children.entry(occurrence.parent).or_default().push(occurrence);
}
let mut stack = vec![(0, 0usize)];
let mut count = 0;
let mut visited = 0;
while let Some((component, depth)) = stack.pop() {
for o in children.get(&component).into_iter().flatten() {
visited += 1;
if depth >= 64 || visited > 100_000 {
return Err(bad(cmd, "sketch owner placement expansion exceeds supported bounds"));
}
if o.component == owner {
count += 1;
if count > 1 {
return Err(bad(cmd, "activate the desired sketch owner occurrence before editing world 3D points"));
}
}
stack.push((o.component, depth + 1));
}
}
}
let frame = if owner == s.active_component { s.active_component_frame() } else { s.placed_document().component_transform(owner) };
solidcraft_doc::mat_inverse(&frame).ok_or_else(|| bad(cmd, "the sketch owner placement is not invertible"))
}
fn line3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.line3d";
let inverse = inverse_owner_frame(s, p, cmd)?;
let pts: Vec<Vec3> = points(cmd, p, 2)?.into_iter().map(|at| solidcraft_doc::apply_point(&inverse, at)).collect();
let (ids, info) = edit(s, p, cmd, false, |sk, _| {
needs_3d(sk, cmd)?;
pts.windows(2).map(|w| sk.add_drawn_wire_in(w, WireSpace::Component).map_err(Into::into)).collect::<Result<Vec<String>>>()
})?;
Ok(json!({"curves": ids, "sketch": info}))
}
fn spline3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.spline3d";
let inverse = inverse_owner_frame(s, p, cmd)?;
let pts: Vec<Vec3> = points(cmd, p, 2)?.into_iter().map(|at| solidcraft_doc::apply_point(&inverse, at)).collect();
let (id, info) = edit(s, p, cmd, false, |sk, _| {
needs_3d(sk, cmd)?;
Ok(sk.add_drawn_wire_in(&pts, WireSpace::Component)?)
})?;
Ok(json!({"curves": [id], "sketch": info}))
}
fn point3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.point3d";
let inverse = inverse_owner_frame(s, p, cmd)?;
let at = solidcraft_doc::apply_point(&inverse, req_vec3(cmd, p, "point")?);
let (id, info) = edit(s, p, cmd, false, |sk, _| {
needs_3d(sk, cmd)?;
Ok(sk.add_drawn_wire_in(&[at], WireSpace::Component)?)
})?;
Ok(json!({"point": id, "sketch": info}))
}
fn move3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.move3d";
let wire = p.get("wire").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`wire` must be a 3D curve or point id"))?.to_string();
let index = match p.get("index") {
None => 0,
Some(v) => v
.as_u64()
.and_then(|v| usize::try_from(v).ok())
.filter(|v| *v < solidcraft_sketch::MAX_FIT_POINTS)
.ok_or_else(|| bad(cmd, "`index` must be a fit point index below 500"))?,
};
let parse = |name| p.get(name).map(|v| vec3(v).ok_or_else(|| bad(cmd, format!("`{name}` must be [x,y,z]")))).transpose();
let to = parse("to")?;
let by = parse("by")?;
let inverse = inverse_owner_frame(s, p, cmd)?;
let (n, info) = edit(s, p, cmd, false, |sk, _| {
let drawn = sk
.wire_index(&wire)
.and_then(|w| sk.wires.get(w))
.filter(|w| !w.fit.is_empty() && w.link.is_none())
.ok_or_else(|| bad(cmd, format!("`{wire}` is not an editable drawn 3D curve")))?;
let from = drawn.fit.get(index).copied().ok_or_else(|| bad(cmd, format!("`{wire}` has no drawn point {index}")))?;
let to = match (to, by) {
(Some(t), _) => {
if drawn.space == WireSpace::Component {
solidcraft_doc::apply_point(&inverse, t)
} else {
t
}
}
(None, Some(d)) => from + if drawn.space == WireSpace::Component { solidcraft_doc::apply_vector(&inverse, d) } else { d },
_ => return Err(bad(cmd, "give `to` or `by`")),
};
Ok(sk.move_drawn_point(&wire, index, to)?)
})?;
Ok(json!({"moved": n, "sketch": info}))
}
#[cfg(test)]
#[path = "sketch_3d_frame_tests.rs"]
mod frame_tests;