feat: strengthen sketches, feature editing, and independent assemblies

This commit is contained in:
bherbruck committed 2026-10-11 02:44:31 -04:00
1 parent 07932d0f42
commit 0398a813c8
65 files changed
+7082 -268

No files matched your search

+92 -7
View File
@@ -29,7 +29,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.emboss", "Emboss", emboss)
.at("SOLID", "CREATE")
.icon("emboss")
.params("sketch (on a planar face), profiles?; depth; mode?: emboss|deboss (default emboss); targets?"),
.params("sketch, profiles?; depth; mode?: emboss|deboss; targets?; face?: world point [x,y,z] selecting a planar or complete exterior cylindrical wall (required for wrapped orthogonal line profiles)"),
CommandSpec::new("solid.replace_face", "Replace Face", replace_face)
.at("SOLID", "MODIFY")
.icon("offset_face")
@@ -143,12 +143,97 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
Some("deboss") => true,
Some(m) => return Err(bad(cmd, format!("unknown mode `{m}` (emboss or deboss)"))),
};
if let Some(fp) = p.get("face").and_then(vec3)
&& super::face::face_normal(s, fp).is_none()
{
return Err(bad(cmd, "not supported yet: embossing onto a curved face"));
}
add_feature(s, p, FeatureKind::Emboss { sketch, profiles, depth, deboss, targets: string_list(p, "targets") })
let mut targets = string_list(p, "targets");
let face = if let Some(value) = p.get("face") {
let local = vec3(value).ok_or_else(|| bad(cmd, "`face` must be a finite point [x,y,z]"))?;
let at = solvecraft_doc::apply_point(&s.active_component_frame(), local);
let state = s.world_state();
let mut candidates = Vec::new();
for body in &state.bodies {
let definition = body.definition_name();
if !targets.is_empty() && !targets.iter().any(|n| n == definition || n == &body.name) {
continue;
}
let mesh = body.mesh();
let closest = mesh
.triangles
.iter()
.zip(&mesh.tri_face)
.filter_map(|(tri, face)| {
let [a, b, c] = mesh.tri(tri)?;
let n = (b - a).cross(c - a).normalized()?;
let q = at - n * (at - a).dot(n);
let inside = [(a, b), (b, c), (c, a)].iter().all(|(u, v)| (*v - *u).cross(q - *u).dot(n) >= -1e-9);
inside.then_some(((at - a).dot(n).abs(), *face as usize))
})
.min_by(|a, b| a.0.total_cmp(&b.0));
if let Some((distance, index)) = closest
&& distance < (body.body.size() * 1e-3).max(1e-3)
{
candidates.push((body, index));
}
}
if candidates.len() != 1 {
return Err(bad(cmd, "`face` must identify exactly one target body surface"));
}
let (selected, index) = candidates.first().copied().ok_or_else(|| bad(cmd, "no target face"))?;
let name = selected.definition_name();
let body = &selected.body;
match solvecraft_kernel::face_surface(body, index) {
Some(solvecraft_kernel::FaceSurface::Plane { .. }) => {
targets = vec![name.to_owned()];
None
}
Some(solvecraft_kernel::FaceSurface::Cylinder { .. }) => {
if targets.len() > 1 {
return Err(bad(cmd, "cylinder emboss needs one selected target body"));
}
// Viewport picks lie on tessellated chords. Keep their axial and
// angular position, and certify their distance before storing an
// exact surface anchor for later replay.
let at = solvecraft_kernel::cylinder_emboss_anchor(body, index, at).map_err(|e| bad(cmd, e.to_string()))?;
let original = s.model.state();
let owner = s.doc.body_component(name, selected.feature);
let paths: Vec<_> = s
.doc
.placements()
.into_iter()
.filter(|(comp, path, _)| *comp == owner && s.doc.world_body_name(&original, name, owner, path) == selected.name)
.map(|(_, path, _)| path)
.collect();
if paths.len() != 1 {
return Err(bad(cmd, "selected cylinder placement is ambiguous"));
}
let path = paths.into_iter().next().ok_or_else(|| bad(cmd, "selected cylinder placement"))?;
if path.len() > 64 {
return Err(bad(cmd, "selected cylinder placement is too deep"));
}
let author_path = s.active_occurrence_path.clone().unwrap_or_else(|| s.doc.first_path(s.active_component));
if author_path.len() > 64 {
return Err(bad(cmd, "author placement is too deep"));
}
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
let sketch_path = if sketch_owner == owner {
path.clone()
} else if sketch_owner == s.active_component {
author_path.clone()
} else {
s.doc.first_path(sketch_owner)
};
if path.iter().chain(&author_path).chain(&sketch_path).any(|id| s.pending_moves.contains_key(id)) {
return Err(bad(cmd, "capture pending source, author and profile positions with Capture Position before cylinder emboss"));
}
let inverse = solvecraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid active frame"))?;
let at = solvecraft_doc::apply_point(&inverse, at);
targets = vec![name.to_owned()];
Some(solvecraft_doc::EmbossFace { body: name.to_owned(), at, path, author_path })
}
_ => return Err(bad(cmd, "only planar faces and complete exterior cylindrical walls support emboss")),
}
} else {
None
};
add_feature(s, p, FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face })
}
fn face_points(p: &Value, k: &str, cmd: &str) -> Result<Vec<Vec3>> {