Skip to main content

tilegraph/
runtime.rs

1use rayon::prelude::*;
2use rustc_hash::FxHashSet;
3use std::sync::RwLock;
4use theframework::prelude::*;
5
6#[inline(always)]
7fn hash21(p: Vec2<f32>) -> f32 {
8    let mut p3 = Vec3::new(
9        (p.x * 0.1031).fract(),
10        (p.y * 0.1031).fract(),
11        (p.x * 0.1031).fract(),
12    );
13    let dot = p3.dot(Vec3::new(p3.y + 33.333, p3.z + 33.333, p3.x + 33.333));
14
15    p3.x += dot;
16    p3.y += dot;
17    p3.z += dot;
18
19    ((p3.x + p3.y) * p3.z).fract()
20}
21
22fn rot(a: f32) -> Mat2<f32> {
23    Mat2::new(a.cos(), -a.sin(), a.sin(), a.cos())
24}
25
26fn box_divide(
27    p: Vec2<f32>,
28    cell: Vec2<f32>,
29    gap: f32,
30    rotation: f32,
31    rounding: f32,
32    iterations: i32,
33) -> (f32, f32) {
34    fn s_box(p: Vec2<f32>, b: Vec2<f32>, r: f32) -> f32 {
35        let d = p.map(|v| v.abs()) - b + Vec2::new(r, r);
36        d.x.max(d.y).min(0.0) + (d.map(|v| v.max(0.0))).magnitude() - r
37    }
38
39    let mut p = p;
40
41    let mut l = Vec2::new(1.0, 1.0);
42    let mut last_l;
43    let mut r = hash21(cell);
44
45    for _ in 0..iterations.max(1) {
46        r = (l + Vec2::new(r, r)).dot(Vec2::new(123.71, 439.43)).fract() * 0.4 + 0.3;
47
48        last_l = l;
49        if l.x > l.y {
50            p = Vec2::new(p.y, p.x);
51            l = Vec2::new(l.y, l.x);
52        }
53
54        if p.x < r {
55            l.x /= r;
56            p.x /= r;
57        } else {
58            l.x /= 1.0 - r;
59            p.x = (p.x - r) / (1.0 - r);
60        }
61
62        if last_l.x > last_l.y {
63            p = Vec2::new(p.y, p.x);
64            l = Vec2::new(l.y, l.x);
65        }
66    }
67    p -= 0.5;
68
69    let id = hash21(cell + l);
70    p = rot((id - 0.5) * rotation) * p;
71
72    let th = l * 0.02 * gap;
73    let c = s_box(p, Vec2::new(0.5, 0.5) - th, rounding);
74
75    (c, id)
76}
77
78fn default_tile_node_nodes() -> Vec<TileNodeState> {
79    vec![TileNodeState {
80        kind: TileNodeKind::default_output_root(),
81        position: (420, 40),
82        preview_open: true,
83        bypass: false,
84        mute: false,
85        solo: false,
86    }]
87}
88
89fn default_voronoi_warp_amount() -> f32 {
90    0.0
91}
92
93fn default_voronoi_falloff() -> f32 {
94    1.0
95}
96
97fn default_layout_warp_amount() -> f32 {
98    0.0
99}
100
101fn default_layout_falloff() -> f32 {
102    1.0
103}
104
105fn default_height_shape_rim() -> f32 {
106    0.0
107}
108
109fn default_brick_staggered() -> bool {
110    true
111}
112
113fn default_colorize4_color_1() -> u16 {
114    0
115}
116
117fn default_colorize4_color_2() -> u16 {
118    1
119}
120
121fn default_colorize4_color_3() -> u16 {
122    2
123}
124
125fn default_colorize4_color_4() -> u16 {
126    3
127}
128
129fn default_colorize4_pixel_size() -> u16 {
130    1
131}
132
133fn default_colorize4_dither() -> bool {
134    false
135}
136
137fn default_colorize4_auto_range() -> bool {
138    true
139}
140
141fn default_particle_color_1() -> u16 {
142    0
143}
144
145fn default_particle_color_2() -> u16 {
146    1
147}
148
149fn default_particle_color_3() -> u16 {
150    2
151}
152
153fn default_particle_color_4() -> u16 {
154    3
155}
156
157#[derive(Serialize, Deserialize, Default, Clone, Debug)]
158pub struct TileNodeGraphState {
159    #[serde(default = "default_tile_node_nodes")]
160    pub nodes: Vec<TileNodeState>,
161    #[serde(default)]
162    pub connections: Vec<(u16, u8, u16, u8)>,
163    #[serde(default)]
164    pub offset: (i32, i32),
165    #[serde(default)]
166    pub selected_node: Option<usize>,
167    #[serde(default)]
168    pub preview_mode: u8,
169}
170
171#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
172pub enum TileGraphPaletteSource {
173    #[default]
174    Local,
175    Project,
176}
177
178#[derive(Serialize, Deserialize, Clone, Debug)]
179pub struct TileNodeGraphExchange {
180    #[serde(default)]
181    pub version: u32,
182    #[serde(default)]
183    pub graph_name: String,
184    #[serde(default)]
185    pub palette_source: TileGraphPaletteSource,
186    #[serde(default)]
187    pub palette_colors: Vec<TheColor>,
188    pub output_grid_width: u16,
189    pub output_grid_height: u16,
190    pub tile_pixel_width: u16,
191    pub tile_pixel_height: u16,
192    #[serde(default)]
193    pub graph_state: TileNodeGraphState,
194}
195
196#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
197pub struct TileParticleOutput {
198    pub rate: f32,
199    pub spread: f32,
200    pub lifetime_min: f32,
201    pub lifetime_max: f32,
202    pub radius_min: f32,
203    pub radius_max: f32,
204    pub speed_min: f32,
205    pub speed_max: f32,
206    pub flame_base: bool,
207    pub color_variation: u8,
208    pub ramp_colors: [[u8; 4]; 4],
209}
210
211#[derive(Clone, Debug)]
212pub struct TileLightOutput {
213    pub intensity: f32,
214    pub range: f32,
215    pub flicker: f32,
216    pub lift: f32,
217}
218
219#[derive(Serialize, Deserialize, Clone, Debug)]
220pub struct TileNodeState {
221    pub kind: TileNodeKind,
222    pub position: (i32, i32),
223    #[serde(default = "default_node_preview_open")]
224    pub preview_open: bool,
225    #[serde(default)]
226    pub bypass: bool,
227    #[serde(default)]
228    pub mute: bool,
229    #[serde(default)]
230    pub solo: bool,
231}
232
233impl Default for TileNodeState {
234    fn default() -> Self {
235        Self {
236            kind: TileNodeKind::default_output_root(),
237            position: (420, 40),
238            preview_open: true,
239            bypass: false,
240            mute: false,
241            solo: false,
242        }
243    }
244}
245
246fn default_node_preview_open() -> bool {
247    true
248}
249
250#[derive(Serialize, Deserialize, Clone, Debug)]
251pub enum TileNodeKind {
252    OutputRoot {
253        #[serde(default = "default_output_roughness")]
254        roughness: f32,
255        #[serde(default = "default_output_metallic")]
256        metallic: f32,
257        #[serde(default = "default_output_opacity")]
258        opacity: f32,
259        #[serde(default = "default_output_emissive")]
260        emissive: f32,
261        #[serde(default = "default_output_particle_enabled")]
262        particle_enabled: bool,
263        #[serde(default = "default_output_light_enabled")]
264        light_enabled: bool,
265    },
266    LayerInput {
267        name: String,
268        value: f32,
269    },
270    ImportLayer {
271        source: String,
272    },
273    GroupUV,
274    Scalar {
275        value: f32,
276    },
277    Colorize4 {
278        #[serde(default = "default_colorize4_color_1")]
279        color_1: u16,
280        #[serde(default = "default_colorize4_color_2")]
281        color_2: u16,
282        #[serde(default = "default_colorize4_color_3")]
283        color_3: u16,
284        #[serde(default = "default_colorize4_color_4")]
285        color_4: u16,
286        #[serde(default = "default_colorize4_pixel_size")]
287        pixel_size: u16,
288        #[serde(default = "default_colorize4_dither")]
289        dither: bool,
290        #[serde(default = "default_colorize4_auto_range")]
291        auto_range: bool,
292    },
293    Color {
294        color: TheColor,
295    },
296    PaletteColor {
297        index: u16,
298    },
299    NearestPalette,
300    Mix {
301        factor: f32,
302    },
303    Checker {
304        scale: u16,
305    },
306    Gradient {
307        mode: u8,
308    },
309    Curve {
310        power: f32,
311    },
312    Noise {
313        scale: f32,
314        seed: u32,
315        wrap: bool,
316    },
317    Voronoi {
318        scale: f32,
319        seed: u32,
320        jitter: f32,
321        #[serde(default = "default_voronoi_warp_amount")]
322        warp_amount: f32,
323        #[serde(default = "default_voronoi_falloff")]
324        falloff: f32,
325    },
326    BoxDivide {
327        scale: f32,
328        gap: f32,
329        rotation: f32,
330        rounding: f32,
331        #[serde(default = "default_layout_warp_amount")]
332        warp_amount: f32,
333        #[serde(default = "default_layout_falloff")]
334        falloff: f32,
335    },
336    Offset {
337        x: f32,
338        y: f32,
339    },
340    Scale {
341        x: f32,
342        y: f32,
343    },
344    Repeat {
345        repeat_x: f32,
346        repeat_y: f32,
347    },
348    Rotate {
349        angle: f32,
350    },
351    DirectionalWarp {
352        amount: f32,
353        angle: f32,
354    },
355    Brick {
356        columns: u16,
357        rows: u16,
358        #[serde(default = "default_brick_staggered")]
359        staggered: bool,
360        offset: f32,
361        #[serde(default = "default_layout_warp_amount")]
362        warp_amount: f32,
363        #[serde(default = "default_layout_falloff")]
364        falloff: f32,
365    },
366    Disc {
367        scale: f32,
368        seed: u32,
369        jitter: f32,
370        radius: f32,
371        #[serde(default = "default_layout_warp_amount")]
372        warp_amount: f32,
373        #[serde(default = "default_layout_falloff")]
374        falloff: f32,
375    },
376    IdRandom,
377    Min,
378    Max,
379    Add,
380    Subtract,
381    Multiply,
382    MakeMaterial,
383    Material {
384        roughness: f32,
385        metallic: f32,
386        opacity: f32,
387        emissive: f32,
388    },
389    MaterialMix {
390        factor: f32,
391    },
392    ParticleEmitter {
393        rate: f32,
394        spread: f32,
395        lifetime_min: f32,
396        lifetime_max: f32,
397        radius_min: f32,
398        radius_max: f32,
399        speed_min: f32,
400        speed_max: f32,
401        color_variation: u8,
402    },
403    ParticleSpawn {
404        rate: f32,
405        spread: f32,
406    },
407    ParticleMotion {
408        lifetime_min: f32,
409        lifetime_max: f32,
410        speed_min: f32,
411        speed_max: f32,
412    },
413    ParticleRender {
414        radius_min: f32,
415        radius_max: f32,
416        #[serde(default)]
417        flame_base: bool,
418        color_variation: u8,
419        #[serde(default = "default_particle_color_1")]
420        color_1: u16,
421        #[serde(default = "default_particle_color_2")]
422        color_2: u16,
423        #[serde(default = "default_particle_color_3")]
424        color_3: u16,
425        #[serde(default = "default_particle_color_4")]
426        color_4: u16,
427    },
428    LightEmitter {
429        intensity: f32,
430        range: f32,
431        flicker: f32,
432        lift: f32,
433    },
434    MaskBlend {
435        factor: f32,
436    },
437    Levels {
438        level: f32,
439        width: f32,
440    },
441    HeightShape {
442        contrast: f32,
443        bias: f32,
444        plateau: f32,
445        #[serde(default = "default_height_shape_rim")]
446        rim: f32,
447        #[serde(default = "default_layout_warp_amount")]
448        warp_amount: f32,
449    },
450    Threshold {
451        cutoff: f32,
452    },
453    Blur {
454        radius: f32,
455    },
456    SlopeBlur {
457        radius: f32,
458        amount: f32,
459    },
460    HeightEdge {
461        radius: f32,
462    },
463    Warp {
464        amount: f32,
465    },
466    Invert,
467}
468
469impl Default for TileNodeKind {
470    fn default() -> Self {
471        Self::default_output_root()
472    }
473}
474
475fn default_output_roughness() -> f32 {
476    0.9
477}
478
479fn default_output_metallic() -> f32 {
480    0.0
481}
482
483fn default_output_opacity() -> f32 {
484    1.0
485}
486
487fn default_output_emissive() -> f32 {
488    0.0
489}
490
491fn default_output_particle_enabled() -> bool {
492    true
493}
494
495fn default_output_light_enabled() -> bool {
496    true
497}
498
499impl TileNodeKind {
500    pub fn default_output_root() -> Self {
501        Self::OutputRoot {
502            roughness: default_output_roughness(),
503            metallic: default_output_metallic(),
504            opacity: default_output_opacity(),
505            emissive: default_output_emissive(),
506            particle_enabled: default_output_particle_enabled(),
507            light_enabled: default_output_light_enabled(),
508        }
509    }
510}
511
512#[derive(Clone, Copy)]
513pub struct TileEvalContext {
514    pub cell_x: u16,
515    pub cell_y: u16,
516    pub group_width: u16,
517    pub group_height: u16,
518    pub tile_pixel_width: u16,
519    pub tile_pixel_height: u16,
520    pub u: f32,
521    pub v: f32,
522}
523
524impl TileEvalContext {
525    pub fn group_u(&self) -> f32 {
526        ((self.cell_x as f32) + self.u) / (self.group_width.max(1) as f32)
527    }
528
529    pub fn group_v(&self) -> f32 {
530        ((self.cell_y as f32) + self.v) / (self.group_height.max(1) as f32)
531    }
532
533    pub fn with_group_uv(&self, group_u: f32, group_v: f32) -> Self {
534        let width = self.group_width.max(1) as f32;
535        let height = self.group_height.max(1) as f32;
536        let gx = group_u.clamp(0.0, 0.999_999) * width;
537        let gy = group_v.clamp(0.0, 0.999_999) * height;
538        let cell_x = gx.floor() as u16;
539        let cell_y = gy.floor() as u16;
540        Self {
541            cell_x,
542            cell_y,
543            group_width: self.group_width,
544            group_height: self.group_height,
545            tile_pixel_width: self.tile_pixel_width,
546            tile_pixel_height: self.tile_pixel_height,
547            u: gx.fract(),
548            v: gy.fract(),
549        }
550    }
551}
552
553impl TileNodeGraphState {
554    pub fn from_graph_data(graph_data: &str) -> Self {
555        let mut state = serde_json::from_str::<TileNodeGraphState>(graph_data).unwrap_or_default();
556        state.ensure_root();
557        state
558    }
559
560    pub fn ensure_root(&mut self) {
561        if self.nodes.is_empty() {
562            self.nodes = default_tile_node_nodes();
563        } else if !matches!(
564            self.nodes.first().map(|n| &n.kind),
565            Some(TileNodeKind::OutputRoot { .. })
566        ) {
567            self.nodes.insert(
568                0,
569                TileNodeState {
570                    kind: TileNodeKind::default_output_root(),
571                    position: (420, 40),
572                    preview_open: true,
573                    bypass: false,
574                    mute: false,
575                    solo: false,
576                },
577            );
578        }
579    }
580}
581
582impl TileNodeGraphExchange {
583    pub fn new(
584        graph_name: String,
585        output_grid_width: u16,
586        output_grid_height: u16,
587        tile_pixel_width: u16,
588        tile_pixel_height: u16,
589        graph_state: TileNodeGraphState,
590    ) -> Self {
591        Self {
592            version: 1,
593            graph_name,
594            palette_source: TileGraphPaletteSource::Local,
595            palette_colors: vec![],
596            output_grid_width,
597            output_grid_height,
598            tile_pixel_width,
599            tile_pixel_height,
600            graph_state,
601        }
602    }
603}
604
605#[derive(Clone, Debug)]
606pub struct RenderedTileGraph {
607    pub grid_width: usize,
608    pub grid_height: usize,
609    pub tile_width: usize,
610    pub tile_height: usize,
611    pub sheet_color: Vec<u8>,
612    pub sheet_material: Vec<u8>,
613    pub sheet_height: Vec<u8>,
614    pub tiles_color: Vec<Vec<u8>>,
615    pub tiles_material: Vec<Vec<u8>>,
616    pub tiles_height: Vec<Vec<u8>>,
617    pub particle_output: Option<TileParticleOutput>,
618    pub light_output: Option<TileLightOutput>,
619}
620
621pub trait TileGraphSubgraphResolver {
622    fn resolve_subgraph_state(&self, source: &str) -> Option<TileNodeGraphState>;
623
624    fn resolve_subgraph_exchange(&self, source: &str) -> Option<TileNodeGraphExchange> {
625        self.resolve_subgraph_state(source)
626            .map(|graph_state| TileNodeGraphExchange {
627                version: 1,
628                graph_name: String::new(),
629                palette_source: TileGraphPaletteSource::Local,
630                palette_colors: Vec::new(),
631                output_grid_width: 1,
632                output_grid_height: 1,
633                tile_pixel_width: 32,
634                tile_pixel_height: 32,
635                graph_state,
636            })
637    }
638}
639
640pub struct NoTileGraphSubgraphs;
641
642impl TileGraphSubgraphResolver for NoTileGraphSubgraphs {
643    fn resolve_subgraph_state(&self, _source: &str) -> Option<TileNodeGraphState> {
644        None
645    }
646}
647
648#[derive(Clone, Copy, Default)]
649struct FlatSubgraphOutputs {
650    outputs: [Option<u16>; 8],
651}
652
653#[derive(Clone, Default)]
654struct FlatSubgraphInputs {
655    inputs: Vec<Option<u16>>,
656}
657
658pub fn flatten_graph_exchange_with<R: TileGraphSubgraphResolver>(
659    graph: &TileNodeGraphExchange,
660    resolver: &R,
661) -> TileNodeGraphExchange {
662    let mut flattened = graph.clone();
663    flattened.graph_state = flatten_graph_state_recursive(
664        &graph.graph_state,
665        resolver,
666        &mut FxHashSet::default(),
667        graph.palette_source,
668        &graph.palette_colors,
669    );
670    flattened
671}
672
673pub fn flatten_graph_state_with<R: TileGraphSubgraphResolver>(
674    state: &TileNodeGraphState,
675    resolver: &R,
676) -> TileNodeGraphState {
677    let mut state = state.clone();
678    state.ensure_root();
679    flatten_graph_state_recursive(
680        &state,
681        resolver,
682        &mut FxHashSet::default(),
683        TileGraphPaletteSource::Local,
684        &[],
685    )
686}
687
688fn flatten_graph_state_recursive<R: TileGraphSubgraphResolver>(
689    state: &TileNodeGraphState,
690    resolver: &R,
691    stack: &mut FxHashSet<String>,
692    target_palette_source: TileGraphPaletteSource,
693    target_palette: &[TheColor],
694) -> TileNodeGraphState {
695    let mut nodes = Vec::new();
696    let mut node_map: Vec<Option<u16>> = vec![None; state.nodes.len()];
697    let mut subgraph_outputs: Vec<FlatSubgraphOutputs> =
698        vec![FlatSubgraphOutputs::default(); state.nodes.len()];
699    let mut subgraph_inputs: Vec<FlatSubgraphInputs> =
700        vec![FlatSubgraphInputs::default(); state.nodes.len()];
701    let mut connections = Vec::new();
702
703    if let Some(root) = state.nodes.first() {
704        nodes.push(root.clone());
705        node_map[0] = Some(0);
706    }
707
708    for (old_index, node) in state.nodes.iter().enumerate().skip(1) {
709        match &node.kind {
710            TileNodeKind::ImportLayer { source } => {
711                if !stack.insert(source.clone()) {
712                    continue;
713                }
714                let Some(mut sub_exchange) = resolver.resolve_subgraph_exchange(source) else {
715                    stack.remove(source);
716                    continue;
717                };
718                sub_exchange.graph_state.ensure_root();
719                remap_exchange_palette_for_instancing(
720                    &mut sub_exchange,
721                    target_palette_source,
722                    target_palette,
723                );
724                let sub_flat = flatten_graph_state_recursive(
725                    &sub_exchange.graph_state,
726                    resolver,
727                    stack,
728                    target_palette_source,
729                    target_palette,
730                );
731                stack.remove(source);
732
733                let base = nodes.len() as u16;
734                let mut sub_map: Vec<Option<u16>> = vec![None; sub_flat.nodes.len()];
735                let mut input_slots = Vec::new();
736                for (sub_index, sub_node) in sub_flat.nodes.iter().enumerate().skip(1) {
737                    let new_index = nodes.len() as u16;
738                    nodes.push(sub_node.clone());
739                    sub_map[sub_index] = Some(new_index);
740                    if matches!(sub_node.kind, TileNodeKind::LayerInput { .. }) {
741                        input_slots.push(Some(new_index));
742                    }
743                }
744
745                let mut outputs = [None; 8];
746                for (terminal, slot) in outputs.iter_mut().enumerate() {
747                    *slot = input_connection_source(&sub_flat, 0, terminal as u8)
748                        .and_then(|src| remap_sub_index(src, &sub_map, base));
749                }
750                subgraph_outputs[old_index] = FlatSubgraphOutputs { outputs };
751                subgraph_inputs[old_index] = FlatSubgraphInputs {
752                    inputs: input_slots,
753                };
754
755                for (src_node, src_terminal, dest_node, dest_terminal) in &sub_flat.connections {
756                    if *src_node == 0 || *dest_node == 0 {
757                        continue;
758                    }
759                    if let (Some(src), Some(dest)) =
760                        (sub_map[*src_node as usize], sub_map[*dest_node as usize])
761                    {
762                        connections.push((src, *src_terminal, dest, *dest_terminal));
763                    }
764                }
765            }
766            _ => {
767                let new_index = nodes.len() as u16;
768                nodes.push(node.clone());
769                node_map[old_index] = Some(new_index);
770            }
771        }
772    }
773
774    for (src_node, src_terminal, dest_node, dest_terminal) in &state.connections {
775        let src = if matches!(
776            state.nodes.get(*src_node as usize).map(|n| &n.kind),
777            Some(TileNodeKind::ImportLayer { .. })
778        ) {
779            let outputs = subgraph_outputs[*src_node as usize];
780            outputs.outputs.get(*src_terminal as usize).and_then(|v| *v)
781        } else {
782            node_map.get(*src_node as usize).and_then(|v| *v)
783        };
784        let dest = if matches!(
785            state.nodes.get(*dest_node as usize).map(|n| &n.kind),
786            Some(TileNodeKind::ImportLayer { .. })
787        ) {
788            subgraph_inputs
789                .get(*dest_node as usize)
790                .and_then(|i| i.inputs.get(*dest_terminal as usize))
791                .and_then(|v| *v)
792        } else {
793            node_map.get(*dest_node as usize).and_then(|v| *v)
794        };
795        if let (Some(src), Some(dest)) = (src, dest) {
796            let target_terminal = if matches!(
797                nodes.get(dest as usize).map(|n| &n.kind),
798                Some(TileNodeKind::LayerInput { .. })
799            ) {
800                0
801            } else {
802                *dest_terminal
803            };
804            connections.push((src, *src_terminal, dest, target_terminal));
805        }
806    }
807
808    TileNodeGraphState {
809        nodes,
810        connections,
811        offset: state.offset,
812        selected_node: state
813            .selected_node
814            .and_then(|index| node_map.get(index).and_then(|v| *v).map(|v| v as usize)),
815        preview_mode: state.preview_mode,
816    }
817}
818
819fn remap_exchange_palette_for_instancing(
820    exchange: &mut TileNodeGraphExchange,
821    target_palette_source: TileGraphPaletteSource,
822    target_palette: &[TheColor],
823) {
824    if exchange.palette_source != TileGraphPaletteSource::Local
825        || exchange.palette_colors.is_empty()
826        || target_palette.is_empty()
827    {
828        exchange.palette_source = target_palette_source;
829        if !target_palette.is_empty() {
830            exchange.palette_colors = target_palette.to_vec();
831        }
832        return;
833    }
834
835    for node in &mut exchange.graph_state.nodes {
836        match &mut node.kind {
837            TileNodeKind::PaletteColor { index } => {
838                *index = nearest_palette_index(
839                    palette_color_for_index(&exchange.palette_colors, *index),
840                    target_palette,
841                ) as u16;
842            }
843            TileNodeKind::Colorize4 {
844                color_1,
845                color_2,
846                color_3,
847                color_4,
848                ..
849            } => {
850                *color_1 = nearest_palette_index(
851                    palette_color_for_index(&exchange.palette_colors, *color_1),
852                    target_palette,
853                ) as u16;
854                *color_2 = nearest_palette_index(
855                    palette_color_for_index(&exchange.palette_colors, *color_2),
856                    target_palette,
857                ) as u16;
858                *color_3 = nearest_palette_index(
859                    palette_color_for_index(&exchange.palette_colors, *color_3),
860                    target_palette,
861                ) as u16;
862                *color_4 = nearest_palette_index(
863                    palette_color_for_index(&exchange.palette_colors, *color_4),
864                    target_palette,
865                ) as u16;
866            }
867            _ => {}
868        }
869    }
870
871    exchange.palette_source = target_palette_source;
872    exchange.palette_colors = target_palette.to_vec();
873}
874
875fn palette_color_for_index(palette: &[TheColor], index: u16) -> TheColor {
876    palette
877        .get(index as usize)
878        .cloned()
879        .or_else(|| palette.last().cloned())
880        .unwrap_or_else(|| TheColor::from_u8_array([0, 0, 0, 255]))
881}
882
883fn nearest_palette_index(color: TheColor, palette: &[TheColor]) -> usize {
884    if palette.is_empty() {
885        return 0;
886    }
887    let src = color.to_u8_array();
888    let mut best = 0usize;
889    let mut best_dist = i64::MAX;
890    for (i, candidate) in palette.iter().enumerate() {
891        let c = candidate.to_u8_array();
892        let dr = src[0] as i64 - c[0] as i64;
893        let dg = src[1] as i64 - c[1] as i64;
894        let db = src[2] as i64 - c[2] as i64;
895        let dist = dr * dr + dg * dg + db * db;
896        if dist < best_dist {
897            best_dist = dist;
898            best = i;
899        }
900    }
901    best
902}
903
904fn input_connection_source(
905    state: &TileNodeGraphState,
906    node_index: usize,
907    input_terminal: u8,
908) -> Option<u16> {
909    state
910        .connections
911        .iter()
912        .find(|(_, _, dest_node, dest_terminal)| {
913            *dest_node as usize == node_index && *dest_terminal == input_terminal
914        })
915        .map(|(src_node, _, _, _)| *src_node)
916}
917
918fn remap_sub_index(index: u16, sub_map: &[Option<u16>], _base: u16) -> Option<u16> {
919    if index == 0 {
920        None
921    } else {
922        sub_map.get(index as usize).and_then(|v| *v)
923    }
924}
925
926pub struct TileGraphRenderer {
927    palette: Vec<TheColor>,
928    colorize4_ranges: RwLock<Vec<Option<(f32, f32)>>>,
929}
930
931impl TileGraphRenderer {
932    pub fn new(palette: Vec<TheColor>) -> Self {
933        Self {
934            palette,
935            colorize4_ranges: RwLock::new(Vec::new()),
936        }
937    }
938
939    pub fn render_graph(&self, graph: &TileNodeGraphExchange) -> RenderedTileGraph {
940        let mut state = graph.graph_state.clone();
941        state.ensure_root();
942
943        let tile_width = graph.tile_pixel_width.max(1) as usize;
944        let tile_height = graph.tile_pixel_height.max(1) as usize;
945        let grid_width = graph.output_grid_width.max(1) as usize;
946        let grid_height = graph.output_grid_height.max(1) as usize;
947
948        let sheet_width = tile_width * grid_width;
949        let sheet_height = tile_height * grid_height;
950        let colorize4_ranges = self.compute_colorize4_ranges(
951            &state,
952            graph,
953            grid_width,
954            grid_height,
955            tile_width,
956            tile_height,
957        );
958        if let Ok(mut ranges) = self.colorize4_ranges.write() {
959            *ranges = colorize4_ranges;
960        }
961        let mut sheet_color = vec![0_u8; sheet_width * sheet_height * 4];
962        let mut sheet_material = vec![0_u8; sheet_width * sheet_height * 4];
963        let mut sheet_height_data = vec![0_u8; sheet_width * sheet_height];
964
965        for sy in 0..sheet_height {
966            for sx in 0..sheet_width {
967                let gx = if sheet_width <= 1 {
968                    0.5
969                } else {
970                    sx as f32 / (sheet_width - 1) as f32
971                };
972                let gy = if sheet_height <= 1 {
973                    0.5
974                } else {
975                    sy as f32 / (sheet_height - 1) as f32
976                };
977                let scaled_x = gx * grid_width as f32;
978                let scaled_y = gy * grid_height as f32;
979                let cell_x = scaled_x.floor().min((grid_width - 1) as f32) as u16;
980                let cell_y = scaled_y.floor().min((grid_height - 1) as f32) as u16;
981                let local_u = (scaled_x - cell_x as f32).clamp(0.0, 1.0);
982                let local_v = (scaled_y - cell_y as f32).clamp(0.0, 1.0);
983                let eval = TileEvalContext {
984                    cell_x,
985                    cell_y,
986                    group_width: graph.output_grid_width.max(1),
987                    group_height: graph.output_grid_height.max(1),
988                    tile_pixel_width: graph.tile_pixel_width.max(1),
989                    tile_pixel_height: graph.tile_pixel_height.max(1),
990                    u: local_u,
991                    v: local_v,
992                };
993                let color = self
994                    .evaluate_node_color(&state, 0, eval, &mut FxHashSet::default())
995                    .unwrap_or_else(|| TheColor::from_u8_array_3([96, 96, 96]))
996                    .to_u8_array();
997                let material = self.evaluate_output_material(&state, eval);
998                let height = self.evaluate_output_height(&state, eval);
999                let i = (sy * sheet_width + sx) * 4;
1000                sheet_color[i..i + 4].copy_from_slice(&color);
1001                sheet_material[i] = unit_to_u8(material.0);
1002                sheet_material[i + 1] = unit_to_u8(material.1);
1003                sheet_material[i + 2] = unit_to_u8(material.2);
1004                sheet_material[i + 3] = unit_to_u8(material.3);
1005                sheet_height_data[sy * sheet_width + sx] = unit_to_u8(height);
1006            }
1007        }
1008
1009        let tile_count = grid_width * grid_height;
1010        let rendered_tiles: Vec<(Vec<u8>, Vec<u8>, Vec<u8>)> = (0..tile_count)
1011            .into_par_iter()
1012            .map(|tile_index| {
1013                let cell_x = tile_index % grid_width;
1014                let cell_y = tile_index / grid_width;
1015                let mut tile_color = vec![0_u8; tile_width * tile_height * 4];
1016                let mut tile_material = vec![0_u8; tile_width * tile_height * 4];
1017                let mut tile_height_data = vec![0_u8; tile_width * tile_height];
1018
1019                for py in 0..tile_height {
1020                    for px in 0..tile_width {
1021                        let u = if tile_width <= 1 {
1022                            0.5
1023                        } else {
1024                            px as f32 / (tile_width - 1) as f32
1025                        };
1026                        let v = if tile_height <= 1 {
1027                            0.5
1028                        } else {
1029                            py as f32 / (tile_height - 1) as f32
1030                        };
1031                        let eval = TileEvalContext {
1032                            cell_x: cell_x as u16,
1033                            cell_y: cell_y as u16,
1034                            group_width: graph.output_grid_width.max(1),
1035                            group_height: graph.output_grid_height.max(1),
1036                            tile_pixel_width: graph.tile_pixel_width.max(1),
1037                            tile_pixel_height: graph.tile_pixel_height.max(1),
1038                            // Sample the full 0..1 tile span inclusively so adjacent tiles
1039                            // duplicate their shared border texels and stitch cleanly when
1040                            // used as separate atlas tiles on surfaces.
1041                            u,
1042                            v,
1043                        };
1044
1045                        let color = self
1046                            .evaluate_node_color(&state, 0, eval, &mut FxHashSet::default())
1047                            .unwrap_or_else(|| TheColor::from_u8_array_3([96, 96, 96]))
1048                            .to_u8_array();
1049                        let material = self.evaluate_output_material(&state, eval);
1050                        let height = self.evaluate_output_height(&state, eval);
1051
1052                        let i = (py * tile_width + px) * 4;
1053                        tile_color[i..i + 4].copy_from_slice(&color);
1054                        tile_material[i] = unit_to_u8(material.0);
1055                        tile_material[i + 1] = unit_to_u8(material.1);
1056                        tile_material[i + 2] = unit_to_u8(material.2);
1057                        tile_material[i + 3] = unit_to_u8(material.3);
1058                        tile_height_data[py * tile_width + px] = unit_to_u8(height);
1059                    }
1060                }
1061
1062                (tile_color, tile_material, tile_height_data)
1063            })
1064            .collect();
1065
1066        let mut tiles_color = Vec::with_capacity(tile_count);
1067        let mut tiles_material = Vec::with_capacity(tile_count);
1068        let mut tiles_height = Vec::with_capacity(tile_count);
1069
1070        for (_tile_index, (tile_color, tile_material, tile_height_data)) in
1071            rendered_tiles.into_iter().enumerate()
1072        {
1073            tiles_color.push(tile_color);
1074            tiles_material.push(tile_material);
1075            tiles_height.push(tile_height_data);
1076        }
1077
1078        RenderedTileGraph {
1079            grid_width,
1080            grid_height,
1081            tile_width,
1082            tile_height,
1083            sheet_color,
1084            sheet_material,
1085            sheet_height: sheet_height_data,
1086            tiles_color,
1087            tiles_material,
1088            tiles_height,
1089            particle_output: self.output_particle(&state),
1090            light_output: self.output_light(&state),
1091        }
1092    }
1093
1094    pub fn output_particle(&self, state: &TileNodeGraphState) -> Option<TileParticleOutput> {
1095        let Some(root) = state.nodes.first() else {
1096            return None;
1097        };
1098        let particle_enabled = match &root.kind {
1099            TileNodeKind::OutputRoot {
1100                particle_enabled, ..
1101            } => *particle_enabled,
1102            _ => false,
1103        };
1104        if !particle_enabled {
1105            return None;
1106        }
1107        let emitter_index =
1108            state
1109                .connections
1110                .iter()
1111                .find_map(|(src_node, src_term, dst_node, dst_term)| {
1112                    if *dst_node == 0 && *dst_term == 6 && *src_term == 0 {
1113                        Some(*src_node as usize)
1114                    } else {
1115                        None
1116                    }
1117                })?;
1118        match state.nodes.get(emitter_index).map(|node| &node.kind) {
1119            Some(TileNodeKind::ParticleEmitter {
1120                rate,
1121                spread,
1122                lifetime_min,
1123                lifetime_max,
1124                radius_min,
1125                radius_max,
1126                speed_min,
1127                speed_max,
1128                color_variation,
1129            }) => Some(TileParticleOutput {
1130                rate: (*rate).max(0.0),
1131                spread: (*spread).clamp(0.0, std::f32::consts::PI),
1132                lifetime_min: (*lifetime_min).max(0.01),
1133                lifetime_max: (*lifetime_max).max(*lifetime_min),
1134                radius_min: (*radius_min).max(0.001),
1135                radius_max: (*radius_max).max(*radius_min),
1136                speed_min: (*speed_min).max(0.0),
1137                speed_max: (*speed_max).max(*speed_min),
1138                flame_base: false,
1139                color_variation: *color_variation,
1140                ramp_colors: [
1141                    [255, 240, 200, 255],
1142                    [255, 176, 72, 255],
1143                    [224, 84, 24, 255],
1144                    [40, 36, 36, 255],
1145                ],
1146            }),
1147            Some(TileNodeKind::ParticleRender {
1148                radius_min,
1149                radius_max,
1150                flame_base,
1151                color_variation,
1152                color_1,
1153                color_2,
1154                color_3,
1155                color_4,
1156            }) => {
1157                let spawn = state.connections.iter().find_map(
1158                    |(src_node, src_term, dst_node, dst_term)| {
1159                        if *dst_node as usize == emitter_index && *dst_term == 0 && *src_term == 0 {
1160                            Some(*src_node as usize)
1161                        } else {
1162                            None
1163                        }
1164                    },
1165                );
1166                let motion = state.connections.iter().find_map(
1167                    |(src_node, src_term, dst_node, dst_term)| {
1168                        if *dst_node as usize == emitter_index && *dst_term == 1 && *src_term == 0 {
1169                            Some(*src_node as usize)
1170                        } else {
1171                            None
1172                        }
1173                    },
1174                );
1175                let (rate, spread) = match spawn
1176                    .and_then(|index| state.nodes.get(index))
1177                    .map(|node| &node.kind)
1178                {
1179                    Some(TileNodeKind::ParticleSpawn { rate, spread }) => {
1180                        ((*rate).max(0.0), (*spread).clamp(0.0, std::f32::consts::PI))
1181                    }
1182                    _ => (24.0, 0.75),
1183                };
1184                let (lifetime_min, lifetime_max, speed_min, speed_max) = match motion
1185                    .and_then(|index| state.nodes.get(index))
1186                    .map(|node| &node.kind)
1187                {
1188                    Some(TileNodeKind::ParticleMotion {
1189                        lifetime_min,
1190                        lifetime_max,
1191                        speed_min,
1192                        speed_max,
1193                    }) => (
1194                        (*lifetime_min).max(0.01),
1195                        (*lifetime_max).max(*lifetime_min),
1196                        (*speed_min).max(0.0),
1197                        (*speed_max).max(*speed_min),
1198                    ),
1199                    _ => (0.35, 0.9, 0.35, 1.1),
1200                };
1201                Some(TileParticleOutput {
1202                    rate,
1203                    spread,
1204                    lifetime_min,
1205                    lifetime_max,
1206                    radius_min: (*radius_min).max(0.001),
1207                    radius_max: (*radius_max).max(*radius_min),
1208                    speed_min,
1209                    speed_max,
1210                    flame_base: *flame_base,
1211                    color_variation: *color_variation,
1212                    ramp_colors: self.particle_ramp_colors(
1213                        state,
1214                        emitter_index,
1215                        [*color_1, *color_2, *color_3, *color_4],
1216                    ),
1217                })
1218            }
1219            _ => None,
1220        }
1221    }
1222
1223    pub fn output_light(&self, state: &TileNodeGraphState) -> Option<TileLightOutput> {
1224        let Some(root) = state.nodes.first() else {
1225            return None;
1226        };
1227        let light_enabled = match &root.kind {
1228            TileNodeKind::OutputRoot { light_enabled, .. } => *light_enabled,
1229            _ => false,
1230        };
1231        if !light_enabled {
1232            return None;
1233        }
1234        let light_index =
1235            state
1236                .connections
1237                .iter()
1238                .find_map(|(src_node, src_term, dst_node, dst_term)| {
1239                    if *dst_node == 0 && *dst_term == 7 && *src_term == 0 {
1240                        Some(*src_node as usize)
1241                    } else {
1242                        None
1243                    }
1244                })?;
1245        match state.nodes.get(light_index).map(|node| &node.kind) {
1246            Some(TileNodeKind::LightEmitter {
1247                intensity,
1248                range,
1249                flicker,
1250                lift,
1251            }) => Some(TileLightOutput {
1252                intensity: (*intensity).max(0.0),
1253                range: (*range).max(0.0),
1254                flicker: (*flicker).clamp(0.0, 1.0),
1255                lift: (*lift).max(0.0),
1256            }),
1257            _ => None,
1258        }
1259    }
1260
1261    fn evaluate_output_height(&self, state: &TileNodeGraphState, eval: TileEvalContext) -> f32 {
1262        self.evaluate_output_height_internal(
1263            state,
1264            eval,
1265            &mut FxHashSet::default(),
1266            &mut FxHashSet::default(),
1267        )
1268        .unwrap_or(0.5)
1269        .clamp(0.0, 1.0)
1270    }
1271
1272    fn evaluate_output_height_internal(
1273        &self,
1274        state: &TileNodeGraphState,
1275        eval: TileEvalContext,
1276        visiting: &mut FxHashSet<usize>,
1277        visiting_subgraphs: &mut FxHashSet<Uuid>,
1278    ) -> Option<f32> {
1279        self.evaluate_connected_scalar(state, 0, 1, eval, visiting, visiting_subgraphs)
1280            .or_else(|| {
1281                self.evaluate_connected_color(state, 0, 0, eval, visiting, visiting_subgraphs)
1282                    .map(Self::color_to_mask)
1283            })
1284    }
1285
1286    fn palette_color(&self, index: u16) -> Option<TheColor> {
1287        self.palette.get(index as usize).cloned().or_else(|| {
1288            let v = (index.min(255)) as u8;
1289            Some(TheColor::from_u8_array([v, v, v, 255]))
1290        })
1291    }
1292
1293    fn nearest_palette_color(&self, color: TheColor) -> TheColor {
1294        if self.palette.is_empty() {
1295            return color;
1296        }
1297        let rgba = color.to_u8_array();
1298        let mut best = self.palette[0].clone();
1299        let mut best_dist = f32::MAX;
1300        for candidate in &self.palette {
1301            let c = candidate.to_u8_array();
1302            let dr = rgba[0] as f32 - c[0] as f32;
1303            let dg = rgba[1] as f32 - c[1] as f32;
1304            let db = rgba[2] as f32 - c[2] as f32;
1305            let dist = dr * dr + dg * dg + db * db;
1306            if dist < best_dist {
1307                best_dist = dist;
1308                best = candidate.clone();
1309            }
1310        }
1311        best
1312    }
1313
1314    fn colorize4_palette_color(
1315        &self,
1316        slot: usize,
1317        color_1: u16,
1318        color_2: u16,
1319        color_3: u16,
1320        color_4: u16,
1321    ) -> TheColor {
1322        let index = match slot {
1323            0 => color_1,
1324            1 => color_2,
1325            2 => color_3,
1326            _ => color_4,
1327        };
1328        self.palette_color(index)
1329            .unwrap_or_else(|| TheColor::from_u8_array([255, 255, 255, 255]))
1330    }
1331
1332    fn particle_ramp_colors(
1333        &self,
1334        state: &TileNodeGraphState,
1335        node_index: usize,
1336        fallback: [u16; 4],
1337    ) -> [[u8; 4]; 4] {
1338        let mut colors = fallback.map(|index| {
1339            self.palette_color(index)
1340                .unwrap_or_else(|| TheColor::from_u8_array([255, 255, 255, 255]))
1341                .to_u8_array()
1342        });
1343
1344        for terminal in 2..=5u8 {
1345            let Some(source_index) =
1346                state
1347                    .connections
1348                    .iter()
1349                    .find_map(|(src_node, src_term, dst_node, dst_term)| {
1350                        if *dst_node as usize == node_index
1351                            && *dst_term == terminal
1352                            && *src_term == 0
1353                        {
1354                            Some(*src_node as usize)
1355                        } else {
1356                            None
1357                        }
1358                    })
1359            else {
1360                continue;
1361            };
1362
1363            let Some(source_kind) = state.nodes.get(source_index).map(|node| &node.kind) else {
1364                continue;
1365            };
1366
1367            match source_kind {
1368                TileNodeKind::PaletteColor { index } => {
1369                    if let Some(color) = self.palette_color(*index) {
1370                        colors[(terminal - 2) as usize] = color.to_u8_array();
1371                    }
1372                }
1373                TileNodeKind::Color { color } => {
1374                    colors[(terminal - 2) as usize] = color.to_u8_array();
1375                }
1376                TileNodeKind::Colorize4 {
1377                    color_1,
1378                    color_2,
1379                    color_3,
1380                    color_4,
1381                    ..
1382                } if terminal == 2 => {
1383                    colors = [
1384                        self.colorize4_palette_color(0, *color_1, *color_2, *color_3, *color_4)
1385                            .to_u8_array(),
1386                        self.colorize4_palette_color(1, *color_1, *color_2, *color_3, *color_4)
1387                            .to_u8_array(),
1388                        self.colorize4_palette_color(2, *color_1, *color_2, *color_3, *color_4)
1389                            .to_u8_array(),
1390                        self.colorize4_palette_color(3, *color_1, *color_2, *color_3, *color_4)
1391                            .to_u8_array(),
1392                    ];
1393                }
1394                _ => {}
1395            }
1396        }
1397
1398        colors
1399    }
1400
1401    fn bayer4(x: usize, y: usize) -> f32 {
1402        const BAYER: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
1403        BAYER[y & 3][x & 3] as f32 / 16.0
1404    }
1405
1406    fn colorize4_range(&self, node_index: usize) -> Option<(f32, f32)> {
1407        self.colorize4_ranges
1408            .read()
1409            .ok()
1410            .and_then(|ranges| ranges.get(node_index).copied().flatten())
1411    }
1412
1413    fn compute_colorize4_ranges(
1414        &self,
1415        state: &TileNodeGraphState,
1416        graph: &TileNodeGraphExchange,
1417        grid_width: usize,
1418        grid_height: usize,
1419        tile_width: usize,
1420        tile_height: usize,
1421    ) -> Vec<Option<(f32, f32)>> {
1422        let mut ranges = vec![None; state.nodes.len()];
1423        for (node_index, node) in state.nodes.iter().enumerate() {
1424            let TileNodeKind::Colorize4 { auto_range, .. } = &node.kind else {
1425                continue;
1426            };
1427            if !*auto_range || self.input_connection(state, node_index, 0).is_none() {
1428                continue;
1429            }
1430
1431            let mut min_v = f32::INFINITY;
1432            let mut max_v = f32::NEG_INFINITY;
1433
1434            for cell_y in 0..grid_height {
1435                for cell_x in 0..grid_width {
1436                    for py in 0..tile_height {
1437                        for px in 0..tile_width {
1438                            let u = if tile_width <= 1 {
1439                                0.5
1440                            } else {
1441                                px as f32 / (tile_width - 1) as f32
1442                            };
1443                            let v = if tile_height <= 1 {
1444                                0.5
1445                            } else {
1446                                py as f32 / (tile_height - 1) as f32
1447                            };
1448                            let eval = TileEvalContext {
1449                                cell_x: cell_x as u16,
1450                                cell_y: cell_y as u16,
1451                                group_width: graph.output_grid_width.max(1),
1452                                group_height: graph.output_grid_height.max(1),
1453                                tile_pixel_width: graph.tile_pixel_width.max(1),
1454                                tile_pixel_height: graph.tile_pixel_height.max(1),
1455                                u,
1456                                v,
1457                            };
1458                            if let Some(color) = self.evaluate_connected_color(
1459                                state,
1460                                node_index,
1461                                0,
1462                                eval,
1463                                &mut FxHashSet::default(),
1464                                &mut FxHashSet::default(),
1465                            ) {
1466                                let value = Self::color_to_mask(color).clamp(0.0, 1.0);
1467                                min_v = min_v.min(value);
1468                                max_v = max_v.max(value);
1469                            }
1470                        }
1471                    }
1472                }
1473            }
1474
1475            if min_v.is_finite() && max_v.is_finite() {
1476                if (max_v - min_v).abs() < 1e-5 {
1477                    let center = min_v.clamp(0.0, 1.0);
1478                    let lo = (center - 0.5).clamp(0.0, 1.0);
1479                    let hi = (center + 0.5).clamp(0.0, 1.0);
1480                    ranges[node_index] = Some((lo, hi.max(lo + 1e-5)));
1481                } else {
1482                    ranges[node_index] = Some((min_v, max_v));
1483                }
1484            }
1485        }
1486        ranges
1487    }
1488
1489    fn evaluate_output_material(
1490        &self,
1491        state: &TileNodeGraphState,
1492        eval: TileEvalContext,
1493    ) -> (f32, f32, f32, f32) {
1494        self.evaluate_output_material_internal(
1495            state,
1496            eval,
1497            &mut FxHashSet::default(),
1498            &mut FxHashSet::default(),
1499        )
1500        .unwrap_or((0.5, 0.0, 1.0, 0.0))
1501    }
1502
1503    fn evaluate_output_material_internal(
1504        &self,
1505        state: &TileNodeGraphState,
1506        eval: TileEvalContext,
1507        visiting: &mut FxHashSet<usize>,
1508        visiting_subgraphs: &mut FxHashSet<Uuid>,
1509    ) -> Option<(f32, f32, f32, f32)> {
1510        self.evaluate_connected_material(state, 0, 1, eval, visiting, visiting_subgraphs)
1511    }
1512
1513    fn solo_node_index(&self, state: &TileNodeGraphState) -> Option<usize> {
1514        state.nodes.iter().position(|n| n.solo)
1515    }
1516
1517    fn evaluate_node_scalar_internal(
1518        &self,
1519        state: &TileNodeGraphState,
1520        node_index: usize,
1521        eval: TileEvalContext,
1522        visiting: &mut FxHashSet<usize>,
1523        visiting_subgraphs: &mut FxHashSet<Uuid>,
1524    ) -> Option<f32> {
1525        self.evaluate_node_scalar_output_internal(
1526            state,
1527            node_index,
1528            0,
1529            eval,
1530            visiting,
1531            visiting_subgraphs,
1532        )
1533    }
1534
1535    fn evaluate_node_scalar_output_internal(
1536        &self,
1537        state: &TileNodeGraphState,
1538        node_index: usize,
1539        output_terminal: u8,
1540        eval: TileEvalContext,
1541        visiting: &mut FxHashSet<usize>,
1542        visiting_subgraphs: &mut FxHashSet<Uuid>,
1543    ) -> Option<f32> {
1544        state
1545            .nodes
1546            .get(node_index)
1547            .and_then(|node| match &node.kind {
1548                TileNodeKind::Scalar { value } => Some(*value),
1549                TileNodeKind::LayerInput { value, .. } => self
1550                    .evaluate_connected_scalar(
1551                        state,
1552                        node_index,
1553                        0,
1554                        eval,
1555                        visiting,
1556                        visiting_subgraphs,
1557                    )
1558                    .or(Some(*value)),
1559                _ => self
1560                    .evaluate_node_color_output_internal(
1561                        state,
1562                        node_index,
1563                        output_terminal,
1564                        eval,
1565                        visiting,
1566                        visiting_subgraphs,
1567                    )
1568                    .map(Self::color_to_mask),
1569            })
1570    }
1571
1572    fn evaluate_node_material_internal(
1573        &self,
1574        state: &TileNodeGraphState,
1575        node_index: usize,
1576        eval: TileEvalContext,
1577        visiting: &mut FxHashSet<usize>,
1578        visiting_subgraphs: &mut FxHashSet<Uuid>,
1579    ) -> Option<(f32, f32, f32, f32)> {
1580        if !visiting.insert(node_index) {
1581            return None;
1582        }
1583        let result = state.nodes.get(node_index).and_then(|node| {
1584            if node.bypass && !matches!(node.kind, TileNodeKind::OutputRoot { .. }) {
1585                if let Some(value) = self.evaluate_connected_material(
1586                    state,
1587                    node_index,
1588                    0,
1589                    eval,
1590                    visiting,
1591                    visiting_subgraphs,
1592                ) {
1593                    return Some(value);
1594                }
1595            }
1596            match &node.kind {
1597                TileNodeKind::OutputRoot {
1598                    roughness,
1599                    metallic,
1600                    opacity,
1601                    emissive,
1602                    ..
1603                } => {
1604                    if let Some(solo) = self.solo_node_index(state)
1605                        && solo != node_index
1606                    {
1607                        self.evaluate_node_material_internal(
1608                            state,
1609                            solo,
1610                            eval,
1611                            visiting,
1612                            visiting_subgraphs,
1613                        )
1614                    } else {
1615                        let mut channel = |input_terminal: u8, default: f32| -> f32 {
1616                            self.evaluate_connected_scalar(
1617                                state,
1618                                node_index,
1619                                input_terminal,
1620                                eval,
1621                                visiting,
1622                                visiting_subgraphs,
1623                            )
1624                            .unwrap_or(default)
1625                            .clamp(0.0, 1.0)
1626                        };
1627                        Some((
1628                            channel(2, *roughness),
1629                            channel(3, *metallic),
1630                            channel(4, *opacity),
1631                            channel(5, *emissive),
1632                        ))
1633                    }
1634                }
1635                TileNodeKind::ImportLayer { .. } => None,
1636                TileNodeKind::Material {
1637                    roughness,
1638                    metallic,
1639                    opacity,
1640                    emissive,
1641                } => Some((*roughness, *metallic, *opacity, *emissive)),
1642                TileNodeKind::MakeMaterial => {
1643                    let mut channel = |input_terminal: u8, default: f32| -> f32 {
1644                        self.evaluate_connected_scalar(
1645                            state,
1646                            node_index,
1647                            input_terminal,
1648                            eval,
1649                            visiting,
1650                            visiting_subgraphs,
1651                        )
1652                        .unwrap_or(default)
1653                        .clamp(0.0, 1.0)
1654                    };
1655                    Some((
1656                        channel(0, 0.5),
1657                        channel(1, 0.0),
1658                        channel(2, 1.0),
1659                        channel(3, 0.0),
1660                    ))
1661                }
1662                TileNodeKind::MaterialMix { factor } => {
1663                    let a = self.evaluate_connected_material(
1664                        state,
1665                        node_index,
1666                        0,
1667                        eval,
1668                        visiting,
1669                        visiting_subgraphs,
1670                    );
1671                    let b = self.evaluate_connected_material(
1672                        state,
1673                        node_index,
1674                        1,
1675                        eval,
1676                        visiting,
1677                        visiting_subgraphs,
1678                    );
1679                    let mask = self
1680                        .evaluate_connected_scalar(
1681                            state,
1682                            node_index,
1683                            2,
1684                            eval,
1685                            visiting,
1686                            visiting_subgraphs,
1687                        )
1688                        .unwrap_or(0.0)
1689                        .clamp(0.0, 1.0)
1690                        * factor.clamp(0.0, 1.0);
1691                    match (a, b) {
1692                        (Some(a), Some(b)) => Some((
1693                            a.0 * (1.0 - mask) + b.0 * mask,
1694                            a.1 * (1.0 - mask) + b.1 * mask,
1695                            a.2 * (1.0 - mask) + b.2 * mask,
1696                            a.3 * (1.0 - mask) + b.3 * mask,
1697                        )),
1698                        (Some(a), None) => Some(a),
1699                        (None, Some(b)) => Some(b),
1700                        (None, None) => None,
1701                    }
1702                }
1703                _ => {
1704                    if node.mute {
1705                        return Some((0.5, 0.0, 0.0, 0.0));
1706                    }
1707                    let roughness = self
1708                        .evaluate_node_scalar_internal(
1709                            state,
1710                            node_index,
1711                            eval,
1712                            visiting,
1713                            visiting_subgraphs,
1714                        )
1715                        .unwrap_or(0.5)
1716                        .clamp(0.0, 1.0);
1717                    Some((roughness, 0.0, 1.0, 0.0))
1718                }
1719            }
1720        });
1721        visiting.remove(&node_index);
1722        result
1723    }
1724
1725    fn input_connection(
1726        &self,
1727        state: &TileNodeGraphState,
1728        node_index: usize,
1729        input_terminal: u8,
1730    ) -> Option<(usize, u8)> {
1731        state
1732            .connections
1733            .iter()
1734            .find(|(_, _, dest_node, dest_terminal)| {
1735                *dest_node as usize == node_index && *dest_terminal == input_terminal
1736            })
1737            .map(|(src_node, src_terminal, _, _)| (*src_node as usize, *src_terminal))
1738    }
1739
1740    fn evaluate_connected_color(
1741        &self,
1742        state: &TileNodeGraphState,
1743        node_index: usize,
1744        input_terminal: u8,
1745        eval: TileEvalContext,
1746        visiting: &mut FxHashSet<usize>,
1747        visiting_subgraphs: &mut FxHashSet<Uuid>,
1748    ) -> Option<TheColor> {
1749        self.input_connection(state, node_index, input_terminal)
1750            .and_then(|(src, output_terminal)| {
1751                self.evaluate_node_color_output_internal(
1752                    state,
1753                    src,
1754                    output_terminal,
1755                    eval,
1756                    visiting,
1757                    visiting_subgraphs,
1758                )
1759            })
1760    }
1761
1762    fn input_connection_source(
1763        &self,
1764        state: &TileNodeGraphState,
1765        node_index: usize,
1766        input_terminal: u8,
1767    ) -> Option<usize> {
1768        self.input_connection(state, node_index, input_terminal)
1769            .map(|(src, _)| src)
1770    }
1771
1772    fn evaluate_connected_scalar(
1773        &self,
1774        state: &TileNodeGraphState,
1775        node_index: usize,
1776        input_terminal: u8,
1777        eval: TileEvalContext,
1778        visiting: &mut FxHashSet<usize>,
1779        visiting_subgraphs: &mut FxHashSet<Uuid>,
1780    ) -> Option<f32> {
1781        self.input_connection(state, node_index, input_terminal)
1782            .and_then(|(src, output_terminal)| {
1783                self.evaluate_node_scalar_output_internal(
1784                    state,
1785                    src,
1786                    output_terminal,
1787                    eval,
1788                    visiting,
1789                    visiting_subgraphs,
1790                )
1791            })
1792    }
1793
1794    fn connected_warp_vector(
1795        &self,
1796        state: &TileNodeGraphState,
1797        node_index: usize,
1798        input_terminal: u8,
1799        eval: TileEvalContext,
1800        amount: f32,
1801        visiting: &mut FxHashSet<usize>,
1802        visiting_subgraphs: &mut FxHashSet<Uuid>,
1803    ) -> Vec2<f32> {
1804        if amount <= f32::EPSILON
1805            || self
1806                .input_connection(state, node_index, input_terminal)
1807                .is_none()
1808        {
1809            return Vec2::new(0.0, 0.0);
1810        }
1811
1812        let sx = self
1813            .evaluate_connected_scalar(
1814                state,
1815                node_index,
1816                input_terminal,
1817                eval,
1818                visiting,
1819                visiting_subgraphs,
1820            )
1821            .unwrap_or(0.5);
1822        let wrapped_u = (eval.group_u() + 0.173).rem_euclid(1.0);
1823        let wrapped_v = (eval.group_v() + 0.317).rem_euclid(1.0);
1824        let sy = self
1825            .evaluate_connected_scalar(
1826                state,
1827                node_index,
1828                input_terminal,
1829                eval.with_group_uv(wrapped_u, wrapped_v),
1830                visiting,
1831                visiting_subgraphs,
1832            )
1833            .unwrap_or(sx);
1834
1835        Vec2::new((sx - 0.5) * 2.0 * amount, (sy - 0.5) * 2.0 * amount)
1836    }
1837
1838    fn evaluate_connected_material(
1839        &self,
1840        state: &TileNodeGraphState,
1841        node_index: usize,
1842        input_terminal: u8,
1843        eval: TileEvalContext,
1844        visiting: &mut FxHashSet<usize>,
1845        visiting_subgraphs: &mut FxHashSet<Uuid>,
1846    ) -> Option<(f32, f32, f32, f32)> {
1847        self.input_connection(state, node_index, input_terminal)
1848            .and_then(|(src, _output_terminal)| {
1849                self.evaluate_node_material_internal(state, src, eval, visiting, visiting_subgraphs)
1850            })
1851    }
1852
1853    fn evaluate_node_color(
1854        &self,
1855        state: &TileNodeGraphState,
1856        node_index: usize,
1857        eval: TileEvalContext,
1858        visiting: &mut FxHashSet<usize>,
1859    ) -> Option<TheColor> {
1860        self.evaluate_node_color_output_internal(
1861            state,
1862            node_index,
1863            0,
1864            eval,
1865            visiting,
1866            &mut FxHashSet::default(),
1867        )
1868    }
1869
1870    fn evaluate_node_color_internal(
1871        &self,
1872        state: &TileNodeGraphState,
1873        node_index: usize,
1874        eval: TileEvalContext,
1875        visiting: &mut FxHashSet<usize>,
1876        visiting_subgraphs: &mut FxHashSet<Uuid>,
1877    ) -> Option<TheColor> {
1878        self.evaluate_node_color_output_internal(
1879            state,
1880            node_index,
1881            0,
1882            eval,
1883            visiting,
1884            visiting_subgraphs,
1885        )
1886    }
1887
1888    fn evaluate_node_color_output_internal(
1889        &self,
1890        state: &TileNodeGraphState,
1891        node_index: usize,
1892        output_terminal: u8,
1893        eval: TileEvalContext,
1894        visiting: &mut FxHashSet<usize>,
1895        visiting_subgraphs: &mut FxHashSet<Uuid>,
1896    ) -> Option<TheColor> {
1897        if !visiting.insert(node_index) {
1898            return None;
1899        }
1900        let result = state.nodes.get(node_index).and_then(|node| {
1901            if node.mute {
1902                return Some(TheColor::from_u8_array([0, 0, 0, 0]));
1903            }
1904            if node.bypass
1905                && !matches!(
1906                    node.kind,
1907                    TileNodeKind::OutputRoot { .. } | TileNodeKind::GroupUV
1908                )
1909            {
1910                if let Some(color) = self.evaluate_connected_color(
1911                    state,
1912                    node_index,
1913                    0,
1914                    eval,
1915                    visiting,
1916                    visiting_subgraphs,
1917                ) {
1918                    return Some(color);
1919                }
1920            }
1921            match &node.kind {
1922                TileNodeKind::OutputRoot { .. } => {
1923                    if let Some(solo) = self.solo_node_index(state)
1924                        && solo != node_index
1925                    {
1926                        self.evaluate_node_color_output_internal(
1927                            state,
1928                            solo,
1929                            output_terminal,
1930                            eval,
1931                            visiting,
1932                            visiting_subgraphs,
1933                        )
1934                    } else {
1935                        self.evaluate_connected_color(
1936                            state,
1937                            node_index,
1938                            0,
1939                            eval,
1940                            visiting,
1941                            visiting_subgraphs,
1942                        )
1943                    }
1944                }
1945                TileNodeKind::LayerInput { value, .. } => self
1946                    .evaluate_connected_color(
1947                        state,
1948                        node_index,
1949                        0,
1950                        eval,
1951                        visiting,
1952                        visiting_subgraphs,
1953                    )
1954                    .or_else(|| {
1955                        let v = unit_to_u8(*value);
1956                        Some(TheColor::from_u8_array([v, v, v, 255]))
1957                    }),
1958                TileNodeKind::ImportLayer { .. } => None,
1959                TileNodeKind::ParticleEmitter { .. }
1960                | TileNodeKind::ParticleSpawn { .. }
1961                | TileNodeKind::ParticleMotion { .. }
1962                | TileNodeKind::ParticleRender { .. }
1963                | TileNodeKind::LightEmitter { .. } => None,
1964                TileNodeKind::GroupUV => Some(TheColor::from_u8_array([
1965                    unit_to_u8(eval.group_u()),
1966                    unit_to_u8(eval.group_v()),
1967                    0,
1968                    255,
1969                ])),
1970                TileNodeKind::Scalar { value } => {
1971                    let v = unit_to_u8(*value);
1972                    Some(TheColor::from_u8_array([v, v, v, 255]))
1973                }
1974                TileNodeKind::Colorize4 {
1975                    color_1,
1976                    color_2,
1977                    color_3,
1978                    color_4,
1979                    pixel_size,
1980                    dither,
1981                    auto_range,
1982                } => {
1983                    let quantized_eval = if *pixel_size > 1 {
1984                        let px_size = (*pixel_size).max(1) as f32;
1985                        let tile_w = eval.tile_pixel_width.max(1) as f32;
1986                        let tile_h = eval.tile_pixel_height.max(1) as f32;
1987                        let x = (eval.u * (tile_w - 1.0)).round();
1988                        let y = (eval.v * (tile_h - 1.0)).round();
1989                        let qx =
1990                            ((x / px_size).floor() * px_size).clamp(0.0, (tile_w - 1.0).max(0.0));
1991                        let qy =
1992                            ((y / px_size).floor() * px_size).clamp(0.0, (tile_h - 1.0).max(0.0));
1993                        let u = if tile_w <= 1.0 {
1994                            0.5
1995                        } else {
1996                            qx / (tile_w - 1.0)
1997                        };
1998                        let v = if tile_h <= 1.0 {
1999                            0.5
2000                        } else {
2001                            qy / (tile_h - 1.0)
2002                        };
2003                        TileEvalContext { u, v, ..eval }
2004                    } else {
2005                        eval
2006                    };
2007                    self.evaluate_connected_color(
2008                        state,
2009                        node_index,
2010                        0,
2011                        quantized_eval,
2012                        visiting,
2013                        visiting_subgraphs,
2014                    )
2015                    .map(|color| {
2016                        let mut t = Self::color_to_mask(color).clamp(0.0, 1.0);
2017                        if *auto_range
2018                            && let Some((min_v, max_v)) = self.colorize4_range(node_index)
2019                        {
2020                            let width = (max_v - min_v).max(1e-5);
2021                            t = ((t - min_v) / width).clamp(0.0, 1.0);
2022                        }
2023                        t = t.clamp(0.0, 0.999_999);
2024                        if *dither {
2025                            let x = (quantized_eval.u
2026                                * (quantized_eval.tile_pixel_width.max(1) - 1) as f32)
2027                                .round()
2028                                .max(0.0) as usize;
2029                            let y = (quantized_eval.v
2030                                * (quantized_eval.tile_pixel_height.max(1) - 1) as f32)
2031                                .round()
2032                                .max(0.0) as usize;
2033                            let threshold = Self::bayer4(x, y) - 0.5;
2034                            t = (t + threshold * 0.18).clamp(0.0, 0.999_999);
2035                        }
2036                        let slot = (t * 4.0).floor() as usize;
2037                        self.colorize4_palette_color(slot, *color_1, *color_2, *color_3, *color_4)
2038                    })
2039                }
2040                TileNodeKind::Gradient { mode } => {
2041                    let gu = eval.group_u().clamp(0.0, 1.0);
2042                    let gv = eval.group_v().clamp(0.0, 1.0);
2043                    let value = match mode {
2044                        0 => gu,
2045                        1 => gv,
2046                        _ => {
2047                            let dx = gu - 0.5;
2048                            let dy = gv - 0.5;
2049                            (1.0 - (dx * dx + dy * dy).sqrt() * 2.0).clamp(0.0, 1.0)
2050                        }
2051                    };
2052                    let v = unit_to_u8(value);
2053                    Some(TheColor::from_u8_array([v, v, v, 255]))
2054                }
2055                TileNodeKind::Curve { power } => self
2056                    .evaluate_connected_color(
2057                        state,
2058                        node_index,
2059                        0,
2060                        eval,
2061                        visiting,
2062                        visiting_subgraphs,
2063                    )
2064                    .map(|color| {
2065                        let value = Self::color_to_mask(color)
2066                            .clamp(0.0, 1.0)
2067                            .powf(power.clamp(0.1, 4.0));
2068                        let v = unit_to_u8(value);
2069                        TheColor::from_u8_array([v, v, v, 255])
2070                    }),
2071                TileNodeKind::Color { color } => Some(color.clone()),
2072                TileNodeKind::PaletteColor { index } => self.palette_color(*index),
2073                TileNodeKind::NearestPalette => self
2074                    .evaluate_connected_color(
2075                        state,
2076                        node_index,
2077                        0,
2078                        eval,
2079                        visiting,
2080                        visiting_subgraphs,
2081                    )
2082                    .map(|color| self.nearest_palette_color(color)),
2083                TileNodeKind::Mix { factor } => {
2084                    let a = self.evaluate_connected_color(
2085                        state,
2086                        node_index,
2087                        0,
2088                        eval,
2089                        visiting,
2090                        visiting_subgraphs,
2091                    );
2092                    let b = self.evaluate_connected_color(
2093                        state,
2094                        node_index,
2095                        1,
2096                        eval,
2097                        visiting,
2098                        visiting_subgraphs,
2099                    );
2100                    match (a, b) {
2101                        (Some(a), Some(b)) => Some(Self::mix_colors(a, b, *factor)),
2102                        (Some(a), None) => Some(a),
2103                        (None, Some(b)) => Some(b),
2104                        (None, None) => None,
2105                    }
2106                }
2107                TileNodeKind::Checker { scale } => {
2108                    let s = (*scale).max(1) as f32;
2109                    let cx = (eval.group_u() * s).floor() as i32;
2110                    let cy = (eval.group_v() * s).floor() as i32;
2111                    let a = self
2112                        .evaluate_connected_color(
2113                            state,
2114                            node_index,
2115                            0,
2116                            eval,
2117                            visiting,
2118                            visiting_subgraphs,
2119                        )
2120                        .unwrap_or_else(|| TheColor::from_u8_array_3([255, 255, 255]));
2121                    let b = self
2122                        .evaluate_connected_color(
2123                            state,
2124                            node_index,
2125                            1,
2126                            eval,
2127                            visiting,
2128                            visiting_subgraphs,
2129                        )
2130                        .unwrap_or_else(|| TheColor::from_u8_array_3([0, 0, 0]));
2131                    if (cx + cy) & 1 == 0 { Some(a) } else { Some(b) }
2132                }
2133                TileNodeKind::Noise { scale, seed, wrap } => {
2134                    let s = (*scale).clamp(0.0, 1.0).max(0.0001);
2135                    let frequency = (s * 64.0).round().max(1.0) as i32;
2136                    let v = unit_to_u8(Self::value_noise(
2137                        eval.group_u(),
2138                        eval.group_v(),
2139                        frequency,
2140                        *seed,
2141                        *wrap,
2142                    ));
2143                    Some(TheColor::from_u8_array([v, v, v, 255]))
2144                }
2145                TileNodeKind::Voronoi {
2146                    scale,
2147                    seed,
2148                    jitter,
2149                    warp_amount,
2150                    falloff,
2151                } => {
2152                    let warp = self.connected_warp_vector(
2153                        state,
2154                        node_index,
2155                        0,
2156                        eval,
2157                        warp_amount.clamp(0.0, 0.25),
2158                        visiting,
2159                        visiting_subgraphs,
2160                    );
2161                    let value = match output_terminal {
2162                        1 => Self::voronoi_height(eval, warp, *scale, *seed, *jitter, *falloff),
2163                        2 => Self::voronoi_cell_id(eval, warp, *scale, *seed, *jitter),
2164                        _ => Self::voronoi_center(eval, warp, *scale, *seed, *jitter),
2165                    };
2166                    let v = unit_to_u8(value);
2167                    Some(TheColor::from_u8_array([v, v, v, 255]))
2168                }
2169                TileNodeKind::BoxDivide {
2170                    scale,
2171                    gap,
2172                    rotation,
2173                    rounding,
2174                    warp_amount,
2175                    falloff,
2176                } => {
2177                    let warp = self.connected_warp_vector(
2178                        state,
2179                        node_index,
2180                        0,
2181                        eval,
2182                        warp_amount.clamp(0.0, 0.25),
2183                        visiting,
2184                        visiting_subgraphs,
2185                    );
2186                    let value = match output_terminal {
2187                        1 => Self::box_divide_height(
2188                            eval, warp, *scale, *gap, *rotation, *rounding, *falloff,
2189                        ),
2190                        2 => {
2191                            Self::box_divide_cell_id(eval, warp, *scale, *gap, *rotation, *rounding)
2192                        }
2193                        _ => {
2194                            Self::box_divide_center(eval, warp, *scale, *gap, *rotation, *rounding)
2195                        }
2196                    };
2197                    let v = unit_to_u8(value);
2198                    Some(TheColor::from_u8_array([v, v, v, 255]))
2199                }
2200                TileNodeKind::Offset { x, y } => self
2201                    .input_connection(state, node_index, 0)
2202                    .and_then(|(src, out)| {
2203                        self.evaluate_node_color_output_internal(
2204                            state,
2205                            src,
2206                            out,
2207                            eval.with_group_uv(eval.group_u() + *x, eval.group_v() + *y),
2208                            visiting,
2209                            visiting_subgraphs,
2210                        )
2211                    }),
2212                TileNodeKind::Scale { x, y } => self
2213                    .input_connection(state, node_index, 0)
2214                    .and_then(|(src, out)| {
2215                        let gu = (eval.group_u() - 0.5) * x.max(0.1) + 0.5;
2216                        let gv = (eval.group_v() - 0.5) * y.max(0.1) + 0.5;
2217                        self.evaluate_node_color_output_internal(
2218                            state,
2219                            src,
2220                            out,
2221                            eval.with_group_uv(gu, gv),
2222                            visiting,
2223                            visiting_subgraphs,
2224                        )
2225                    }),
2226                TileNodeKind::Repeat { repeat_x, repeat_y } => self
2227                    .input_connection(state, node_index, 0)
2228                    .and_then(|(src, out)| {
2229                        let wrapped_u = (eval.group_u() * repeat_x.max(0.1)).fract();
2230                        let wrapped_v = (eval.group_v() * repeat_y.max(0.1)).fract();
2231                        self.evaluate_node_color_output_internal(
2232                            state,
2233                            src,
2234                            out,
2235                            eval.with_group_uv(wrapped_u, wrapped_v),
2236                            visiting,
2237                            visiting_subgraphs,
2238                        )
2239                    }),
2240                TileNodeKind::Rotate { angle } => self
2241                    .input_connection(state, node_index, 0)
2242                    .and_then(|(src, out)| {
2243                        let radians = angle.to_radians();
2244                        let s = radians.sin();
2245                        let c = radians.cos();
2246                        let dx = eval.group_u() - 0.5;
2247                        let dy = eval.group_v() - 0.5;
2248                        let ru = dx * c - dy * s + 0.5;
2249                        let rv = dx * s + dy * c + 0.5;
2250                        self.evaluate_node_color_output_internal(
2251                            state,
2252                            src,
2253                            out,
2254                            eval.with_group_uv(ru, rv),
2255                            visiting,
2256                            visiting_subgraphs,
2257                        )
2258                    }),
2259                TileNodeKind::DirectionalWarp { amount, angle } => self
2260                    .input_connection_source(state, node_index, 0)
2261                    .and_then(|src| {
2262                        let warp = self
2263                            .input_connection_source(state, node_index, 1)
2264                            .and_then(|warp_src| {
2265                                self.evaluate_node_color_internal(
2266                                    state,
2267                                    warp_src,
2268                                    eval,
2269                                    visiting,
2270                                    visiting_subgraphs,
2271                                )
2272                            })
2273                            .map(Self::color_to_mask)
2274                            .unwrap_or(0.5);
2275                        let radians = angle.to_radians();
2276                        let delta = (warp - 0.5) * amount.clamp(0.0, 1.0);
2277                        let du = radians.cos() * delta;
2278                        let dv = radians.sin() * delta;
2279                        self.evaluate_node_color_internal(
2280                            state,
2281                            src,
2282                            eval.with_group_uv(eval.group_u() + du, eval.group_v() + dv),
2283                            visiting,
2284                            visiting_subgraphs,
2285                        )
2286                    }),
2287                TileNodeKind::Brick {
2288                    columns,
2289                    rows,
2290                    staggered,
2291                    offset,
2292                    warp_amount,
2293                    falloff,
2294                } => {
2295                    let warp = self.connected_warp_vector(
2296                        state,
2297                        node_index,
2298                        0,
2299                        eval,
2300                        warp_amount.clamp(0.0, 0.25),
2301                        visiting,
2302                        visiting_subgraphs,
2303                    );
2304                    let value = match output_terminal {
2305                        1 => Self::brick_height(
2306                            eval, warp, *columns, *rows, *staggered, *offset, *falloff,
2307                        ),
2308                        2 => Self::brick_cell_id(eval, warp, *columns, *rows, *staggered, *offset),
2309                        _ => Self::brick_center(eval, warp, *columns, *rows, *staggered, *offset),
2310                    };
2311                    let v = unit_to_u8(value);
2312                    Some(TheColor::from_u8_array([v, v, v, 255]))
2313                }
2314                TileNodeKind::Disc {
2315                    scale,
2316                    seed,
2317                    jitter,
2318                    radius,
2319                    warp_amount,
2320                    falloff,
2321                } => {
2322                    let warp = self.connected_warp_vector(
2323                        state,
2324                        node_index,
2325                        0,
2326                        eval,
2327                        warp_amount.clamp(0.0, 0.25),
2328                        visiting,
2329                        visiting_subgraphs,
2330                    );
2331                    let density = self
2332                        .evaluate_connected_scalar(
2333                            state,
2334                            node_index,
2335                            1,
2336                            eval,
2337                            visiting,
2338                            visiting_subgraphs,
2339                        )
2340                        .unwrap_or(*scale)
2341                        .clamp(0.05, 2.0);
2342                    let jitter = self
2343                        .evaluate_connected_scalar(
2344                            state,
2345                            node_index,
2346                            2,
2347                            eval,
2348                            visiting,
2349                            visiting_subgraphs,
2350                        )
2351                        .unwrap_or(*jitter)
2352                        .clamp(0.0, 1.0);
2353                    let radius = self
2354                        .evaluate_connected_scalar(
2355                            state,
2356                            node_index,
2357                            3,
2358                            eval,
2359                            visiting,
2360                            visiting_subgraphs,
2361                        )
2362                        .unwrap_or(*radius)
2363                        .clamp(0.05, 1.0);
2364                    let falloff = self
2365                        .evaluate_connected_scalar(
2366                            state,
2367                            node_index,
2368                            4,
2369                            eval,
2370                            visiting,
2371                            visiting_subgraphs,
2372                        )
2373                        .unwrap_or(*falloff)
2374                        .clamp(0.1, 4.0);
2375                    let value = match output_terminal {
2376                        1 => Self::disc_height(eval, warp, density, *seed, jitter, radius, falloff),
2377                        2 => Self::disc_cell_id(eval, warp, density, *seed, jitter),
2378                        _ => Self::disc_center(eval, warp, density, *seed, jitter, radius),
2379                    };
2380                    let v = unit_to_u8(value);
2381                    Some(TheColor::from_u8_array([v, v, v, 255]))
2382                }
2383                TileNodeKind::IdRandom => {
2384                    let id = self
2385                        .evaluate_connected_color(
2386                            state,
2387                            node_index,
2388                            0,
2389                            eval,
2390                            visiting,
2391                            visiting_subgraphs,
2392                        )
2393                        .map(Self::color_to_mask)
2394                        .unwrap_or(0.0);
2395                    let key = (id.clamp(0.0, 1.0) * 65535.0).round() as i32;
2396                    let v = unit_to_u8(Self::hash2(key, key ^ 0x45d9f3, 0x9e37_79b9));
2397                    Some(TheColor::from_u8_array([v, v, v, 255]))
2398                }
2399                TileNodeKind::Min => {
2400                    let a = self.evaluate_connected_color(
2401                        state,
2402                        node_index,
2403                        0,
2404                        eval,
2405                        visiting,
2406                        visiting_subgraphs,
2407                    );
2408                    let b = self.evaluate_connected_color(
2409                        state,
2410                        node_index,
2411                        1,
2412                        eval,
2413                        visiting,
2414                        visiting_subgraphs,
2415                    );
2416                    match (a, b) {
2417                        (Some(a), Some(b)) => {
2418                            let av = Self::color_to_mask(a);
2419                            let bv = Self::color_to_mask(b);
2420                            let v = unit_to_u8(av.min(bv));
2421                            Some(TheColor::from_u8_array([v, v, v, 255]))
2422                        }
2423                        (Some(a), None) => Some(a),
2424                        (None, Some(b)) => Some(b),
2425                        (None, None) => None,
2426                    }
2427                }
2428                TileNodeKind::Max => {
2429                    let a = self.evaluate_connected_color(
2430                        state,
2431                        node_index,
2432                        0,
2433                        eval,
2434                        visiting,
2435                        visiting_subgraphs,
2436                    );
2437                    let b = self.evaluate_connected_color(
2438                        state,
2439                        node_index,
2440                        1,
2441                        eval,
2442                        visiting,
2443                        visiting_subgraphs,
2444                    );
2445                    match (a, b) {
2446                        (Some(a), Some(b)) => {
2447                            let av = Self::color_to_mask(a);
2448                            let bv = Self::color_to_mask(b);
2449                            let v = unit_to_u8(av.max(bv));
2450                            Some(TheColor::from_u8_array([v, v, v, 255]))
2451                        }
2452                        (Some(a), None) => Some(a),
2453                        (None, Some(b)) => Some(b),
2454                        (None, None) => None,
2455                    }
2456                }
2457                TileNodeKind::Add => {
2458                    let a = self.evaluate_connected_color(
2459                        state,
2460                        node_index,
2461                        0,
2462                        eval,
2463                        visiting,
2464                        visiting_subgraphs,
2465                    );
2466                    let b = self.evaluate_connected_color(
2467                        state,
2468                        node_index,
2469                        1,
2470                        eval,
2471                        visiting,
2472                        visiting_subgraphs,
2473                    );
2474                    match (a, b) {
2475                        (Some(a), Some(b)) => {
2476                            let av = Self::color_to_mask(a);
2477                            let bv = Self::color_to_mask(b);
2478                            let v = unit_to_u8((av + bv).clamp(0.0, 1.0));
2479                            Some(TheColor::from_u8_array([v, v, v, 255]))
2480                        }
2481                        (Some(a), None) => Some(a),
2482                        (None, Some(b)) => Some(b),
2483                        (None, None) => None,
2484                    }
2485                }
2486                TileNodeKind::Subtract => {
2487                    let a = self.evaluate_connected_color(
2488                        state,
2489                        node_index,
2490                        0,
2491                        eval,
2492                        visiting,
2493                        visiting_subgraphs,
2494                    );
2495                    let b = self.evaluate_connected_color(
2496                        state,
2497                        node_index,
2498                        1,
2499                        eval,
2500                        visiting,
2501                        visiting_subgraphs,
2502                    );
2503                    match (a, b) {
2504                        (Some(a), Some(b)) => {
2505                            let av = Self::color_to_mask(a);
2506                            let bv = Self::color_to_mask(b);
2507                            let v = unit_to_u8((av - bv).clamp(0.0, 1.0));
2508                            Some(TheColor::from_u8_array([v, v, v, 255]))
2509                        }
2510                        (Some(a), None) => Some(a),
2511                        (None, Some(_)) => Some(TheColor::from_u8_array([0, 0, 0, 255])),
2512                        (None, None) => None,
2513                    }
2514                }
2515                TileNodeKind::Multiply => {
2516                    let a = self.evaluate_connected_color(
2517                        state,
2518                        node_index,
2519                        0,
2520                        eval,
2521                        visiting,
2522                        visiting_subgraphs,
2523                    );
2524                    let b = self.evaluate_connected_color(
2525                        state,
2526                        node_index,
2527                        1,
2528                        eval,
2529                        visiting,
2530                        visiting_subgraphs,
2531                    );
2532                    match (a, b) {
2533                        (Some(a), Some(b)) => Some(Self::multiply_colors(a, b)),
2534                        (Some(a), None) => Some(a),
2535                        (None, Some(b)) => Some(b),
2536                        (None, None) => None,
2537                    }
2538                }
2539                TileNodeKind::MakeMaterial => self
2540                    .evaluate_node_material_internal(
2541                        state,
2542                        node_index,
2543                        eval,
2544                        visiting,
2545                        visiting_subgraphs,
2546                    )
2547                    .map(material_to_color),
2548                TileNodeKind::Material {
2549                    roughness,
2550                    metallic,
2551                    opacity,
2552                    emissive,
2553                } => Some(material_to_color((
2554                    *roughness, *metallic, *opacity, *emissive,
2555                ))),
2556                TileNodeKind::MaterialMix { .. } => self
2557                    .evaluate_node_material_internal(
2558                        state,
2559                        node_index,
2560                        eval,
2561                        visiting,
2562                        visiting_subgraphs,
2563                    )
2564                    .map(material_to_color),
2565                TileNodeKind::MaskBlend { factor } => {
2566                    let a = self.evaluate_connected_color(
2567                        state,
2568                        node_index,
2569                        0,
2570                        eval,
2571                        visiting,
2572                        visiting_subgraphs,
2573                    );
2574                    let b = self.evaluate_connected_color(
2575                        state,
2576                        node_index,
2577                        1,
2578                        eval,
2579                        visiting,
2580                        visiting_subgraphs,
2581                    );
2582                    let mask = self
2583                        .evaluate_connected_color(
2584                            state,
2585                            node_index,
2586                            2,
2587                            eval,
2588                            visiting,
2589                            visiting_subgraphs,
2590                        )
2591                        .map(Self::color_to_mask)
2592                        .unwrap_or(0.0);
2593                    match (a, b) {
2594                        (Some(a), Some(b)) => Some(Self::mix_colors(a, b, mask * *factor)),
2595                        (Some(a), None) => Some(a),
2596                        (None, Some(b)) => Some(b),
2597                        (None, None) => None,
2598                    }
2599                }
2600                TileNodeKind::Levels { level, width } => self
2601                    .evaluate_connected_color(
2602                        state,
2603                        node_index,
2604                        0,
2605                        eval,
2606                        visiting,
2607                        visiting_subgraphs,
2608                    )
2609                    .map(|color| {
2610                        let width = width.clamp(0.001, 1.0);
2611                        let level = level.clamp(0.0, 1.0);
2612                        let half = width * 0.5;
2613                        let low = (level - half).clamp(0.0, 1.0);
2614                        let high = (level + half).clamp(0.0, 1.0);
2615                        let v = unit_to_u8(Self::remap_unit(Self::color_to_mask(color), low, high));
2616                        TheColor::from_u8_array([v, v, v, 255])
2617                    }),
2618                TileNodeKind::HeightShape {
2619                    contrast,
2620                    bias,
2621                    plateau,
2622                    rim,
2623                    warp_amount,
2624                } => self
2625                    .evaluate_connected_color(
2626                        state,
2627                        node_index,
2628                        0,
2629                        eval,
2630                        visiting,
2631                        visiting_subgraphs,
2632                    )
2633                    .map(|fallback| {
2634                        let warp = self.connected_warp_vector(
2635                            state,
2636                            node_index,
2637                            1,
2638                            eval,
2639                            warp_amount.clamp(0.0, 0.25),
2640                            visiting,
2641                            visiting_subgraphs,
2642                        );
2643                        let input = self
2644                            .evaluate_connected_color(
2645                                state,
2646                                node_index,
2647                                0,
2648                                eval.with_group_uv(
2649                                    (eval.group_u() + warp.x).rem_euclid(1.0),
2650                                    (eval.group_v() + warp.y).rem_euclid(1.0),
2651                                ),
2652                                visiting,
2653                                visiting_subgraphs,
2654                            )
2655                            .unwrap_or(fallback);
2656                        let mut v = Self::color_to_mask(input).clamp(0.0, 1.0);
2657                        let contrast = self
2658                            .evaluate_connected_scalar(
2659                                state,
2660                                node_index,
2661                                2,
2662                                eval,
2663                                visiting,
2664                                visiting_subgraphs,
2665                            )
2666                            .unwrap_or(*contrast)
2667                            .clamp(0.1, 4.0);
2668                        v = ((v - 0.5) * contrast + 0.5).clamp(0.0, 1.0);
2669
2670                        let bias = self
2671                            .evaluate_connected_scalar(
2672                                state,
2673                                node_index,
2674                                3,
2675                                eval,
2676                                visiting,
2677                                visiting_subgraphs,
2678                            )
2679                            .unwrap_or(*bias)
2680                            .clamp(-1.0, 1.0);
2681                        if bias < 0.0 {
2682                            let power = (1.0 + (-bias * 3.0)).clamp(1.0, 4.0);
2683                            v = v.powf(power);
2684                        } else if bias > 0.0 {
2685                            let power = (1.0 + (bias * 3.0)).clamp(1.0, 4.0);
2686                            v = 1.0 - (1.0 - v).powf(power);
2687                        }
2688
2689                        let plateau = self
2690                            .evaluate_connected_scalar(
2691                                state,
2692                                node_index,
2693                                4,
2694                                eval,
2695                                visiting,
2696                                visiting_subgraphs,
2697                            )
2698                            .unwrap_or(*plateau)
2699                            .clamp(0.0, 3.0);
2700                        let rim = self
2701                            .evaluate_connected_scalar(
2702                                state,
2703                                node_index,
2704                                5,
2705                                eval,
2706                                visiting,
2707                                visiting_subgraphs,
2708                            )
2709                            .unwrap_or(*rim)
2710                            .clamp(0.0, 4.0);
2711                        if rim > 0.0 {
2712                            let shoulder = (4.0 * v * (1.0 - v)).clamp(0.0, 1.0);
2713                            v = (v - shoulder * rim * 0.18).clamp(0.0, 1.0);
2714                        }
2715                        if plateau > 0.0 {
2716                            let top = (1.0 - plateau * 0.18).clamp(0.35, 0.999);
2717                            if v > top {
2718                                let t = ((v - top) / (1.0 - top).max(0.0001)).clamp(0.0, 1.0);
2719                                let flatten = (0.2 / (1.0 + plateau * 1.2)).clamp(0.03, 0.2);
2720                                let curve = t * t * (3.0 - 2.0 * t);
2721                                v = top + curve * (1.0 - top) * flatten;
2722                            }
2723                        }
2724
2725                        let out = unit_to_u8(v);
2726                        TheColor::from_u8_array([out, out, out, 255])
2727                    }),
2728                TileNodeKind::Threshold { cutoff } => self
2729                    .evaluate_connected_color(
2730                        state,
2731                        node_index,
2732                        0,
2733                        eval,
2734                        visiting,
2735                        visiting_subgraphs,
2736                    )
2737                    .map(|color| {
2738                        let v = if Self::color_to_mask(color) >= *cutoff {
2739                            255
2740                        } else {
2741                            0
2742                        };
2743                        TheColor::from_u8_array([v, v, v, 255])
2744                    }),
2745                TileNodeKind::Blur { radius } => {
2746                    let radius = radius.clamp(0.001, 0.08);
2747                    let offsets = [
2748                        (-1.0f32, -1.0f32),
2749                        (0.0, -1.0),
2750                        (1.0, -1.0),
2751                        (-1.0, 0.0),
2752                        (0.0, 0.0),
2753                        (1.0, 0.0),
2754                        (-1.0, 1.0),
2755                        (0.0, 1.0),
2756                        (1.0, 1.0),
2757                    ];
2758                    let mut sum = 0.0;
2759                    let mut weight_sum = 0.0;
2760                    for (ox, oy) in offsets {
2761                        let weight = if ox == 0.0 && oy == 0.0 {
2762                            2.0
2763                        } else if ox == 0.0 || oy == 0.0 {
2764                            1.0
2765                        } else {
2766                            0.75
2767                        };
2768                        let value = self
2769                            .evaluate_connected_color(
2770                                state,
2771                                node_index,
2772                                0,
2773                                eval.with_group_uv(
2774                                    eval.group_u() + ox * radius,
2775                                    eval.group_v() + oy * radius,
2776                                ),
2777                                visiting,
2778                                visiting_subgraphs,
2779                            )
2780                            .map(Self::color_to_mask)
2781                            .unwrap_or(0.0);
2782                        sum += value * weight;
2783                        weight_sum += weight;
2784                    }
2785                    let v = unit_to_u8((sum / weight_sum.max(0.0001)).clamp(0.0, 1.0));
2786                    Some(TheColor::from_u8_array([v, v, v, 255]))
2787                }
2788                TileNodeKind::SlopeBlur { radius, amount } => {
2789                    let radius = radius.clamp(0.001, 0.08);
2790                    let amount = amount.clamp(0.0, 1.0);
2791                    let center = self
2792                        .evaluate_connected_color(
2793                            state,
2794                            node_index,
2795                            0,
2796                            eval,
2797                            visiting,
2798                            visiting_subgraphs,
2799                        )
2800                        .map(Self::color_to_mask)
2801                        .unwrap_or(0.0);
2802                    let directions = [
2803                        Vec2::new(1.0f32, 0.0),
2804                        Vec2::new(0.707, 0.707),
2805                        Vec2::new(0.0, 1.0),
2806                        Vec2::new(-0.707, 0.707),
2807                        Vec2::new(-1.0, 0.0),
2808                        Vec2::new(-0.707, -0.707),
2809                        Vec2::new(0.0, -1.0),
2810                        Vec2::new(0.707, -0.707),
2811                    ];
2812                    let mut sum = center * 2.0;
2813                    let mut weight_sum = 2.0;
2814                    for dir in directions {
2815                        let near_u = eval.group_u() + dir.x * radius;
2816                        let near_v = eval.group_v() + dir.y * radius;
2817                        let near = self
2818                            .evaluate_connected_color(
2819                                state,
2820                                node_index,
2821                                0,
2822                                eval.with_group_uv(near_u, near_v),
2823                                visiting,
2824                                visiting_subgraphs,
2825                            )
2826                            .map(Self::color_to_mask)
2827                            .unwrap_or(center);
2828                        let shifted_u =
2829                            eval.group_u() + dir.x * radius * (1.0 + near * amount * 2.0);
2830                        let shifted_v =
2831                            eval.group_v() + dir.y * radius * (1.0 + near * amount * 2.0);
2832                        let shifted = self
2833                            .evaluate_connected_color(
2834                                state,
2835                                node_index,
2836                                0,
2837                                eval.with_group_uv(shifted_u, shifted_v),
2838                                visiting,
2839                                visiting_subgraphs,
2840                            )
2841                            .map(Self::color_to_mask)
2842                            .unwrap_or(near);
2843                        let weight = 0.75 + near * amount;
2844                        sum += shifted * weight;
2845                        weight_sum += weight;
2846                    }
2847                    let v = unit_to_u8((sum / weight_sum.max(0.0001)).clamp(0.0, 1.0));
2848                    Some(TheColor::from_u8_array([v, v, v, 255]))
2849                }
2850                TileNodeKind::HeightEdge { radius } => {
2851                    let radius = radius.clamp(0.001, 0.08);
2852                    let sample = |u: f32,
2853                                  v: f32,
2854                                  visiting: &mut FxHashSet<usize>,
2855                                  visiting_subgraphs: &mut FxHashSet<Uuid>|
2856                     -> f32 {
2857                        self.evaluate_connected_color(
2858                            state,
2859                            node_index,
2860                            0,
2861                            eval.with_group_uv(u, v),
2862                            visiting,
2863                            visiting_subgraphs,
2864                        )
2865                        .map(Self::color_to_mask)
2866                        .unwrap_or(0.0)
2867                    };
2868                    let c = sample(eval.group_u(), eval.group_v(), visiting, visiting_subgraphs);
2869                    let l = sample(
2870                        eval.group_u() - radius,
2871                        eval.group_v(),
2872                        visiting,
2873                        visiting_subgraphs,
2874                    );
2875                    let r = sample(
2876                        eval.group_u() + radius,
2877                        eval.group_v(),
2878                        visiting,
2879                        visiting_subgraphs,
2880                    );
2881                    let t = sample(
2882                        eval.group_u(),
2883                        eval.group_v() - radius,
2884                        visiting,
2885                        visiting_subgraphs,
2886                    );
2887                    let b = sample(
2888                        eval.group_u(),
2889                        eval.group_v() + radius,
2890                        visiting,
2891                        visiting_subgraphs,
2892                    );
2893                    let edge = (((c - l).abs() + (c - r).abs() + (c - t).abs() + (c - b).abs())
2894                        * 0.5)
2895                        .clamp(0.0, 1.0);
2896                    let v = unit_to_u8(edge);
2897                    Some(TheColor::from_u8_array([v, v, v, 255]))
2898                }
2899                TileNodeKind::Warp { amount } => self
2900                    .input_connection_source(state, node_index, 0)
2901                    .and_then(|src| {
2902                        let warp = self
2903                            .input_connection_source(state, node_index, 1)
2904                            .and_then(|warp_src| {
2905                                self.evaluate_node_color_internal(
2906                                    state,
2907                                    warp_src,
2908                                    eval,
2909                                    visiting,
2910                                    visiting_subgraphs,
2911                                )
2912                            })
2913                            .map(Self::color_to_mask)
2914                            .unwrap_or(0.5);
2915                        let delta = (warp - 0.5) * amount * 0.5;
2916                        self.evaluate_node_color_internal(
2917                            state,
2918                            src,
2919                            eval.with_group_uv(eval.group_u() + delta, eval.group_v() + delta),
2920                            visiting,
2921                            visiting_subgraphs,
2922                        )
2923                    }),
2924                TileNodeKind::Invert => self
2925                    .input_connection_source(state, node_index, 0)
2926                    .and_then(|src| {
2927                        self.evaluate_node_color_internal(
2928                            state,
2929                            src,
2930                            eval,
2931                            visiting,
2932                            visiting_subgraphs,
2933                        )
2934                    })
2935                    .map(|color| {
2936                        let rgba = color.to_u8_array();
2937                        TheColor::from_u8_array([
2938                            255 - rgba[0],
2939                            255 - rgba[1],
2940                            255 - rgba[2],
2941                            rgba[3],
2942                        ])
2943                    }),
2944            }
2945        });
2946        visiting.remove(&node_index);
2947        result
2948    }
2949
2950    fn mix_colors(a: TheColor, b: TheColor, factor: f32) -> TheColor {
2951        let t = factor.clamp(0.0, 1.0);
2952        let aa = a.to_u8_array();
2953        let bb = b.to_u8_array();
2954        let lerp = |x: u8, y: u8| -> u8 {
2955            ((x as f32 * (1.0 - t) + y as f32 * t).round()).clamp(0.0, 255.0) as u8
2956        };
2957        TheColor::from_u8_array([
2958            lerp(aa[0], bb[0]),
2959            lerp(aa[1], bb[1]),
2960            lerp(aa[2], bb[2]),
2961            lerp(aa[3], bb[3]),
2962        ])
2963    }
2964
2965    fn multiply_colors(a: TheColor, b: TheColor) -> TheColor {
2966        let aa = a.to_u8_array();
2967        let bb = b.to_u8_array();
2968        let mul = |x: u8, y: u8| -> u8 { ((x as u16 * y as u16) / 255) as u8 };
2969        TheColor::from_u8_array([
2970            mul(aa[0], bb[0]),
2971            mul(aa[1], bb[1]),
2972            mul(aa[2], bb[2]),
2973            mul(aa[3], bb[3]),
2974        ])
2975    }
2976
2977    fn color_to_mask(color: TheColor) -> f32 {
2978        let rgba = color.to_u8_array();
2979        (0.2126 * rgba[0] as f32 + 0.7152 * rgba[1] as f32 + 0.0722 * rgba[2] as f32) / 255.0
2980    }
2981
2982    fn hash2(x: i32, y: i32, seed: u32) -> f32 {
2983        let mut n = x as u32;
2984        n = n
2985            .wrapping_mul(374761393)
2986            .wrapping_add((y as u32).wrapping_mul(668265263));
2987        n ^= seed.wrapping_mul(2246822519);
2988        n = (n ^ (n >> 13)).wrapping_mul(1274126177);
2989        ((n ^ (n >> 16)) & 0x00ff_ffff) as f32 / 0x00ff_ffff as f32
2990    }
2991
2992    fn smoothstep_unit(t: f32) -> f32 {
2993        let t = t.clamp(0.0, 1.0);
2994        t * t * (3.0 - 2.0 * t)
2995    }
2996
2997    fn value_noise(u: f32, v: f32, frequency: i32, seed: u32, wrap: bool) -> f32 {
2998        let frequency = frequency.max(1);
2999        let (u, v) = if wrap {
3000            (u.rem_euclid(1.0), v.rem_euclid(1.0))
3001        } else {
3002            (u, v)
3003        };
3004        let x = u * frequency as f32;
3005        let y = v * frequency as f32;
3006        let x0 = x.floor() as i32;
3007        let y0 = y.floor() as i32;
3008        let x1 = x0 + 1;
3009        let y1 = y0 + 1;
3010        let fx = Self::smoothstep_unit(x.fract());
3011        let fy = Self::smoothstep_unit(y.fract());
3012
3013        let sample = |ix: i32, iy: i32| -> f32 {
3014            let (sx, sy) = if wrap {
3015                (ix.rem_euclid(frequency), iy.rem_euclid(frequency))
3016            } else {
3017                (ix, iy)
3018            };
3019            Self::hash2(sx, sy, seed)
3020        };
3021
3022        let v00 = sample(x0, y0);
3023        let v10 = sample(x1, y0);
3024        let v01 = sample(x0, y1);
3025        let v11 = sample(x1, y1);
3026        let vx0 = v00 * (1.0 - fx) + v10 * fx;
3027        let vx1 = v01 * (1.0 - fx) + v11 * fx;
3028        (vx0 * (1.0 - fy) + vx1 * fy).clamp(0.0, 1.0)
3029    }
3030
3031    fn remap_unit(value: f32, low: f32, high: f32) -> f32 {
3032        let span = (high - low).max(0.000_1);
3033        ((value - low) / span).clamp(0.0, 1.0)
3034    }
3035
3036    fn layout_repeat_from_scale(scale: f32) -> i32 {
3037        ((scale.clamp(0.05, 2.0) * 6.0).round() as i32).max(1)
3038    }
3039
3040    fn box_divide_repeat_from_density(density: f32) -> i32 {
3041        (1 + (density.clamp(0.0, 0.2) * 20.0).round() as i32).max(1)
3042    }
3043
3044    fn box_divide_iterations_from_density(density: f32) -> i32 {
3045        (1 + (density.clamp(0.0, 0.2) / 0.2 * 5.0).round() as i32).max(1)
3046    }
3047
3048    fn voronoi_data(
3049        eval: TileEvalContext,
3050        warp: Vec2<f32>,
3051        scale: f32,
3052        seed: u32,
3053        jitter: f32,
3054        falloff: f32,
3055    ) -> (f32, f32, f32) {
3056        let repeat = Self::layout_repeat_from_scale(scale);
3057        let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3058        let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3059        let x = u * repeat as f32;
3060        let y = v * repeat as f32;
3061        let cell_x = x.floor() as i32;
3062        let cell_y = y.floor() as i32;
3063        let frac_x = x.fract();
3064        let frac_y = y.fract();
3065        let jitter = jitter.clamp(0.0, 1.0);
3066        let mut min_dist = f32::MAX;
3067        let mut second_dist = f32::MAX;
3068        let mut nearest = (cell_x, cell_y);
3069        for oy in -1..=1 {
3070            for ox in -1..=1 {
3071                let sx = cell_x + ox;
3072                let sy = cell_y + oy;
3073                let wx = sx.rem_euclid(repeat);
3074                let wy = sy.rem_euclid(repeat);
3075                let px = 0.5 + (Self::hash2(wx, wy, seed) - 0.5) * jitter;
3076                let py = 0.5 + (Self::hash2(wx, wy, seed ^ 0x9e37_79b9) - 0.5) * jitter;
3077                let dx = ox as f32 + px - frac_x;
3078                let dy = oy as f32 + py - frac_y;
3079                let dist = (dx * dx + dy * dy).sqrt();
3080                if dist < min_dist {
3081                    second_dist = min_dist;
3082                    min_dist = dist;
3083                    nearest = (wx, wy);
3084                } else if dist < second_dist {
3085                    second_dist = dist;
3086                }
3087            }
3088        }
3089        let center = (1.0 - (min_dist / 1.4142)).clamp(0.0, 1.0);
3090        let edge_distance = ((second_dist - min_dist) / second_dist.max(0.0001)).clamp(0.0, 1.0);
3091        let height = edge_distance.powf(falloff.clamp(0.1, 4.0));
3092        let id = Self::hash2(nearest.0, nearest.1, seed ^ 0x51f1_5e11);
3093        (center, height, id)
3094    }
3095
3096    fn voronoi_center(
3097        eval: TileEvalContext,
3098        warp: Vec2<f32>,
3099        scale: f32,
3100        seed: u32,
3101        jitter: f32,
3102    ) -> f32 {
3103        Self::voronoi_data(eval, warp, scale, seed, jitter, 1.0).0
3104    }
3105
3106    fn voronoi_height(
3107        eval: TileEvalContext,
3108        warp: Vec2<f32>,
3109        scale: f32,
3110        seed: u32,
3111        jitter: f32,
3112        falloff: f32,
3113    ) -> f32 {
3114        Self::voronoi_data(eval, warp, scale, seed, jitter, falloff).1
3115    }
3116
3117    fn voronoi_cell_id(
3118        eval: TileEvalContext,
3119        warp: Vec2<f32>,
3120        scale: f32,
3121        seed: u32,
3122        jitter: f32,
3123    ) -> f32 {
3124        Self::voronoi_data(eval, warp, scale, seed, jitter, 1.0).2
3125    }
3126
3127    fn brick_data(
3128        eval: TileEvalContext,
3129        warp: Vec2<f32>,
3130        columns: u16,
3131        rows: u16,
3132        staggered: bool,
3133        offset: f32,
3134        falloff: f32,
3135    ) -> (f32, f32, f32) {
3136        let cols = columns.max(1) as i32;
3137        let rows = rows.max(1) as i32;
3138        let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3139        let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3140        let gv = v * rows as f32;
3141        let row = gv.floor() as i32;
3142        let gu = u * cols as f32
3143            + if staggered && row & 1 == 1 {
3144                offset
3145            } else {
3146                0.0
3147            };
3148        let brick_x = gu.rem_euclid(cols as f32);
3149        let col = brick_x.floor() as i32;
3150        let local_x = brick_x.fract();
3151        let local_y = gv.fract();
3152
3153        let dx = ((local_x - 0.5).abs() * 2.0).clamp(0.0, 1.0);
3154        let dy = ((local_y - 0.5).abs() * 2.0).clamp(0.0, 1.0);
3155        let center = (1.0 - ((dx * dx + dy * dy).sqrt() / 1.4142)).clamp(0.0, 1.0);
3156
3157        let edge =
3158            (local_x.min(1.0 - local_x).min(local_y.min(1.0 - local_y)) * 2.0).clamp(0.0, 1.0);
3159        let height = edge.powf(falloff.clamp(0.1, 4.0));
3160
3161        let id = Self::hash2(col.rem_euclid(cols), row.rem_euclid(rows), 0x61c8_8647);
3162        (center, height, id)
3163    }
3164
3165    fn brick_center(
3166        eval: TileEvalContext,
3167        warp: Vec2<f32>,
3168        columns: u16,
3169        rows: u16,
3170        staggered: bool,
3171        offset: f32,
3172    ) -> f32 {
3173        Self::brick_data(eval, warp, columns, rows, staggered, offset, 1.0).0
3174    }
3175
3176    fn brick_height(
3177        eval: TileEvalContext,
3178        warp: Vec2<f32>,
3179        columns: u16,
3180        rows: u16,
3181        staggered: bool,
3182        offset: f32,
3183        falloff: f32,
3184    ) -> f32 {
3185        Self::brick_data(eval, warp, columns, rows, staggered, offset, falloff).1
3186    }
3187
3188    fn brick_cell_id(
3189        eval: TileEvalContext,
3190        warp: Vec2<f32>,
3191        columns: u16,
3192        rows: u16,
3193        staggered: bool,
3194        offset: f32,
3195    ) -> f32 {
3196        Self::brick_data(eval, warp, columns, rows, staggered, offset, 1.0).2
3197    }
3198
3199    fn disc_data(
3200        eval: TileEvalContext,
3201        warp: Vec2<f32>,
3202        scale: f32,
3203        seed: u32,
3204        jitter: f32,
3205        radius: f32,
3206        falloff: f32,
3207    ) -> (f32, f32, f32) {
3208        let repeat = Self::layout_repeat_from_scale(scale);
3209        let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3210        let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3211        let x = u * repeat as f32;
3212        let y = v * repeat as f32;
3213        let cell_x = x.floor() as i32;
3214        let cell_y = y.floor() as i32;
3215        let frac_x = x.fract();
3216        let frac_y = y.fract();
3217        let jitter = jitter.clamp(0.0, 1.0);
3218        let radius = radius.clamp(0.05, 1.0);
3219
3220        let mut min_dist = f32::MAX;
3221        let mut nearest = (cell_x, cell_y);
3222        for oy in -1..=1 {
3223            for ox in -1..=1 {
3224                let sx = cell_x + ox;
3225                let sy = cell_y + oy;
3226                let wx = sx.rem_euclid(repeat);
3227                let wy = sy.rem_euclid(repeat);
3228                let px = 0.5 + (Self::hash2(wx, wy, seed) - 0.5) * jitter;
3229                let py = 0.5 + (Self::hash2(wx, wy, seed ^ 0x9e37_79b9) - 0.5) * jitter;
3230                let dx = ox as f32 + px - frac_x;
3231                let dy = oy as f32 + py - frac_y;
3232                let dist = (dx * dx + dy * dy).sqrt();
3233                if dist < min_dist {
3234                    min_dist = dist;
3235                    nearest = (wx, wy);
3236                }
3237            }
3238        }
3239
3240        let radius_cells = 0.5 * radius;
3241        let center = (1.0 - (min_dist / radius_cells.max(0.0001))).clamp(0.0, 1.0);
3242        let height = center.powf(falloff.clamp(0.1, 4.0));
3243        let id = Self::hash2(nearest.0, nearest.1, seed ^ 0x2f6e_2b1d);
3244        (center, height, id)
3245    }
3246
3247    fn disc_center(
3248        eval: TileEvalContext,
3249        warp: Vec2<f32>,
3250        scale: f32,
3251        seed: u32,
3252        jitter: f32,
3253        radius: f32,
3254    ) -> f32 {
3255        Self::disc_data(eval, warp, scale, seed, jitter, radius, 1.0).0
3256    }
3257
3258    fn disc_height(
3259        eval: TileEvalContext,
3260        warp: Vec2<f32>,
3261        scale: f32,
3262        seed: u32,
3263        jitter: f32,
3264        radius: f32,
3265        falloff: f32,
3266    ) -> f32 {
3267        Self::disc_data(eval, warp, scale, seed, jitter, radius, falloff).1
3268    }
3269
3270    fn disc_cell_id(
3271        eval: TileEvalContext,
3272        warp: Vec2<f32>,
3273        scale: f32,
3274        seed: u32,
3275        jitter: f32,
3276    ) -> f32 {
3277        Self::disc_data(eval, warp, scale, seed, jitter, 1.0, 1.0).2
3278    }
3279
3280    fn box_divide_data(
3281        eval: TileEvalContext,
3282        warp: Vec2<f32>,
3283        scale: f32,
3284        gap: f32,
3285        rotation: f32,
3286        rounding: f32,
3287        falloff: f32,
3288    ) -> (f32, f32, f32) {
3289        let repeat = Self::box_divide_repeat_from_density(scale);
3290        let iterations = Self::box_divide_iterations_from_density(scale);
3291        let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3292        let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3293        let x = u * repeat as f32;
3294        let y = v * repeat as f32;
3295        let cell_x = x.floor() as i32;
3296        let cell_y = y.floor() as i32;
3297        let local = Vec2::new(x.fract(), y.fract());
3298        let wrapped_cell = Vec2::new(
3299            cell_x.rem_euclid(repeat) as f32,
3300            cell_y.rem_euclid(repeat) as f32,
3301        );
3302        let (dist, id) = box_divide(
3303            local,
3304            wrapped_cell,
3305            gap.clamp(0.0, 4.0),
3306            rotation,
3307            rounding.clamp(0.0, 0.5),
3308            iterations,
3309        );
3310        let center = (1.0 - (dist.abs() * 6.0)).clamp(0.0, 1.0);
3311        let height = (1.0 - (dist.max(0.0) * 12.0))
3312            .clamp(0.0, 1.0)
3313            .powf(falloff.clamp(0.1, 4.0));
3314        (center, height, id)
3315    }
3316
3317    fn box_divide_center(
3318        eval: TileEvalContext,
3319        warp: Vec2<f32>,
3320        scale: f32,
3321        gap: f32,
3322        rotation: f32,
3323        rounding: f32,
3324    ) -> f32 {
3325        Self::box_divide_data(eval, warp, scale, gap, rotation, rounding, 1.0).0
3326    }
3327
3328    fn box_divide_height(
3329        eval: TileEvalContext,
3330        warp: Vec2<f32>,
3331        scale: f32,
3332        gap: f32,
3333        rotation: f32,
3334        rounding: f32,
3335        falloff: f32,
3336    ) -> f32 {
3337        Self::box_divide_data(eval, warp, scale, gap, rotation, rounding, falloff).1
3338    }
3339
3340    fn box_divide_cell_id(
3341        eval: TileEvalContext,
3342        warp: Vec2<f32>,
3343        scale: f32,
3344        gap: f32,
3345        rotation: f32,
3346        rounding: f32,
3347    ) -> f32 {
3348        Self::box_divide_data(eval, warp, scale, gap, rotation, rounding, 1.0).2
3349    }
3350}
3351
3352fn material_to_color(material: (f32, f32, f32, f32)) -> TheColor {
3353    TheColor::from_u8_array([
3354        unit_to_u8(material.0),
3355        unit_to_u8(material.1),
3356        unit_to_u8(material.2),
3357        unit_to_u8(material.3),
3358    ])
3359}
3360
3361pub fn unit_to_u8(value: f32) -> u8 {
3362    (value.clamp(0.0, 1.0) * 255.0).round() as u8
3363}