Imported looks outlive later features: a body's imported colour, opacity and face colours (kernel paint, dropped by operations) are carried to the next version of the body, its faces by persistent name (split faces keep the colour, new faces take the body's); assigned face appearances already follow their faces by name

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 12:47:56 +00:00
1 parent 2ea9955621
commit 9ee8ba5dd7
12 files changed
+120 -45

No files matched your search

+40 -8
View File
@@ -47,6 +47,33 @@ pub struct FaceLook {
pub name: Option<String>,
}
/// The look a body was imported with, carried through later features (which build new kernel
/// bodies without it): its colour and opacity, and its faces' colours by persistent face name.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ImportedLook {
pub color: Option<[f32; 3]>,
pub opacity: Option<f32>,
pub faces: BTreeMap<String, ([f32; 3], f32)>,
}
impl ImportedLook {
/// A body's imported look: its own kernel paint, else what it carries.
pub fn of(b: &crate::ModelBody) -> Option<std::sync::Arc<ImportedLook>> {
if b.body.paint().is_none() && b.body.color().is_none() {
return b.imported.clone();
}
let names = crate::naming::face_names(b);
let faces = b
.body
.paint()
.map(|p| {
p.faces.iter().filter_map(|f| names.get(f.face).map(|n| (crate::naming::strip_piece(n).to_string(), (f.color, f.opacity)))).collect()
})
.unwrap_or_default();
Some(std::sync::Arc::new(ImportedLook { color: b.body.color(), opacity: b.body.paint().map(|p| p.opacity), faces }))
}
}
/// The design's appearances.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Appearances {
@@ -126,15 +153,20 @@ impl Document {
/// assignment to the same face wins.
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
// Face colours the body was imported with, under the design's own.
let mut out: Vec<(usize, Look)> = b
.body
.paint()
.map(|p| {
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect()
})
.unwrap_or_default();
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
let names = crate::naming::face_names(b);
let mut out: Vec<(usize, Look)> = match b.body.paint() {
Some(p) => p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect(),
// Carried from an earlier version of the body: by face name.
None => match &b.imported {
Some(imp) => names
.iter()
.enumerate()
.filter_map(|(i, n)| imp.faces.get(crate::naming::strip_piece(n)).map(|(c, o)| (i, Look::custom(c.map(to8), *o as f64))))
.collect(),
None => Vec::new(),
},
};
for f in self.appearances.faces.iter().filter(|f| f.body == b.name) {
let (p, look) = (f.point, f.look.clone());
// By name (every piece of a split face), else by the point.
+6 -2
View File
@@ -1329,7 +1329,7 @@ impl Document {
/// else its component's, else its material's look, else the colour it was imported with.
/// (Faces may have their own: `face_looks`.)
pub fn body_color(&self, b: &crate::ModelBody) -> Option<[f32; 3]> {
self.body_look(&b.name, b.feature).map(|l| l.rgb()).or_else(|| b.body.color())
self.body_look(&b.name, b.feature).map(|l| l.rgb()).or_else(|| b.body.color()).or_else(|| b.imported.as_ref().and_then(|i| i.color))
}
/// The model with each body carrying the colour, opacity and face colours it is shown with
@@ -1344,7 +1344,11 @@ impl Document {
.into_iter()
.map(|(face, l)| solvecraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
.collect();
let opacity = look.map(|l| l.opacity as f32).or_else(|| b.body.paint().map(|p| p.opacity)).unwrap_or(1.0);
let opacity = look
.map(|l| l.opacity as f32)
.or_else(|| b.body.paint().map(|p| p.opacity))
.or_else(|| b.imported.as_ref().and_then(|i| i.opacity))
.unwrap_or(1.0);
let paint = solvecraft_kernel::Paint { opacity, faces };
if c != b.body.color() || b.body.paint() != Some(&paint) {
b.body = b.body.clone().with_color(c).with_paint(Some(paint));
+10 -1
View File
@@ -27,11 +27,13 @@ pub struct ModelBody {
pub(crate) names: crate::naming::NameCell,
/// The component the body belongs to (set after each feature).
pub component: u64,
/// The look it was imported with, carried through later features.
pub imported: Option<Arc<crate::appearance::ImportedLook>>,
}
impl ModelBody {
pub fn new(name: String, body: Body, feature: u64) -> Self {
ModelBody { name, body, feature, mesh: OnceLock::new(), key: 0, names: Default::default(), component: 0 }
ModelBody { name, body, feature, mesh: OnceLock::new(), key: 0, names: Default::default(), component: 0, imported: None }
}
/// Display mesh (chord tolerance 1/1000 of the body size), computed once.
pub fn mesh(&self) -> Arc<Mesh> {
@@ -236,6 +238,13 @@ impl ModelState {
(key, &b.name).hash(&mut h);
b.key = h.finish();
b.names = crate::naming::NameCell::new(Some(origin.clone()));
// An imported look outlives the operations on the body.
if b.body.paint().is_none()
&& b.body.color().is_none()
&& let Some(old) = before.bodies.iter().find(|x| x.name == b.name)
{
b.imported = crate::appearance::ImportedLook::of(old);
}
}
}
}