feat: stabilize face attachments and closed spline workflows
This commit is contained in:
1 parent
c1d819f235
commit
a6bb8fc924
83 files changed
+6271
-385
No files matched your search
@@ -11,6 +11,8 @@ use crate::{EngineError, Result, Session};
|
||||
|
||||
#[path = "sketch_dimensioned.rs"]
|
||||
mod dimensioned;
|
||||
#[path = "sketch_face.rs"]
|
||||
pub(super) mod face;
|
||||
use dimensioned::draw as draw_dimensioned;
|
||||
|
||||
/// The `connect` parameter every drawing command takes (see `connect_points`).
|
||||
@@ -24,8 +26,8 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("sketch.create", "Create Sketch", create_sketch)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("sketch")
|
||||
.params("plane: XY|XZ|YZ | construction plane name or id | {origin, x_dir, y_dir} | {face: [x,y,z]} (planar face at point), offset?: expr, name?"),
|
||||
CommandSpec::new("sketch.edit", "Edit Sketch", edit_sketch).icon("sketch").params("sketch: id or name"),
|
||||
.params("plane: XY|XZ|YZ | construction plane name or id | {origin, x_dir, y_dir} | {face: [world x,y,z], body?: exact world body, index?: face index, path?: complete occurrence ids, fixed?: bool}; new face attachments require a unique certified source, fixed:true explicitly freezes the selected plane; offset?: expr, project_edges?: bool, name?"),
|
||||
CommandSpec::new("sketch.edit", "Edit Sketch", edit_sketch).noundo().icon("sketch").params("sketch: id or name"),
|
||||
CommandSpec::new("sketch.finish", "Finish Sketch", finish_sketch).at("SKETCH", "FINISH SKETCH").icon("finish").enabled(in_sketch).noundo(),
|
||||
CommandSpec::new("sketch.line", "Line", draw_line)
|
||||
.at("SKETCH", "CREATE")
|
||||
@@ -232,6 +234,7 @@ pub(super) fn edit<T>(
|
||||
if strict && !rep.ok() {
|
||||
return Err(EngineError::Other(format!("that would over-constrain the sketch (conflicts with {})", rep.failing.join(", "))));
|
||||
}
|
||||
sk.validate_periodic_splines()?;
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
Ok((out, json!({"solved": rep.ok(), "dof": rep.dof})))
|
||||
@@ -357,36 +360,56 @@ pub(super) fn result(sk_out: (Vec<String>, Vec<String>), info: Value) -> Value {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Sketch lifecycle
|
||||
|
||||
pub(crate) fn author_path_for_rebind(s: &Session, feature: &solvecraft_doc::Feature, p: &Value, cmd: &str) -> Result<Vec<u64>> {
|
||||
let mut saved = None;
|
||||
if let FeatureKind::Sketch { plane, .. } = &feature.kind {
|
||||
let mut plane = plane;
|
||||
loop {
|
||||
match plane {
|
||||
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => plane = base,
|
||||
PlaneRef::Face { anchor: Some(anchor), .. } => {
|
||||
saved = Some(anchor.author_path.clone());
|
||||
break;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
let path = if let Some(value) = p.get("author_path") {
|
||||
value
|
||||
.as_array()
|
||||
.filter(|path| path.len() <= 64)
|
||||
.ok_or_else(|| bad(cmd, "author_path must contain at most64 ids"))?
|
||||
.iter()
|
||||
.map(|id| id.as_u64().filter(|id| *id != 0).ok_or_else(|| bad(cmd, "invalid author_path id")))
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
} else if let Some(path) = saved {
|
||||
path
|
||||
} else if feature.component == s.active_component && s.active_occurrence_path.is_some() {
|
||||
s.active_occurrence_path.clone().unwrap_or_default()
|
||||
} else {
|
||||
let placements = super::component::properties::placements(&s.placed_document(), cmd)?;
|
||||
let paths: Vec<_> = placements.into_iter().filter(|(component, _, _)| *component == feature.component).map(|(_, path, _)| path).collect();
|
||||
if paths.len() != 1 {
|
||||
return Err(bad(cmd, "sketch author placement is ambiguous; provide exact author_path or activate the intended occurrence"));
|
||||
}
|
||||
paths.into_iter().next().ok_or_else(|| bad(cmd, "sketch author placement is missing"))?
|
||||
};
|
||||
if feature.component != 0 && path.is_empty() {
|
||||
return Err(bad(cmd, "a component sketch needs an explicit complete author_path"));
|
||||
}
|
||||
solvecraft_doc::EmbossFace::component_frame(&s.placed_document(), feature.component, &path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub(super) fn plane_ref(s: &Session, p: &Value, cmd: &str) -> Result<PlaneRef> {
|
||||
let base = match p.get("plane") {
|
||||
None => PlaneRef::Origin { name: "XY".into() },
|
||||
// An origin plane, or a construction plane by name or id.
|
||||
Some(v @ (Value::String(_) | Value::Number(_))) => super::features::plane_param(s, Some(v), cmd)?,
|
||||
Some(v @ Value::Object(o)) => {
|
||||
if let Some(fp) = o.get("face").and_then(vec3) {
|
||||
// A planar face of a body at this point, as the active component sees the model
|
||||
// (another component's face where it is placed).
|
||||
let model = s.model.state();
|
||||
let placed = s.placed_document();
|
||||
let st = solvecraft_doc::state_in_frame_at(&placed, &model, s.active_component, s.active_component_frame());
|
||||
let mut found = None;
|
||||
let mut face_name = None;
|
||||
for b in &st.bodies {
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
for f in b.body.faces(tol).unwrap_or_default() {
|
||||
if let Some(n) = f.plane_normal
|
||||
&& (fp - f.centroid).dot(n).abs() < tol * 10.0
|
||||
&& b.body.tessellate(tol).map(|m| face_contains(&m, f.index, fp, tol * 10.0)).unwrap_or(false)
|
||||
{
|
||||
// Sketch origin: the model origin projected onto the face plane.
|
||||
let d = (f.centroid + n * (fp - f.centroid).dot(n)).dot(n);
|
||||
found = Plane::from_normal(n * d, n);
|
||||
face_name = solvecraft_doc::naming::face_names(b).get(f.index).cloned();
|
||||
}
|
||||
}
|
||||
}
|
||||
let pl = found.ok_or_else(|| bad(cmd, "no planar face at that point"))?;
|
||||
PlaneRef::Face { plane: pl, at: fp, name: face_name }
|
||||
if o.contains_key("face") {
|
||||
face::capture(s, v, cmd)?
|
||||
} else {
|
||||
let origin = v.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let x = v.get("x_dir").and_then(vec3).ok_or_else(|| bad(cmd, "plane needs x_dir"))?;
|
||||
@@ -403,7 +426,7 @@ pub(super) fn plane_ref(s: &Session, p: &Value, cmd: &str) -> Result<PlaneRef> {
|
||||
}
|
||||
|
||||
/// Does triangle set of face `fi` contain point `p` (within `tol` of the face plane)?
|
||||
fn face_contains(m: &solvecraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> bool {
|
||||
pub(super) fn face_contains(m: &solvecraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> bool {
|
||||
m.triangles.iter().zip(&m.tri_face).filter(|(_, f)| **f as usize == fi).any(|(t, _)| {
|
||||
let Some([a, b, c]) = m.tri(t) else { return false };
|
||||
let n = (b - a).cross(c - a);
|
||||
@@ -423,13 +446,28 @@ fn face_contains(m: &solvecraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> boo
|
||||
|
||||
fn create_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.create";
|
||||
let plane = plane_ref(s, p, cmd)?;
|
||||
create_sketch_with_plane(s, p, plane_ref(s, p, cmd)?, None)
|
||||
}
|
||||
|
||||
/// Used only by face Extrude/Revolve/PressPull, preserving pre-certificate linked profiles.
|
||||
pub(super) fn create_face_profile(s: &mut Session, p: &Value, body: &str, index: usize) -> Result<Value> {
|
||||
let value = p.get("plane").ok_or_else(|| bad("face profile", "missing picked face plane"))?;
|
||||
let plane = face::capture_internal(s, value, "face profile")?;
|
||||
create_sketch_with_plane(s, p, plane, Some((body, index)))
|
||||
}
|
||||
|
||||
fn create_sketch_with_plane(s: &mut Session, p: &Value, plane: PlaneRef, internal_body: Option<(&str, usize)>) -> Result<Value> {
|
||||
if face::is_face(p) {
|
||||
face::capture_positions(s, &plane)?;
|
||||
}
|
||||
// Validate the plane resolves.
|
||||
let (vals, _) = s.doc.param_values();
|
||||
s.doc.resolve_plane_in(&vals, &plane, 0, Some(&s.model.state()))?;
|
||||
let name = str_(p, "name");
|
||||
let mut kind = FeatureKind::Sketch { plane, sketch: Sketch::new() };
|
||||
super::component::to_active_frame(s, &mut kind);
|
||||
if !face::is_face(p) {
|
||||
super::component::to_active_frame(s, &mut kind);
|
||||
}
|
||||
let id = s.doc_mut().add_feature(kind, name)?;
|
||||
let comp = s.active_component;
|
||||
if let Some(f) = s.doc_mut().feature_mut(id) {
|
||||
@@ -444,8 +482,23 @@ fn create_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
&& bool_(p, "project_edges").unwrap_or(s.auto_project)
|
||||
{
|
||||
link = super::sketch_project::auto_project_face(s, id, at)?;
|
||||
if link.is_none()
|
||||
&& let Some((body, index)) = internal_body
|
||||
{
|
||||
link = Some(super::sketch_project::project_internal_face(s, id, body, index)?);
|
||||
}
|
||||
}
|
||||
Ok(json!({"sketch": id, "name": name, "projected": link}))
|
||||
let attachment = s.doc.feature(id).and_then(|feature| match &feature.kind {
|
||||
FeatureKind::Sketch { plane, .. } => Some(if face::anchor(plane).is_some() {
|
||||
"exact_face"
|
||||
} else if internal_body.is_some() {
|
||||
"internal_legacy_face"
|
||||
} else {
|
||||
"fixed"
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
Ok(json!({"sketch": id, "name": name, "projected": link,"attachment":attachment}))
|
||||
}
|
||||
|
||||
fn edit_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -458,7 +511,17 @@ fn edit_sketch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !matches!(f.kind, FeatureKind::Sketch { .. }) {
|
||||
return Err(bad("sketch.edit", format!("`{key}` is not a sketch")));
|
||||
}
|
||||
let (id, name) = (f.id, f.name.clone());
|
||||
let (id, name, component) = (f.id, f.name.clone(), f.component);
|
||||
let authored_path = match &f.kind {
|
||||
FeatureKind::Sketch { plane, .. } => face::anchor(plane).map(|anchor| anchor.author_path.clone()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(path) = authored_path {
|
||||
solvecraft_doc::EmbossFace::component_frame(&s.placed_document(), component, &path)
|
||||
.map_err(|error| bad("sketch.edit", format!("{error}; explicitly rebind the author placement before editing")))?;
|
||||
s.active_component = component;
|
||||
s.active_occurrence_path = (!path.is_empty()).then_some(path);
|
||||
}
|
||||
s.active_sketch = Some(id);
|
||||
s.revision += 1;
|
||||
Ok(json!({"sketch": id, "name": name}))
|
||||
@@ -1759,7 +1822,7 @@ fn drag_entity(sk: &mut Sketch, e: Ent, from: Vec2, to: Vec2) {
|
||||
}
|
||||
k => {
|
||||
// Lines, splines and conics slide with the cursor.
|
||||
for q in k.point_ids() {
|
||||
for q in k.point_ids().into_iter().collect::<std::collections::BTreeSet<_>>() {
|
||||
if let Some(at) = sk.point(q) {
|
||||
set(sk, q, at + d);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user