use super::design_mesh::{DesignMesh, MeshMiss, MeshSource, simplified_flags, simplify_ring};
use crate::gui::rough_plan::{
format::{group_order, piece_positions},
metrics::PALETTE,
shape_worker::centred_mesh,
};
use glam::{DVec3, Vec3};
use indicatrix::geometry::stone_metrics::SolidMesh;
use indicatrix_cut_core::rough_plan::{PlacedStone, RoughBase, RoughLayout, RoughModel, StonePose};
use std::{
collections::BTreeMap,
sync::{
Arc, OnceLock,
atomic::{AtomicU64, Ordering},
},
};
pub(super) const BASE_GREY: [u8; 3] = [200, 205, 215];
pub(super) const CUT_TINT: [u8; 3] = [230, 190, 120];
pub(super) const DELETED_GREY: [u8; 3] = [120, 126, 138];
pub(super) const UNREADABLE_TINT: [u8; 3] = [150, 110, 118];
const NOTE_DELETED: &str = "design deleted";
const NOTE_UNREADABLE: &str = "design could not be loaded";
const NOTE_CHANGED: &str = "design changed since saved";
const NOTE_MATCHED: &str = "matched by title";
const NOTE_SCALED: &str = ", drawn at the saved width";
pub(super) const SCENE_RADIUS: f64 = 1.0;
static NEXT_SERIAL: AtomicU64 = AtomicU64::new(1);
fn next_serial() -> u64 {
NEXT_SERIAL.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct WorldFrame {
pub(super) centre: DVec3,
pub(super) scale: f64,
}
impl WorldFrame {
#[must_use]
pub(super) fn for_base(base: &RoughBase) -> Self {
let half_diagonal = DVec3::from_array(base.bounding_box_extents()).length() * 0.5;
Self {
centre: base.bounding_box_centre(),
scale: if half_diagonal > 0.0 {
1.0 / half_diagonal
} else {
1.0
},
}
}
#[must_use]
pub(super) fn point(&self, mm: DVec3) -> DVec3 {
(mm - self.centre) * self.scale
}
}
fn push_facet(mesh: &mut SolidMesh, id: usize, normal: DVec3, ring: Vec<DVec3>) {
mesh.facet_id.push(id);
mesh.normals.push(normal);
mesh.rings.push((id, ring));
}
fn set_drawn_edges(mesh: &mut SolidMesh, drawn: Option<&[bool]>) {
let Some(last) = mesh.rings.len().checked_sub(1) else {
return;
};
if drawn.is_none() && mesh.edge_visible.is_none() {
return;
}
let visible = mesh.edge_visible.get_or_insert_with(Vec::new);
while visible.len() < last {
let len = mesh.rings[visible.len()].1.len();
visible.push(vec![true; len]);
}
visible.push(drawn.map_or_else(|| vec![true; mesh.rings[last].1.len()], <[bool]>::to_vec));
}
#[must_use]
fn world_mesh(centred_mm: &SolidMesh, scale: f64) -> SolidMesh {
let mut normals: BTreeMap<usize, DVec3> = BTreeMap::new();
for (&id, &normal) in centred_mm.facet_id.iter().zip(¢red_mm.normals) {
normals.entry(id).or_insert(normal);
}
let mut mesh = SolidMesh::default();
for (index, (id, ring)) in centred_mm.rings.iter().enumerate() {
let normal = centred_mm.piece_normals.as_ref().map_or_else(
|| normals.get(id).copied(),
|pieces| pieces.get(index).copied(),
);
let Some(normal) = normal else {
continue;
};
let scaled: Vec<DVec3> = ring.iter().map(|&p| p * scale).collect();
let corners = simplify_ring(&scaled);
if corners.len() < 3 {
continue;
}
let drawn = centred_mm.edge_visible.as_ref().map(|visible| {
simplified_flags(
&scaled,
&corners,
visible.get(index).map_or(&[], Vec::as_slice),
)
});
push_facet(&mut mesh, *id, normal, corners);
if centred_mm.piece_normals.is_some() {
mesh.piece_normals.get_or_insert_with(Vec::new).push(normal);
}
set_drawn_edges(&mut mesh, drawn.as_deref());
}
mesh
}
#[must_use]
pub(super) fn box_faces(min: DVec3, max: DVec3) -> [(DVec3, [DVec3; 4]); 6] {
let corner = |x: bool, y: bool, z: bool| {
DVec3::new(
if x { max.x } else { min.x },
if y { max.y } else { min.y },
if z { max.z } else { min.z },
)
};
[
(
DVec3::X,
[
corner(true, false, false),
corner(true, true, false),
corner(true, true, true),
corner(true, false, true),
],
),
(
DVec3::NEG_X,
[
corner(false, false, false),
corner(false, false, true),
corner(false, true, true),
corner(false, true, false),
],
),
(
DVec3::Y,
[
corner(false, true, false),
corner(false, true, true),
corner(true, true, true),
corner(true, true, false),
],
),
(
DVec3::NEG_Y,
[
corner(false, false, false),
corner(true, false, false),
corner(true, false, true),
corner(false, false, true),
],
),
(
DVec3::Z,
[
corner(false, false, true),
corner(true, false, true),
corner(true, true, true),
corner(false, true, true),
],
),
(
DVec3::NEG_Z,
[
corner(false, false, false),
corner(false, true, false),
corner(true, true, false),
corner(true, false, false),
],
),
]
}
#[derive(Debug)]
pub(super) struct ModelScene {
pub(super) mesh: SolidMesh,
pub(super) base_facets: usize,
pub(super) cut_count: usize,
pub(super) half_extents: [f32; 3],
pub(super) block: bool,
pub(super) cut_planes: Vec<(Vec3, f32)>,
}
fn cut_planes_world(
model: &RoughModel,
base_facets: usize,
frame: &WorldFrame,
) -> Vec<(Vec3, f32)> {
let Ok(planes) = model.halfspaces() else {
return Vec::new();
};
planes
.into_iter()
.skip(base_facets)
.map(|(normal, offset)| {
(
normal.as_vec3(),
((offset - normal.dot(frame.centre)) * frame.scale) as f32,
)
})
.collect()
}
impl ModelScene {
#[must_use]
pub(super) fn new(centred_mm: &SolidMesh, model: &RoughModel) -> Self {
let frame = WorldFrame::for_base(&model.base);
let extents = model.base.bounding_box_extents();
let base_facets = model
.base
.to_halfspaces(false)
.map_or(0, |planes| planes.len());
Self {
mesh: world_mesh(centred_mm, frame.scale),
base_facets,
cut_count: model.cuts.len(),
half_extents: extents.map(|e| (e * frame.scale * 0.5) as f32),
block: matches!(model.base, RoughBase::Block { .. }),
cut_planes: cut_planes_world(model, base_facets, &frame),
}
}
#[must_use]
pub(super) fn cut_of_facet(&self, facet: usize) -> Option<usize> {
let cut = facet.checked_sub(self.base_facets)?;
(cut < self.cut_count).then_some(cut)
}
#[must_use]
pub(super) fn facet_normal(&self, facet: usize) -> Option<DVec3> {
let at = self.mesh.facet_id.iter().position(|&id| id == facet)?;
self.mesh.normals.get(at).copied()
}
#[must_use]
pub(super) fn facet_colors(&self, tint_cuts: bool) -> Vec<[u8; 3]> {
(0..self.base_facets + self.cut_count)
.map(|id| {
if tint_cuts && id >= self.base_facets {
CUT_TINT
} else {
BASE_GREY
}
})
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PiecePosition {
pub(super) slab: usize,
pub(super) bar: usize,
pub(super) piece: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub(super) struct StoneInfo {
pub(super) title: String,
pub(super) carat: f64,
pub(super) position: Option<PiecePosition>,
pub(super) note: String,
}
impl StoneInfo {
#[must_use]
pub(super) fn hint(&self) -> String {
let mut parts = vec![format!("{} \u{00B7} {:.2} ct", self.title, self.carat)];
if let Some(position) = self.position {
parts.push(format!(
"slab {}, bar {}, piece {}",
position.slab, position.bar, position.piece
));
}
if !self.note.is_empty() {
parts.push(self.note.clone());
}
parts.join(" \u{00B7} ")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum StoneDraw {
Design(i64),
Changed(i64),
Matched(i64),
Deleted,
}
pub(super) struct RoughMesh {
model: RoughModel,
mesh: OnceLock<Arc<SolidMesh>>,
}
impl RoughMesh {
#[must_use]
pub(super) const fn new(model: RoughModel) -> Self {
Self {
model,
mesh: OnceLock::new(),
}
}
#[must_use]
pub(super) const fn model(&self) -> &RoughModel {
&self.model
}
#[must_use]
pub(super) fn get(&self) -> Arc<SolidMesh> {
Arc::clone(self.mesh.get_or_init(|| {
let frame = WorldFrame::for_base(&self.model.base);
let mesh = centred_mesh(&self.model)
.map_or_else(|_| SolidMesh::default(), |c| world_mesh(&c, frame.scale));
Arc::new(mesh)
}))
}
}
#[derive(Debug, Default)]
pub(super) struct FitScene {
pub(super) stones: SolidMesh,
pub(super) facet_colors: Vec<[u8; 3]>,
pub(super) stone_starts: Vec<usize>,
pub(super) stone_group: Vec<usize>,
pub(super) info: Vec<StoneInfo>,
pub(super) rough: Arc<SolidMesh>,
pub(super) saw: SolidMesh,
}
impl FitScene {
#[must_use]
pub(super) fn stone_at_facet(&self, facet: usize) -> Option<usize> {
let total = *self.stone_starts.last()?;
if facet >= total {
return None;
}
Some(self.stone_starts.partition_point(|&start| start <= facet) - 1)
}
#[must_use]
pub(super) fn facets_of_group(&self, group: usize) -> Vec<bool> {
let mut flags = vec![false; self.facet_colors.len()];
for (stone, span) in self.stone_starts.windows(2).enumerate() {
if self.stone_group.get(stone) == Some(&group) {
flags[span[0]..span[1]].fill(true);
}
}
flags
}
fn push_design(
&mut self,
design: &DesignMesh,
pose: &StonePose,
frame: &WorldFrame,
color: [u8; 3],
) {
for facet in &design.facets {
let ring: Vec<DVec3> = facet
.ring
.iter()
.map(|&v| point_to_world(v, pose, frame))
.collect();
let id = self.facet_colors.len();
push_facet(
&mut self.stones,
id,
normal_to_world(facet.normal, pose),
ring,
);
set_drawn_edges(&mut self.stones, facet.edge_drawn.as_deref());
self.facet_colors.push(color);
}
}
fn push_placeholder(&mut self, stone: &PlacedStone, frame: &WorldFrame, color: [u8; 3]) {
let centre_mm = DVec3::from_array(stone.pose.center_mm);
let half = DVec3::from_array(stone.stone_size_mm) * 0.5;
for (normal, corners) in box_faces(centre_mm - half, centre_mm + half) {
let id = self.facet_colors.len();
push_facet(
&mut self.stones,
id,
normal,
corners.map(|c| frame.point(c)).to_vec(),
);
set_drawn_edges(&mut self.stones, None);
self.facet_colors.push(color);
}
}
fn push_stone(
&mut self,
stone: &PlacedStone,
draw: StoneDraw,
meshes: &dyn MeshSource,
frame: &WorldFrame,
color: [u8; 3],
) -> String {
let (entry_id, rescale, note) = match draw {
StoneDraw::Design(id) => (id, false, ""),
StoneDraw::Changed(id) => (id, true, NOTE_CHANGED),
StoneDraw::Matched(id) => (id, true, NOTE_MATCHED),
StoneDraw::Deleted => {
self.push_placeholder(stone, frame, DELETED_GREY);
return NOTE_DELETED.to_string();
}
};
match meshes.mesh(entry_id) {
Ok(design) if rescale => {
let (pose, scaled) = rescaled_pose(stone, design.caliper_width());
self.push_design(&design, &pose, frame, color);
if scaled {
format!("{note}{NOTE_SCALED}")
} else {
note.to_string()
}
}
Ok(design) => {
self.push_design(&design, &stone.pose, frame, color);
note.to_string()
}
Err(MeshMiss::Gone) => {
self.push_placeholder(stone, frame, DELETED_GREY);
NOTE_DELETED.to_string()
}
Err(MeshMiss::Unreadable) => {
self.push_placeholder(stone, frame, UNREADABLE_TINT);
NOTE_UNREADABLE.to_string()
}
}
}
}
fn recorded_width_mm(stone: &PlacedStone) -> Option<f64> {
let direction = stone.pose.axes[0];
let index = (0..3).find(|&i| {
direction[i].abs() > 0.5
&& direction
.iter()
.enumerate()
.all(|(j, component)| j == i || component.abs() < 1e-6)
})?;
let width = stone.stone_size_mm[index];
(width.is_finite() && width > 0.0).then_some(width)
}
fn rescaled_pose(stone: &PlacedStone, width_units: f64) -> (StonePose, bool) {
let mut pose = stone.pose;
match recorded_width_mm(stone) {
Some(width_mm) if width_units.is_finite() && width_units > 0.0 => {
pose.mm_per_unit = width_mm / width_units;
(pose, true)
}
_ => (pose, false),
}
}
#[must_use]
pub(super) fn point_to_world(v: DVec3, pose: &StonePose, frame: &WorldFrame) -> DVec3 {
let local = normal_to_world(v, pose);
frame.point(DVec3::from_array(pose.center_mm) + local * pose.mm_per_unit)
}
#[must_use]
pub(super) fn normal_to_world(n: DVec3, pose: &StonePose) -> DVec3 {
let [x, y, z] = pose.axes.map(DVec3::from_array);
x * n.x + y * n.y + z * n.z
}
pub(super) struct FitInputs<'a> {
pub(super) layout: &'a RoughLayout,
pub(super) rough: &'a RoughMesh,
pub(super) titles: &'a BTreeMap<i64, String>,
pub(super) mesh_ids: &'a BTreeMap<i64, StoneDraw>,
}
fn push_piece_box(saw: &mut SolidMesh, stone: &PlacedStone, frame: &WorldFrame) {
let origin = DVec3::from_array(stone.piece_origin_mm);
let size = DVec3::from_array(stone.piece_size_mm);
for (normal, corners) in box_faces(origin, origin + size) {
let id = saw.facet_id.len();
push_facet(saw, id, normal, corners.map(|c| frame.point(c)).to_vec());
}
}
#[must_use]
pub(super) fn build_fit_scene(inputs: &FitInputs<'_>, meshes: &dyn MeshSource) -> FitScene {
let frame = WorldFrame::for_base(&inputs.rough.model().base);
let layout = inputs.layout;
let groups: Vec<i64> = group_order(layout).into_iter().map(|(id, _)| id).collect();
let positions = piece_positions(layout);
let mut scene = FitScene {
rough: inputs.rough.get(),
..FitScene::default()
};
for (index, stone) in layout.stones.iter().enumerate() {
let group = groups
.iter()
.position(|&id| id == stone.entry_id)
.unwrap_or(0);
scene.stone_starts.push(scene.facet_colors.len());
scene.stone_group.push(group);
let draw = inputs
.mesh_ids
.get(&stone.entry_id)
.copied()
.unwrap_or(StoneDraw::Design(stone.entry_id));
let color = PALETTE[group % PALETTE.len()];
let note = scene.push_stone(stone, draw, meshes, &frame, color);
scene.info.push(StoneInfo {
title: inputs
.titles
.get(&stone.entry_id)
.cloned()
.unwrap_or_else(|| format!("Design #{}", stone.entry_id)),
carat: stone.carat,
position: positions
.get(index)
.map(|&(slab, bar, piece)| PiecePosition { slab, bar, piece }),
note,
});
push_piece_box(&mut scene.saw, stone, &frame);
}
scene.stone_starts.push(scene.facet_colors.len());
scene
}
#[derive(Debug)]
#[expect(
clippy::large_enum_variant,
reason = "a scene lives once inside an `Arc<Scene>` and is never moved by value in a loop; boxing `ModelScene` would touch every construction and match site"
)]
pub(super) enum SceneKind {
Model(ModelScene),
Fit(Box<FitScene>),
}
#[derive(Debug)]
pub(super) struct Scene {
pub(super) serial: u64,
pub(super) kind: SceneKind,
}
impl Scene {
#[must_use]
pub(super) fn new(kind: SceneKind) -> Self {
Self {
serial: next_serial(),
kind,
}
}
}
#[cfg(test)]
mod tests;