onedrop_engine/engine/
shape_phase.rs

1//! Custom-shape (`shapecode_N`) phase of the engine update loop.
2//!
3//! For every enabled `shapecode_N` block, runs the pre-compiled init (once
4//! per preset load) and per-frame blocks once per instance, packs the
5//! resulting state into a `CustomShapeInstance` storage buffer, and returns
6//! the instance + batch streams. The renderer expands each instance into
7//! a triangle fan on the GPU — one draw call per shape. Caller pushes the
8//! result to the correct chain (primary for `current`, secondary for
9//! `fading_out` during a preset transition).
10
11use super::preset_slot::{PresetSlot, run_block_with_logger};
12use onedrop_eval::ShapeInstance as EvalShapeInstance;
13use onedrop_parser::preset::ShapeCode;
14use onedrop_renderer::{CustomShapeBatch, CustomShapeInstance, ShapeFlags};
15
16pub(super) fn compute_custom_shapes(
17    slot: &mut PresetSlot,
18) -> (Vec<CustomShapeInstance>, Vec<CustomShapeBatch>) {
19    let Some(preset) = slot.preset.as_ref() else {
20        return (Vec::new(), Vec::new());
21    };
22    if preset.shapes.is_empty()
23        || preset.shapes.iter().all(|s| !s.enabled)
24        || slot.compiled_shapes.is_empty()
25    {
26        return (Vec::new(), Vec::new());
27    }
28
29    let mut instances: Vec<CustomShapeInstance> = Vec::with_capacity(4 * 32);
30    let mut batches: Vec<CustomShapeBatch> = Vec::with_capacity(4);
31
32    // Split-borrow the slot into disjoint fields so we can iterate
33    // `&preset.shapes` while mutating `evaluator` and
34    // `shapes_need_init` inside the loop. Skips the previous
35    // `preset.shapes.clone()` that was a borrow-checker workaround
36    // (each `ShapeCode` carries `Vec<String>` equation lists).
37    let PresetSlot {
38        compiled_shapes,
39        shapes_need_init,
40        evaluator,
41        ..
42    } = slot;
43    let shapes: &[ShapeCode] = &preset.shapes;
44
45    for (i, shape) in shapes.iter().enumerate() {
46        if !shape.enabled || i >= compiled_shapes.len() {
47            continue;
48        }
49        let cs = &compiled_shapes[i];
50
51        if shapes_need_init.get(i).copied().unwrap_or(false) {
52            run_block_with_logger(evaluator, &cs.init, |idx, e| {
53                log::warn!("shape[{}] init equation[{}] failed: {}", i, idx, e);
54            });
55            shapes_need_init[i] = false;
56        }
57
58        let num_inst = shape.num_inst.clamp(1, 1024) as u32;
59        let sides = shape.sides.clamp(3, 64) as u32;
60        let start = instances.len() as u32;
61
62        for inst_idx in 0..num_inst {
63            let seed = EvalShapeInstance {
64                instance: inst_idx as f64,
65                num_inst: num_inst as f64,
66                sides: shape.sides as f64,
67                x: shape.x as f64,
68                y: shape.y as f64,
69                rad: shape.rad as f64,
70                ang: shape.ang as f64,
71                tex_zoom: shape.tex_zoom as f64,
72                tex_ang: shape.tex_ang as f64,
73                r: shape.r as f64,
74                g: shape.g as f64,
75                b: shape.b as f64,
76                a: shape.a as f64,
77                r2: shape.r2 as f64,
78                g2: shape.g2 as f64,
79                b2: shape.b2 as f64,
80                a2: shape.a2 as f64,
81                border_r: shape.border_r as f64,
82                border_g: shape.border_g as f64,
83                border_b: shape.border_b as f64,
84                border_a: shape.border_a as f64,
85                thick: if shape.thick_outline { 1.0 } else { 0.0 },
86                additive: if shape.additive { 1.0 } else { 0.0 },
87            };
88            let out = if cs.per_frame.is_empty() {
89                seed
90            } else {
91                match evaluator.run_per_shape_instance(seed, &cs.per_frame) {
92                    Ok(o) => o,
93                    Err(e) => {
94                        log::warn!("shape[{}] per_frame[{}] failed: {}", i, inst_idx, e);
95                        seed
96                    }
97                }
98            };
99
100            let textured = shape.textured;
101            let mut flags = ShapeFlags::empty();
102            if textured {
103                flags |= ShapeFlags::TEXTURED;
104            }
105            if out.additive > 0.5 {
106                flags |= ShapeFlags::ADDITIVE;
107            }
108            if out.thick > 0.5 {
109                flags |= ShapeFlags::THICK_OUTLINE;
110            }
111
112            let color_center = [
113                out.r.clamp(0.0, 1.0) as f32,
114                out.g.clamp(0.0, 1.0) as f32,
115                out.b.clamp(0.0, 1.0) as f32,
116                out.a.clamp(0.0, 1.0) as f32,
117            ];
118            let color_edge = [
119                out.r2.clamp(0.0, 1.0) as f32,
120                out.g2.clamp(0.0, 1.0) as f32,
121                out.b2.clamp(0.0, 1.0) as f32,
122                out.a2.clamp(0.0, 1.0) as f32,
123            ];
124            let border_color = [
125                out.border_r.clamp(0.0, 1.0) as f32,
126                out.border_g.clamp(0.0, 1.0) as f32,
127                out.border_b.clamp(0.0, 1.0) as f32,
128                out.border_a.clamp(0.0, 1.0) as f32,
129            ];
130
131            instances.push(onedrop_renderer::custom_shape::instance_from_md2_state(
132                out.x,
133                out.y,
134                out.rad,
135                out.ang,
136                out.tex_zoom,
137                out.tex_ang,
138                sides,
139                color_center,
140                color_edge,
141                border_color,
142                0.0,
143                flags,
144            ));
145        }
146
147        let count = instances.len() as u32 - start;
148        if count > 0 {
149            batches.push(CustomShapeBatch {
150                start_instance: start,
151                instance_count: count,
152                sides,
153                additive: shape.additive,
154                thick_outline: shape.thick_outline,
155            });
156        }
157    }
158
159    (instances, batches)
160}