feat: strengthen sketches, feature editing, and independent assemblies
This commit is contained in:
1 parent
07932d0f42
commit
0398a813c8
65 files changed
+7082
-268
No files matched your search
@@ -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>> {
|
||||
|
||||
Reference in new issue
Block a user