From 937e557e5a2b1fe261d592b6c05b205fb54692d3 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:37:06 -0400 Subject: [PATCH] mesh.mirror: carry each face tag to the image face its reflection covers The image's face names came from one sample per source face (the first element face's centre), so a source face whose sampled element face was off it, or a sliver, mapped wrong and the true image face went untagged. Each source face now maps to the image faces its own reflection covers, and each element face to the nearest of those. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/engine/src/cmd/mirror.rs | 99 ++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 38 deletions(-) diff --git a/crates/engine/src/cmd/mirror.rs b/crates/engine/src/cmd/mirror.rs index be109ee..67568ed 100644 --- a/crates/engine/src/cmd/mirror.rs +++ b/crates/engine/src/cmd/mirror.rs @@ -9,8 +9,10 @@ use crate::schema::ProblemKind; use std::collections::{BTreeMap, BTreeSet}; -use fieldcraft_geometry::Shape; +use fieldcraft_geometry::{Design, Shape}; use fieldcraft_mesh::mirror::{mirror, reflected_edges, reflected_faces}; +use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions}; +use fieldcraft_mesh::tet::geometry::Tessellation; use fieldcraft_model::geometry::Entity; use fieldcraft_model::{ElementFace, GeometryRef, Model}; use serde_json::{Value, json}; @@ -52,6 +54,11 @@ fn plane(s: &Session, p: &Value) -> Result<([f64; 3], [f64; 3])> { } } +/// The distance between two points. +fn dist(a: [f64; 3], b: [f64; 3]) -> f64 { + (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::().sqrt() +} + fn reflect(x: [f64; 3], point: [f64; 3], n: [f64; 3]) -> [f64; 3] { let d: f64 = (0..3).map(|i| (x[i] - point[i]) * n[i]).sum(); [x[0] - 2.0 * d * n[0], x[1] - 2.0 * d * n[1], x[2] - 2.0 * d * n[2]] @@ -165,7 +172,7 @@ fn mirror_mesh(s: &mut Session, p: &Value) -> Result { let cleared = super::replace::clear(model, image.values().map(|(name, _)| name.as_str())); let before = model.clone(); let r = mirror(model, ®ion, point, n, ON_PLANE * diagonal).map_err(|e| EngineError::Params(e.to_string()))?; - tag_image(model, &before, &r, &image, point, n, diagonal)?; + tag_image(model, &before, &r, &image, design.as_deref(), point, n, diagonal)?; let added_nodes = model.nodes().len() - before.nodes().len(); let images: Vec = image.iter().map(|(b, (t, shape))| json!({"body": b, "image": t, "in_design": shape.is_some()})).collect(); let reply = json!({ @@ -185,50 +192,66 @@ fn tag_image( before: &Model, r: &fieldcraft_mesh::mirror::Mirrored, image: &BTreeMap)>, + design: Option<&Design>, point: [f64; 3], n: [f64; 3], diagonal: f64, ) -> Result<()> { let c = before.classification(); let entity = |t: u32| c.entity(t).cloned(); - // A source face's image: by the image body's face under its reflected centroid, one lookup per - // face name (the first element face of it stands for all). - let mut face_points: BTreeMap<(String, String), [f64; 3]> = BTreeMap::new(); - for (&e, tags) in c.elements() { - if !r.elements.contains_key(&e) { - continue; - } - for (&f, &t) in &tags.faces { - let Some(GeometryRef { body, entity: Entity::Face(name) }) = entity(t) else { continue }; - if face_points.contains_key(&(body.clone(), name.clone())) { - continue; + // Each image body's faces, to find the one each reflected boundary face lies on. A source + // face's image is the set of image faces its own reflection covers (sampled at its + // triangles' centres: one face for an exact mirror, pieces where the image is split + // differently); each element face goes to the one of them nearest its reflected centre. So + // an element face the mesher gave a face narrower than the elements (its centre on a + // neighbour) keeps that face's image. + let tolerance = (1e-3 * diagonal).max(1e-4); + let mut targets: BTreeMap<&str, (Tessellation, Vec)> = BTreeMap::new(); + for (target, shape) in image.values() { + let Some(shape) = shape else { continue }; + let mesh = shape.tessellate(tolerance)?; + let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() }; + let surface = BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options) + .map_err(|e| EngineError::Params(format!("body `{target}`: {e}")))?; + let names = shape.faces().iter().map(|f| f.name.clone()).collect(); + targets.insert(target.as_str(), (Tessellation::new(&surface, Vec::new()), names)); + } + // The image faces each source face's reflection covers, by (source body, face name). + let mut covers: BTreeMap<(String, String), Vec> = BTreeMap::new(); + for (body, (target, _)) in image { + let (Some(shape), Some((tess, _))) = (design.and_then(|d| d.body(body)), targets.get(target.as_str())) else { continue }; + let mesh = shape.tessellate(tolerance)?; + let at = |v: u32| mesh.positions.get(v as usize).copied().unwrap_or_default(); + for (t, &f) in mesh.triangles.iter().zip(&mesh.tri_face) { + let Some(name) = shape.faces().get(f as usize).map(|x| x.name.clone()) else { continue }; + let centre = [0, 1, 2].map(|k| (at(t[0])[k] + at(t[1])[k] + at(t[2])[k]) / 3.0); + let Some(hit) = tess.nearest_face(reflect(centre, point, n)) else { continue }; + let list = covers.entry((body.clone(), name)).or_default(); + if !list.contains(&hit) { + list.push(hit); } - let Some(nodes) = before.face_nodes(ElementFace { element: e, face: f }) else { continue }; + } + } + let image_face = |e: u32, f: u8, body: &str, name: &str| -> Option<(String, String)> { + let (target, _) = image.get(body)?; + let mapped = targets.get(target.as_str()).and_then(|(tess, names)| { + let nodes = before.face_nodes(ElementFace { element: e, face: f })?; let pts: Vec<[f64; 3]> = nodes.iter().filter_map(|n| before.node(*n).map(|x| x.xyz)).collect(); let k = pts.len().max(1) as f64; - let centre = [0, 1, 2].map(|i| pts.iter().map(|p| p[i]).sum::() / k); - face_points.insert((body, name), reflect(centre, point, n)); - } - } - let mut face_names: BTreeMap<(String, String), (String, String)> = BTreeMap::new(); - let mut by_body: BTreeMap> = BTreeMap::new(); - for ((body, name), p) in &face_points { - by_body.entry(body.clone()).or_default().push((name.clone(), *p)); - } - for (body, faces) in by_body { - let Some((target, shape)) = image.get(&body) else { continue }; - let found = match shape { - Some(shape) => { - let pts: Vec<[f64; 3]> = faces.iter().map(|(_, p)| *p).collect(); - shape.nearest_faces(&pts, (1e-3 * diagonal).max(1e-4))? - } - None => vec![None; faces.len()], - }; - for ((name, _), f) in faces.iter().zip(found) { - let mapped = f.and_then(|f| shape.and_then(|s| s.faces().get(f as usize))).map_or_else(|| name.clone(), |x| x.name.clone()); - face_names.insert((body.clone(), name.clone()), (target.clone(), mapped)); - } - } + let p = reflect([0, 1, 2].map(|i| pts.iter().map(|p| p[i]).sum::() / k), point, n); + let face = match covers.get(&(body.to_string(), name.to_string())).map(Vec::as_slice) { + Some([only]) => Some(*only), + Some(several @ [_, _, ..]) => several + .iter() + .filter_map(|&g| tess.project_trimmed(g, p).map(|q| (dist(p, q), g))) + .min_by(|a, b| a.0.total_cmp(&b.0)) + .map(|(_, g)| g), + _ => tess.nearest_face(p), + }; + face.and_then(|i| names.get(i as usize)).cloned() + }); + Some((target.clone(), mapped.unwrap_or_else(|| name.to_string()))) + }; let mut tags: BTreeMap = BTreeMap::new(); let mut tag = |model: &mut Model, r: GeometryRef| -> Result { if let Some(&t) = tags.get(&r) { @@ -251,7 +274,7 @@ fn tag_image( } for (&f, &ft) in &t.faces { let Some(GeometryRef { body, entity: Entity::Face(name) }) = entity(ft) else { continue }; - let Some((tb, tn)) = face_names.get(&(body, name)) else { continue }; + let Some((tb, tn)) = image_face(e, f, &body, &name) else { continue }; let g = faces.get(usize::from(f).saturating_sub(1)).copied().unwrap_or(f); let tg = tag(model, GeometryRef::face(tb.as_str(), tn.as_str())?)?; model.tag_element_face(copy, g, tg)?;