* 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>
152 lines
7.1 KiB
Rust
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;
|