Zebra, Draft and Curvature Map analyses (view state on the session, no undo step): the GPU shades the model's faces with reflected stripes, draft-angle bands to a pull direction (green, yellow within the angle, red) or a blue-green-red curvature map; the toolbar button toggles each

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 09:42:34 +00:00
1 parent 319386760f
commit dea1f46266
7 files changed
+216 -2

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
//! Surface analyses: zebra stripes, draft angle and curvature map shading of the model's faces.
//! View state only: no geometry changes and no undo step.
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_, num, vec3};
use crate::{Result, Session, SurfaceAnalysis};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("FusionZebraAnalysisCommand", "Zebra Analysis", zebra)
.at("SKETCH", "INSPECT")
.icon("zebra")
.noundo()
.params("stripes?: number across the view (default 12); clear?: true turns the analysis off"),
CommandSpec::new("FusionDraftAnalysisCommand", "Draft Analysis", draft)
.at("SKETCH", "INSPECT")
.icon("draft_analysis")
.noundo()
.params("pull?: [x,y,z] direction (default Z); angle?: degrees (default 1); clear?: true turns the analysis off"),
CommandSpec::new("FusionCurvatureMapAnalysisCommand", "Curvature Map Analysis", curvature_map)
.at("SKETCH", "INSPECT")
.icon("curvature_map")
.noundo()
.params("radius?: mm shown red (default: a tenth of the model's size); clear?: true turns the analysis off"),
];
fn set(s: &mut Session, p: &Value, a: SurfaceAnalysis) -> Result<Value> {
if bool_(p, "clear").unwrap_or(false) {
s.analysis = None;
} else {
s.analysis = Some(a);
}
s.revision += 1;
Ok(json!({ "analysis": s.analysis }))
}
fn zebra(s: &mut Session, p: &Value) -> Result<Value> {
let stripes = num(p, "stripes").unwrap_or(12.0);
if !(stripes.is_finite() && (1.0..=200.0).contains(&stripes)) {
return Err(bad("FusionZebraAnalysisCommand", "`stripes` must be 1…200"));
}
set(s, p, SurfaceAnalysis::Zebra { stripes })
}
fn draft(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "FusionDraftAnalysisCommand";
let pull = match p.get("pull") {
Some(v) => vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad(cmd, "`pull` must be a non-zero [x,y,z]"))?,
None => Vec3::Z,
};
let angle = num(p, "angle").unwrap_or(1.0);
if !(angle.is_finite() && (0.0..90.0).contains(&angle)) {
return Err(bad(cmd, "`angle` must be 0…90 degrees"));
}
set(s, p, SurfaceAnalysis::Draft { pull, angle })
}
fn curvature_map(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "FusionCurvatureMapAnalysisCommand";
let radius = match num(p, "radius") {
Some(r) if r.is_finite() && r > 1e-6 && r < 1e9 => r,
Some(_) => return Err(bad(cmd, "`radius` must be positive")),
None => {
let st = s.model.state();
let size = st.bodies.iter().map(|b| b.mesh().bounds().diagonal()).fold(0.0_f64, f64::max);
if size > 0.0 { size / 10.0 } else { 10.0 }
}
};
set(s, p, SurfaceAnalysis::Curvature { radius })
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::{Session, SurfaceAnalysis};
#[test]
fn surface_analyses_are_view_state() {
let mut s = Session::default();
s.execute("PrimitiveBox", &json!({"length": 10, "width": 10, "height": 10})).unwrap();
let undo = s.undo.len();
s.execute("FusionZebraAnalysisCommand", &json!({"stripes": 20})).unwrap();
assert_eq!(s.analysis, Some(SurfaceAnalysis::Zebra { stripes: 20.0 }));
s.execute("FusionDraftAnalysisCommand", &json!({"pull": [0, 0, 2], "angle": 3})).unwrap();
assert!(matches!(s.analysis, Some(SurfaceAnalysis::Draft { angle, .. }) if angle == 3.0));
s.execute("FusionCurvatureMapAnalysisCommand", &json!({})).unwrap();
assert!(matches!(s.analysis, Some(SurfaceAnalysis::Curvature { radius }) if (radius - 300f64.sqrt() / 10.0).abs() < 1e-6));
s.execute("FusionCurvatureMapAnalysisCommand", &json!({"clear": true})).unwrap();
assert_eq!(s.analysis, None);
assert_eq!(s.undo.len(), undo, "no undo steps");
assert!(s.execute("FusionZebraAnalysisCommand", &json!({"stripes": 0})).is_err());
assert!(s.execute("FusionDraftAnalysisCommand", &json!({"pull": [0, 0, 0]})).is_err());
}
}