Appearances on faces, bodies and components (face > body > component > material > imported colour), with names and opacity and a built-in library of 20 (metals, ABS colours, glass, rubber, wood, paint); AppearanceCommand takes bodies, faces (points) or components, a library name or a colour with opacity, or nothing to clear; appearance.library, appearance.list (a body's resolved look and per-face colours); file format 3 turns the old body colour map into body appearances

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 11:37:33 +00:00
1 parent f413a86d3d
commit a283e424d9
8 files changed
+422 -30

No files matched your search

+42
View File
@@ -1032,3 +1032,45 @@ fn face_extrude_follows_edits() {
let want = 30.0 * 20.0 * 20.0 * PI / 2.0;
assert!(rel(added, want) < 1e-4, "{added} vs {want}");
}
/// Appearances on faces, bodies and components: face > body > component > material.
#[test]
fn appearances_by_face_body_and_component() {
let mut s = Session::default();
run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "Root"}));
run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Case"}));
run(&mut s, "PrimitiveBox", json!({"corner": [20, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Shell"}));
run(&mut s, "component.activate", json!({"component": "root"}));
let lib = run(&mut s, "appearance.library", json!({}));
assert!(lib["appearances"].as_array().is_some_and(|a| a.len() >= 15 && a.iter().any(|x| x["name"] == "Glass - Clear")));
let look = |s: &mut Session, b: &str| run(s, "appearance.list", json!({"body": b}))["body"].clone();
// Material, then component, then body.
run(&mut s, "PhysicalMaterialCommand", json!({"bodies": ["Shell"], "material": "Brass"}));
assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "Brass");
run(&mut s, "AppearanceCommand", json!({"components": ["Case"], "appearance": "ABS - Blue"}));
assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "ABS - Blue");
assert!(look(&mut s, "Root")["appearance"].is_null(), "root bodies have no component look");
run(&mut s, "AppearanceCommand", json!({"bodies": ["Shell"], "color": [255, 0, 0], "opacity": 0.5}));
let l = look(&mut s, "Shell");
assert_eq!((l["appearance"]["color"].as_str(), l["appearance"]["opacity"].as_f64()), (Some("#ff0000"), Some(0.5)), "{l}");
// A face: the top of Shell.
run(&mut s, "AppearanceCommand", json!({"faces": [[25, 5, 10]], "appearance": "Glass - Clear"}));
let l = look(&mut s, "Shell");
let faces = l["faces"].as_array().cloned().unwrap_or_default();
assert_eq!(faces.len(), 1, "{l}");
assert_eq!(faces[0]["appearance"]["name"], "Glass - Clear");
// Clearing goes back down the chain.
run(&mut s, "AppearanceCommand", json!({"faces": [[26, 6, 10]]}));
assert_eq!(look(&mut s, "Shell")["faces"].as_array().map(Vec::len), Some(0), "cleared by another point on the same face");
run(&mut s, "AppearanceCommand", json!({"bodies": ["Shell"], "color": null}));
assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "ABS - Blue");
run(&mut s, "AppearanceCommand", json!({"components": ["Case"]}));
assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "Brass");
// Saved and opened again.
run(&mut s, "AppearanceCommand", json!({"faces": [[5, 5, 10]], "color": "#00ff00"}));
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.appearances, s.doc.appearances);
assert!(s.execute("AppearanceCommand", &json!({"bodies": ["Shell"], "appearance": "Unobtainium"})).is_err());
assert!(s.execute("AppearanceCommand", &json!({"color": "#ffffff"})).is_err(), "no target");
assert!(s.execute("AppearanceCommand", &json!({"faces": [[500, 500, 500]], "color": "#ffffff"})).is_err(), "no face there");
}