use axiolid_core::{Point3, Scalar};
use axiolid_mesh::{AttributeChannel, AttributeFate, Blend, DropReason, TriMesh};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FaceSource {
pub operand: u8,
pub triangle: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Sample {
Copied,
Derived,
Unmapped,
}
fn barycentric(p: Point3, a: Point3, b: Point3, c: Point3) -> Option<[Scalar; 3]> {
let n = (b - a).cross(c - a);
let (ax, ay, az) = (n.x.abs(), n.y.abs(), n.z.abs());
let proj = |q: Point3| {
if ax >= ay && ax >= az {
(q.y, q.z)
} else if ay >= az {
(q.z, q.x)
} else {
(q.x, q.y)
}
};
let (p, a, b, c) = (proj(p), proj(a), proj(b), proj(c));
let det = (b.0 - a.0) * (c.1 - a.1) - (c.0 - a.0) * (b.1 - a.1);
if det == 0.0 || !det.is_finite() {
return None;
}
let wb = ((p.0 - a.0) * (c.1 - a.1) - (c.0 - a.0) * (p.1 - a.1)) / det;
let wc = ((b.0 - a.0) * (p.1 - a.1) - (p.0 - a.0) * (b.1 - a.1)) / det;
Some([1.0 - wb - wc, wb, wc])
}
struct Located {
triangle: usize,
weights: [Scalar; 3],
}
const INSIDE: Scalar = 1e-9;
struct Adjacency {
edges: HashMap<(PosKey, PosKey), Vec<usize>>,
}
type PosKey = [u64; 3];
fn pos_key(p: Point3) -> PosKey {
[p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
}
impl Adjacency {
fn new(mesh: &TriMesh) -> Self {
let mut edges: HashMap<(PosKey, PosKey), Vec<usize>> = HashMap::new();
for (t, tri) in mesh.indices.chunks_exact(3).enumerate() {
for k in 0..3 {
let a = pos_key(mesh.positions[tri[k] as usize]);
let b = pos_key(mesh.positions[tri[(k + 1) % 3] as usize]);
edges.entry((a.min(b), a.max(b))).or_default().push(t);
}
}
Self { edges }
}
fn neighbours<'a>(&'a self, mesh: &'a TriMesh, t: usize) -> impl Iterator<Item = usize> + 'a {
let tri = &mesh.indices[t * 3..t * 3 + 3];
(0..3).flat_map(move |k| {
let a = pos_key(mesh.positions[tri[k] as usize]);
let b = pos_key(mesh.positions[tri[(k + 1) % 3] as usize]);
self.edges
.get(&(a.min(b), a.max(b)))
.into_iter()
.flatten()
.copied()
})
}
}
fn coplanar(mesh: &TriMesh, a: usize, b: usize) -> bool {
let n = |t: usize| {
let q = |k: usize| mesh.positions[mesh.indices[t * 3 + k] as usize];
(q(1) - q(0)).cross(q(2) - q(0)).normalize_or_zero()
};
n(a).dot(n(b)) > 1.0 - 1e-9
}
fn weights_in(mesh: &TriMesh, t: usize, p: Point3) -> Option<[Scalar; 3]> {
let q = |k: usize| mesh.positions[mesh.indices[t * 3 + k] as usize];
barycentric(p, q(0), q(1), q(2))
}
fn inside(w: &[Scalar; 3]) -> bool {
w.iter().all(|&x| x >= -INSIDE)
}
fn locate(
mesh: &TriMesh,
adjacency: &mut Option<Adjacency>,
recorded: usize,
p: Point3,
probe: Point3,
) -> Option<Located> {
let hit = |t: usize| {
let w = weights_in(mesh, t, probe)?;
inside(&w).then(|| weights_in(mesh, t, p)).flatten()
};
if let Some(weights) = hit(recorded) {
return Some(Located {
triangle: recorded,
weights,
});
}
let adjacency = adjacency.get_or_insert_with(|| Adjacency::new(mesh));
let mut seen = HashSet::from([recorded]);
let mut queue = VecDeque::from([recorded]);
while let Some(t) = queue.pop_front() {
for n in adjacency.neighbours(mesh, t) {
if !seen.insert(n) || !coplanar(mesh, recorded, n) {
continue;
}
if let Some(weights) = hit(n) {
return Some(Located {
triangle: n,
weights,
});
}
queue.push_back(n);
}
}
weights_in(mesh, recorded, p).map(|weights| Located {
triangle: recorded,
weights,
})
}
fn sample(
source: &TriMesh,
channel: &AttributeChannel,
at: &Located,
p: Point3,
out: &mut Vec<Scalar>,
) -> Sample {
let base = at.triangle * 3;
let Some(corners) = source.indices.get(base..base + 3) else {
return Sample::Unmapped;
};
let value = |k: usize| channel.at_corner(&source.indices, base + k);
let (Some(v0), Some(v1), Some(v2)) = (value(0), value(1), value(2)) else {
return Sample::Unmapped;
};
let pos = |k: usize| source.positions[corners[k] as usize];
let values = [v0, v1, v2];
if let Some(k) = (0..3).find(|&k| pos(k) == p) {
out.extend_from_slice(values[k]);
return Sample::Copied;
}
let w = at.weights;
match channel.blend {
Blend::Linear => {
for d in 0..channel.width {
out.push(w[0] * values[0][d] + w[1] * values[1][d] + w[2] * values[2][d]);
}
}
Blend::Nearest => {
let k = (0..3).max_by(|&i, &j| w[i].total_cmp(&w[j])).unwrap_or(0);
out.extend_from_slice(values[k]);
}
Blend::None => {}
}
Sample::Derived
}
pub(crate) fn carry(
subject: &TriMesh,
tool: &TriMesh,
result: &mut TriMesh,
sources: &[FaceSource],
) -> Vec<(String, AttributeFate)> {
let mut fates = Vec::with_capacity(subject.attributes.len());
for channel in &subject.attributes {
let tool_channel = tool.attributes.iter().find(|c| {
c.name == channel.name && c.width == channel.width && c.blend == channel.blend
});
let operands = [(subject, Some(channel)), (tool, tool_channel)];
match build(operands, channel, result, sources) {
Ok((built, derived)) => {
result.attributes.push(built);
let fate = if derived {
AttributeFate::Interpolated
} else {
AttributeFate::Preserved
};
fates.push((channel.name.clone(), fate));
}
Err(reason) => fates.push((channel.name.clone(), AttributeFate::Dropped(reason))),
}
}
fates
}
fn build(
operands: [(&TriMesh, Option<&AttributeChannel>); 2],
channel: &AttributeChannel,
result: &TriMesh,
sources: &[FaceSource],
) -> Result<(AttributeChannel, bool), DropReason> {
if sources.len() * 3 != result.indices.len() {
return Err(DropReason::ProviderLimitation);
}
let mut adjacency: [Option<Adjacency>; 2] = [None, None];
let mut values = Vec::with_capacity(result.indices.len() * channel.width);
let mut corners = Vec::with_capacity(result.indices.len());
let mut derived = false;
for (face, source) in sources.iter().enumerate() {
let side = usize::from(source.operand != 0);
let (mesh, from) = operands[side];
let tri = &result.indices[face * 3..face * 3 + 3];
let q = |k: usize| result.positions[tri[k] as usize];
let centroid = (q(0) + q(1) + q(2)) / 3.0;
for k in 0..3 {
let p = q(k);
let probe = p + (centroid - p) * PROBE_STEP;
let before = values.len();
let how = match from {
Some(from) => match locate(mesh, &mut adjacency[side], source.triangle, p, probe) {
Some(at) => sample(mesh, from, &at, p, &mut values),
None => Sample::Unmapped,
},
None => Sample::Unmapped,
};
match how {
Sample::Unmapped => corners.push(AttributeChannel::UNMAPPED),
Sample::Derived if channel.blend == Blend::None => {
return Err(DropReason::NotBlendable);
}
Sample::Copied | Sample::Derived => {
derived |= how == Sample::Derived;
corners.push((before / channel.width) as u32);
}
}
}
}
unmap_partial(&mut corners);
let built = AttributeChannel::corner_indexed(
channel.name.clone(),
values,
channel.width,
channel.blend,
corners,
);
Ok((built, derived))
}
fn unmap_partial(corners: &mut [u32]) {
for tri in corners.chunks_exact_mut(3) {
if tri.contains(&AttributeChannel::UNMAPPED) {
tri.fill(AttributeChannel::UNMAPPED);
}
}
}
const PROBE_STEP: Scalar = 1e-6;
pub(crate) fn sources_by_plane(
result: &TriMesh,
operands: &[(&TriMesh, Scalar)],
) -> Option<Vec<FaceSource>> {
let normal = |m: &TriMesh, t: usize| {
let q = |k: usize| m.positions[m.indices[t * 3 + k] as usize];
(q(1) - q(0)).cross(q(2) - q(0)).normalize_or_zero()
};
let mut out = Vec::with_capacity(result.triangle_count());
let d = result.bounds().diagonal();
let scale = d.x.max(d.y).max(d.z).max(Scalar::MIN_POSITIVE);
for f in 0..result.triangle_count() {
let q = |k: usize| result.positions[result.indices[f * 3 + k] as usize];
let centroid = (q(0) + q(1) + q(2)) / 3.0;
let n = normal(result, f);
let found = operands.iter().enumerate().find_map(|(i, (m, sign))| {
(0..m.triangle_count()).find_map(|t| {
let facing = normal(m, t).dot(n) * sign > 1.0 - 1e-9;
let a = m.positions[m.indices[t * 3] as usize];
let on_plane = (centroid - a).dot(normal(m, t)).abs() <= scale * 1e-9;
let w = (facing && on_plane)
.then(|| weights_in(m, t, centroid))
.flatten()?;
inside(&w).then_some(FaceSource {
operand: i as u8,
triangle: t,
})
})
})?;
out.push(found);
}
Some(out)
}