Skip to main content

oxihuman_morph/
sparse_blend_shape.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Sparse (delta-only) blend shape stub.
6
7/// A sparse delta entry: only stores non-zero vertex deltas.
8#[derive(Debug, Clone)]
9pub struct SparseDelta {
10    pub vertex_index: u32,
11    pub delta: [f32; 3],
12}
13
14/// Sparse blend shape storing only affected vertices.
15#[derive(Debug, Clone)]
16pub struct SparseBlendShape {
17    pub name: String,
18    pub deltas: Vec<SparseDelta>,
19    pub total_vertex_count: usize,
20    pub weight: f32,
21    pub enabled: bool,
22}
23
24impl SparseBlendShape {
25    pub fn new(name: impl Into<String>, total_vertex_count: usize) -> Self {
26        SparseBlendShape {
27            name: name.into(),
28            deltas: Vec::new(),
29            total_vertex_count,
30            weight: 0.0,
31            enabled: true,
32        }
33    }
34}
35
36/// Create a new sparse blend shape.
37pub fn new_sparse_blend_shape(
38    name: impl Into<String>,
39    total_vertex_count: usize,
40) -> SparseBlendShape {
41    SparseBlendShape::new(name, total_vertex_count)
42}
43
44/// Add a delta entry.
45pub fn sbs_add_delta(shape: &mut SparseBlendShape, delta: SparseDelta) {
46    shape.deltas.push(delta);
47}
48
49/// Apply the sparse blend shape to a position buffer (stub: no-op).
50pub fn sbs_apply(shape: &SparseBlendShape, positions: &mut [[f32; 3]]) {
51    /* Stub: applies weighted deltas; currently no-op */
52    for d in &shape.deltas {
53        let idx = d.vertex_index as usize;
54        if idx < positions.len() {
55            positions[idx][0] += d.delta[0] * shape.weight;
56            positions[idx][1] += d.delta[1] * shape.weight;
57            positions[idx][2] += d.delta[2] * shape.weight;
58        }
59    }
60}
61
62/// Return delta count.
63pub fn sbs_delta_count(shape: &SparseBlendShape) -> usize {
64    shape.deltas.len()
65}
66
67/// Set shape weight.
68pub fn sbs_set_weight(shape: &mut SparseBlendShape, weight: f32) {
69    shape.weight = weight.clamp(0.0, 1.0);
70}
71
72/// Enable or disable.
73pub fn sbs_set_enabled(shape: &mut SparseBlendShape, enabled: bool) {
74    shape.enabled = enabled;
75}
76
77/// Serialize to JSON-like string.
78pub fn sbs_to_json(shape: &SparseBlendShape) -> String {
79    format!(
80        r#"{{"name":"{}","delta_count":{},"weight":{},"enabled":{}}}"#,
81        shape.name,
82        shape.deltas.len(),
83        shape.weight,
84        shape.enabled
85    )
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_new_no_deltas() {
94        let s = new_sparse_blend_shape("brow_up", 500);
95        assert_eq!(sbs_delta_count(&s), 0 /* no deltas initially */,);
96    }
97
98    #[test]
99    fn test_add_delta() {
100        let mut s = new_sparse_blend_shape("brow_up", 500);
101        sbs_add_delta(
102            &mut s,
103            SparseDelta {
104                vertex_index: 10,
105                delta: [0.0, 0.1, 0.0],
106            },
107        );
108        assert_eq!(sbs_delta_count(&s), 1 /* one delta after add */,);
109    }
110
111    #[test]
112    fn test_apply_with_weight() {
113        let mut s = new_sparse_blend_shape("lift", 4);
114        sbs_add_delta(
115            &mut s,
116            SparseDelta {
117                vertex_index: 0,
118                delta: [0.0, 1.0, 0.0],
119            },
120        );
121        sbs_set_weight(&mut s, 0.5);
122        let mut positions = vec![[0.0; 3]; 4];
123        sbs_apply(&s, &mut positions);
124        assert!((positions[0][1] - 0.5).abs() < 1e-5, /* Y must be displaced by weight */);
125    }
126
127    #[test]
128    fn test_weight_clamped() {
129        let mut s = new_sparse_blend_shape("x", 2);
130        sbs_set_weight(&mut s, 2.0);
131        assert!((s.weight - 1.0).abs() < 1e-6, /* weight must be clamped to 1.0 */);
132    }
133
134    #[test]
135    fn test_weight_clamped_negative() {
136        let mut s = new_sparse_blend_shape("x", 2);
137        sbs_set_weight(&mut s, -0.5);
138        assert!((s.weight).abs() < 1e-6, /* weight must be clamped to 0.0 */);
139    }
140
141    #[test]
142    fn test_set_enabled() {
143        let mut s = new_sparse_blend_shape("x", 2);
144        sbs_set_enabled(&mut s, false);
145        assert!(!s.enabled /* enabled must be false */,);
146    }
147
148    #[test]
149    fn test_to_json_contains_name() {
150        let s = new_sparse_blend_shape("smile", 10);
151        let j = sbs_to_json(&s);
152        assert!(j.contains("smile") /* json must contain shape name */,);
153    }
154
155    #[test]
156    fn test_apply_out_of_bounds_ignored() {
157        let mut s = new_sparse_blend_shape("x", 2);
158        sbs_add_delta(
159            &mut s,
160            SparseDelta {
161                vertex_index: 999,
162                delta: [1.0, 1.0, 1.0],
163            },
164        );
165        sbs_set_weight(&mut s, 1.0);
166        let mut positions = vec![[0.0; 3]; 2];
167        sbs_apply(&s, &mut positions);
168        assert!((positions[0][0]).abs() < 1e-6, /* out-of-bounds delta must be ignored */);
169    }
170
171    #[test]
172    fn test_total_vertex_count() {
173        let s = new_sparse_blend_shape("x", 128);
174        assert_eq!(
175            s.total_vertex_count,
176            128, /* total vertex count must match */
177        );
178    }
179
180    #[test]
181    fn test_enabled_default() {
182        let s = new_sparse_blend_shape("x", 2);
183        assert!(s.enabled /* must be enabled by default */,);
184    }
185}