1use std::collections::hash_map::DefaultHasher;
42use std::collections::HashMap;
43use std::hash::{Hash, Hasher};
44
45use bytemuck::{Pod, Zeroable};
46use wgpu::util::DeviceExt;
47
48use super::gpu::GpuContext;
49
50#[derive(Debug, Clone, PartialEq)]
56pub enum SdfFill {
57 Solid { color: [f32; 4] },
59 Outline { color: [f32; 4], thickness: f32 },
61 SolidWithOutline { fill: [f32; 4], outline: [f32; 4], thickness: f32 },
63 Gradient { from: [f32; 4], to: [f32; 4], angle: f32, scale: f32 },
66 Glow { color: [f32; 4], intensity: f32 },
68 CosinePalette { a: [f32; 3], b: [f32; 3], c: [f32; 3], d: [f32; 3] },
70}
71
72#[derive(Debug, Clone)]
74pub struct SdfCommand {
75 pub sdf_expr: String,
77 pub fill: SdfFill,
79 pub x: f32,
81 pub y: f32,
83 pub bounds: f32,
85 pub layer: i32,
87 pub rotation: f32,
89 pub scale: f32,
91 pub opacity: f32,
93}
94
95#[repr(C)]
101#[derive(Copy, Clone, Pod, Zeroable)]
102struct SdfQuadVertex {
103 position: [f32; 2],
104 uv: [f32; 2],
105}
106
107const SDF_QUAD_VERTICES: &[SdfQuadVertex] = &[
110 SdfQuadVertex { position: [-1.0, -1.0], uv: [0.0, 0.0] },
111 SdfQuadVertex { position: [ 1.0, -1.0], uv: [1.0, 0.0] },
112 SdfQuadVertex { position: [ 1.0, 1.0], uv: [1.0, 1.0] },
113 SdfQuadVertex { position: [-1.0, 1.0], uv: [0.0, 1.0] },
114];
115
116const SDF_QUAD_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
117
118#[repr(C)]
120#[derive(Copy, Clone, Pod, Zeroable)]
121struct SdfInstance {
122 position: [f32; 2],
124 bounds: f32,
126 rotation: f32,
128 scale: f32,
130 opacity: f32,
132 _pad: [f32; 2],
134 color: [f32; 4],
136}
137
138pub fn compute_pipeline_key(sdf_expr: &str, fill: &SdfFill) -> u64 {
145 let mut hasher = DefaultHasher::new();
146 sdf_expr.hash(&mut hasher);
147 compute_fill_hash_into(fill, &mut hasher);
148 hasher.finish()
149}
150
151pub fn compute_fill_hash(fill: &SdfFill) -> u64 {
153 let mut hasher = DefaultHasher::new();
154 compute_fill_hash_into(fill, &mut hasher);
155 hasher.finish()
156}
157
158fn compute_fill_hash_into(fill: &SdfFill, hasher: &mut DefaultHasher) {
160 std::mem::discriminant(fill).hash(hasher);
162 match fill {
164 SdfFill::Solid { color } => {
165 hash_f32_array(color, hasher);
166 }
167 SdfFill::Outline { color, thickness } => {
168 hash_f32_array(color, hasher);
169 thickness.to_bits().hash(hasher);
170 }
171 SdfFill::SolidWithOutline { fill, outline, thickness } => {
172 hash_f32_array(fill, hasher);
173 hash_f32_array(outline, hasher);
174 thickness.to_bits().hash(hasher);
175 }
176 SdfFill::Gradient { from, to, angle, scale } => {
177 hash_f32_array(from, hasher);
178 hash_f32_array(to, hasher);
179 angle.to_bits().hash(hasher);
180 scale.to_bits().hash(hasher);
181 }
182 SdfFill::Glow { color, intensity } => {
183 hash_f32_array(color, hasher);
184 intensity.to_bits().hash(hasher);
185 }
186 SdfFill::CosinePalette { a, b, c, d } => {
187 hash_f32_array(a, hasher);
188 hash_f32_array(b, hasher);
189 hash_f32_array(c, hasher);
190 hash_f32_array(d, hasher);
191 }
192 }
193}
194
195fn hash_f32_array(arr: &[f32], hasher: &mut DefaultHasher) {
196 for v in arr {
197 v.to_bits().hash(hasher);
198 }
199}
200
201const SDF_PRIMITIVES_WGSL: &str = r#"
208// ---- SDF Primitives ----
209
210fn sd_circle(p: vec2<f32>, r: f32) -> f32 {
211 return length(p) - r;
212}
213
214fn sd_box(p: vec2<f32>, b: vec2<f32>) -> f32 {
215 let d = abs(p) - b;
216 return length(max(d, vec2<f32>(0.0))) + min(max(d.x, d.y), 0.0);
217}
218
219fn sd_rounded_box(p: vec2<f32>, b: vec2<f32>, r: vec4<f32>) -> f32 {
220 // r.x = top-left, r.y = top-right, r.z = bottom-right, r.w = bottom-left
221 var radius = r.x;
222 if (p.x > 0.0 && p.y > 0.0) { radius = r.y; } // top-right
223 else if (p.x > 0.0 && p.y < 0.0) { radius = r.z; } // bottom-right
224 else if (p.x < 0.0 && p.y < 0.0) { radius = r.w; } // bottom-left
225 let q = abs(p) - b + vec2<f32>(radius);
226 return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - radius;
227}
228
229fn sd_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
230 let pa = p - a;
231 let ba = b - a;
232 let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
233 return length(pa - ba * h);
234}
235
236fn sd_capsule(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
237 return sd_segment(p, a, b) - r;
238}
239
240fn sd_equilateral_triangle(p_in: vec2<f32>, r: f32) -> f32 {
241 let k = sqrt(3.0);
242 var p = p_in;
243 p.x = abs(p.x) - r;
244 p.y = p.y + r / k;
245 if (p.x + k * p.y > 0.0) {
246 p = vec2<f32>(p.x - k * p.y, -k * p.x - p.y) / 2.0;
247 }
248 p.x -= clamp(p.x, -2.0 * r, 0.0);
249 return -length(p) * sign(p.y);
250}
251
252fn sd_ring(p: vec2<f32>, r: f32, thickness: f32) -> f32 {
253 return abs(length(p) - r) - thickness;
254}
255
256fn sd_ellipse(p: vec2<f32>, ab: vec2<f32>) -> f32 {
257 // Approximate ellipse SDF via scaling
258 let scaled = p / ab;
259 let d = length(scaled) - 1.0;
260 return d * min(ab.x, ab.y);
261}
262
263fn sd_hexagon(p_in: vec2<f32>, r: f32) -> f32 {
264 let k = vec3<f32>(-0.866025404, 0.5, 0.577350269);
265 var p = abs(p_in);
266 p = p - 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
267 p = p - vec2<f32>(clamp(p.x, -k.z * r, k.z * r), r);
268 return length(p) * sign(p.y);
269}
270
271fn sd_star5(p_in: vec2<f32>, r: f32, rf: f32) -> f32 {
272 let k1 = vec2<f32>(0.809016994, -0.587785252);
273 let k2 = vec2<f32>(-k1.x, k1.y);
274 var p = vec2<f32>(abs(p_in.x), p_in.y);
275 p = p - 2.0 * max(dot(k1, p), 0.0) * k1;
276 p = p - 2.0 * max(dot(k2, p), 0.0) * k2;
277 p = vec2<f32>(abs(p.x), p.y - r);
278 let ba = rf * vec2<f32>(-k1.y, k1.x) - vec2<f32>(0.0, 1.0);
279 let h = clamp(dot(p, ba) / dot(ba, ba), 0.0, r);
280 return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y);
281}
282
283fn sd_cross(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
284 var pp = abs(p);
285 if (pp.y > pp.x) { pp = pp.yx; }
286 let q = pp - b;
287 let k = max(q.y, q.x);
288 let w = select(vec2<f32>(b.y - pp.x, -k), q, k > 0.0);
289 return sign(k) * length(max(w, vec2<f32>(0.0))) + r;
290}
291
292fn sd_triangle(p: vec2<f32>, p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>) -> f32 {
293 let e0 = p1 - p0;
294 let e1 = p2 - p1;
295 let e2 = p0 - p2;
296 let v0 = p - p0;
297 let v1 = p - p1;
298 let v2 = p - p2;
299 let pq0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0);
300 let pq1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0);
301 let pq2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0);
302 let s = sign(e0.x * e2.y - e0.y * e2.x);
303 let d0 = vec2<f32>(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x));
304 let d1 = vec2<f32>(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x));
305 let d2 = vec2<f32>(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x));
306 let d = min(min(d0, d1), d2);
307 return -sqrt(d.x) * sign(d.y);
308}
309
310fn sd_egg(p_in: vec2<f32>, ra: f32, rb: f32) -> f32 {
311 // Egg shape: ra is the main radius, rb is the bulge factor
312 let k = sqrt(3.0);
313 var p = vec2<f32>(abs(p_in.x), -p_in.y); // flip Y for our coordinate system
314 let r = ra - rb;
315 if (p.y < 0.0) {
316 return length(p) - r - rb;
317 } else if (k * (p.x + r) < p.y) {
318 return length(vec2<f32>(p.x, p.y - k * r)) - rb;
319 } else {
320 return length(vec2<f32>(p.x + r, p.y)) - 2.0 * r - rb;
321 }
322}
323
324fn sd_heart(p_in: vec2<f32>, size: f32) -> f32 {
325 // Normalize to unit heart, flip Y so point is at bottom
326 var p = vec2<f32>(abs(p_in.x), p_in.y) / size;
327 // IQ's heart SDF (modified for our coordinate system)
328 if (p.y + p.x > 1.0) {
329 return (sqrt(dot(p - vec2<f32>(0.25, 0.75), p - vec2<f32>(0.25, 0.75))) - sqrt(2.0) / 4.0) * size;
330 }
331 return sqrt(min(
332 dot(p - vec2<f32>(0.0, 1.0), p - vec2<f32>(0.0, 1.0)),
333 dot(p - 0.5 * max(p.x + p.y, 0.0), p - 0.5 * max(p.x + p.y, 0.0))
334 )) * sign(p.x - p.y) * size;
335}
336
337fn sd_moon(p_in: vec2<f32>, d: f32, ra: f32, rb: f32) -> f32 {
338 var p = vec2<f32>(p_in.x, abs(p_in.y));
339 let a = (ra * ra - rb * rb + d * d) / (2.0 * d);
340 let b = sqrt(max(ra * ra - a * a, 0.0));
341 if (d * (p.x * b - p.y * a) > d * d * max(b - p.y, 0.0)) {
342 return length(p - vec2<f32>(a, b));
343 }
344 return max(length(p) - ra, -(length(p - vec2<f32>(d, 0.0)) - rb));
345}
346
347fn sd_pentagon(p_in: vec2<f32>, r: f32) -> f32 {
348 // Regular pentagon SDF
349 let k = vec3<f32>(0.809016994, 0.587785252, 0.726542528);
350 var p = vec2<f32>(abs(p_in.x), -p_in.y); // flip Y for our coordinate system
351 p = p - 2.0 * min(dot(vec2<f32>(-k.x, k.y), p), 0.0) * vec2<f32>(-k.x, k.y);
352 p = p - 2.0 * min(dot(vec2<f32>(k.x, k.y), p), 0.0) * vec2<f32>(k.x, k.y);
353 p = p - vec2<f32>(clamp(p.x, -r * k.z, r * k.z), r);
354 return length(p) * sign(p.y);
355}
356
357fn sd_star(p_in: vec2<f32>, r: f32, n: f32, inner_ratio: f32) -> f32 {
358 // n-pointed star with inner radius = r * inner_ratio
359 // Based on IQ's approach but with inner_ratio parameter
360 let pi = 3.141592653;
361 let an = pi / n; // half angle between points
362
363 // Map to first sector using polar coordinates
364 let angle = atan2(p_in.y, p_in.x);
365 // Use fract-based modulo for correct handling of negative angles
366 let sector = (angle / (2.0 * an));
367 let sector_fract = sector - floor(sector);
368 let bn = sector_fract * 2.0 * an - an;
369
370 // Transform point to first sector
371 let radius = length(p_in);
372 var p = vec2<f32>(radius * cos(bn), abs(radius * sin(bn)));
373
374 // Outer tip at (r, 0), inner valley at angle an
375 let inner_r = r * inner_ratio;
376
377 // Vector from outer tip to inner valley
378 let tip = vec2<f32>(r, 0.0);
379 let valley = vec2<f32>(inner_r * cos(an), inner_r * sin(an));
380
381 // Distance to the edge line segment
382 p = p - tip;
383 let edge = valley - tip;
384 let h = clamp(dot(p, edge) / dot(edge, edge), 0.0, 1.0);
385 p = p - edge * h;
386
387 // Sign: negative inside, positive outside
388 // p.x < 0 means we're past the edge toward center (inside)
389 return length(p) * sign(p.x);
390}
391
392// ---- Transform Operations ----
393
394fn rotate_rad(p: vec2<f32>, angle: f32) -> vec2<f32> {
395 let c = cos(angle);
396 let s = sin(angle);
397 return vec2<f32>(p.x * c - p.y * s, p.x * s + p.y * c);
398}
399
400fn op_symmetry_x(p: vec2<f32>) -> vec2<f32> {
401 return vec2<f32>(abs(p.x), p.y);
402}
403
404// ---- Composition Operations ----
405
406fn op_union(d1: f32, d2: f32) -> f32 {
407 return min(d1, d2);
408}
409
410fn op_subtract(d1: f32, d2: f32) -> f32 {
411 return max(d1, -d2);
412}
413
414fn op_intersect(d1: f32, d2: f32) -> f32 {
415 return max(d1, d2);
416}
417
418fn op_smooth_union(d1: f32, d2: f32, k: f32) -> f32 {
419 let h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
420 return mix(d2, d1, h) - k * h * (1.0 - h);
421}
422
423fn op_smooth_subtract(d1: f32, d2: f32, k: f32) -> f32 {
424 let h = clamp(0.5 - 0.5 * (d1 + d2) / k, 0.0, 1.0);
425 return mix(d1, -d2, h) + k * h * (1.0 - h);
426}
427
428fn op_smooth_intersect(d1: f32, d2: f32, k: f32) -> f32 {
429 let h = clamp(0.5 - 0.5 * (d2 - d1) / k, 0.0, 1.0);
430 return mix(d2, d1, h) + k * h * (1.0 - h);
431}
432
433fn op_round(d: f32, r: f32) -> f32 {
434 return d - r;
435}
436
437fn op_annular(d: f32, r: f32) -> f32 {
438 return abs(d) - r;
439}
440
441fn op_repeat(p: vec2<f32>, spacing: vec2<f32>) -> vec2<f32> {
442 return p - spacing * round(p / spacing);
443}
444
445fn op_translate(p: vec2<f32>, offset: vec2<f32>) -> vec2<f32> {
446 return p - offset;
447}
448
449fn op_rotate(p: vec2<f32>, angle: f32) -> vec2<f32> {
450 let c = cos(angle);
451 let s = sin(angle);
452 return vec2<f32>(p.x * c + p.y * s, -p.x * s + p.y * c);
453}
454
455fn op_scale(p: vec2<f32>, s: f32) -> vec2<f32> {
456 return p / s;
457}
458"#;
459
460pub fn generate_fill_wgsl(fill: &SdfFill) -> String {
471 match fill {
472 SdfFill::Solid { color } => {
473 format!(
474 r#" // Solid fill with adaptive AA (fwidth-based)
475 let fill_color = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
476 let aa_width = fwidth(d) * 0.5;
477 let aa = 1.0 - smoothstep(-aa_width, aa_width, d);
478 out_color = vec4<f32>(fill_color.rgb, fill_color.a * aa * in_opacity);"#,
479 color[0], color[1], color[2], color[3]
480 )
481 }
482 SdfFill::Outline { color, thickness } => {
483 format!(
484 r#" // Outline fill with adaptive AA (fwidth-based)
485 let outline_color = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
486 let half_t = {:.6};
487 let aa_width = fwidth(d) * 0.5;
488 let aa_outer = 1.0 - smoothstep(-aa_width, aa_width, abs(d) - half_t);
489 out_color = vec4<f32>(outline_color.rgb, outline_color.a * aa_outer * in_opacity);"#,
490 color[0], color[1], color[2], color[3], thickness
491 )
492 }
493 SdfFill::SolidWithOutline { fill, outline, thickness } => {
494 format!(
495 r#" // Solid + outline fill with adaptive AA (fwidth-based)
496 let fill_color = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
497 let outline_color = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
498 let outline_t = {:.6};
499 let aa_width = fwidth(d) * 0.5;
500 let aa_fill = 1.0 - smoothstep(-aa_width, aa_width, d);
501 let aa_outline = 1.0 - smoothstep(-aa_width, aa_width, abs(d) - outline_t);
502 // Inside the shape: fill color. On the edge: outline color.
503 let is_inside = smoothstep(aa_width, -aa_width, d + outline_t);
504 let base = mix(outline_color, fill_color, is_inside);
505 let alpha = max(aa_fill, aa_outline);
506 out_color = vec4<f32>(base.rgb, base.a * alpha * in_opacity);"#,
507 fill[0], fill[1], fill[2], fill[3],
508 outline[0], outline[1], outline[2], outline[3],
509 thickness
510 )
511 }
512 SdfFill::Gradient { from, to, angle, scale } => {
513 format!(
514 r#" // Gradient fill with adaptive AA (fwidth-based)
515 let grad_from = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
516 let grad_to = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
517 let grad_angle = {:.6};
518 let grad_scale = {:.6};
519 let grad_dir = vec2<f32>(cos(grad_angle), sin(grad_angle));
520 // Map local-space p through the gradient direction, normalized to 0..1
521 // Scale > 1 makes the gradient span a smaller region (tighter fit to shape)
522 let grad_t = clamp(dot(p / in_bounds, grad_dir) * grad_scale * 0.5 + 0.5, 0.0, 1.0);
523 let grad_color = mix(grad_from, grad_to, grad_t);
524 let aa_width = fwidth(d) * 0.5;
525 let aa = 1.0 - smoothstep(-aa_width, aa_width, d);
526 out_color = vec4<f32>(grad_color.rgb, grad_color.a * aa * in_opacity);"#,
527 from[0], from[1], from[2], from[3],
528 to[0], to[1], to[2], to[3],
529 angle,
530 scale
531 )
532 }
533 SdfFill::Glow { color, intensity } => {
534 format!(
535 r#" // Glow fill
536 let glow_color = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});
537 let glow_intensity = {:.6};
538 // Exponential falloff outside the shape; full brightness inside
539 let glow = exp(-max(d, 0.0) * glow_intensity);
540 out_color = vec4<f32>(glow_color.rgb, glow_color.a * glow * in_opacity);"#,
541 color[0], color[1], color[2], color[3],
542 intensity
543 )
544 }
545 SdfFill::CosinePalette { a, b, c, d: d_param } => {
546 format!(
547 r#" // Cosine palette fill with adaptive AA (fwidth-based)
548 let pal_a = vec3<f32>({:.6}, {:.6}, {:.6});
549 let pal_b = vec3<f32>({:.6}, {:.6}, {:.6});
550 let pal_c = vec3<f32>({:.6}, {:.6}, {:.6});
551 let pal_d = vec3<f32>({:.6}, {:.6}, {:.6});
552 // t derived from distance, normalized by bounds
553 let pal_t = d / in_bounds;
554 let pal_color = pal_a + pal_b * cos(6.283185 * (pal_c * pal_t + pal_d));
555 let aa_width = fwidth(d) * 0.5;
556 let aa = 1.0 - smoothstep(-aa_width, aa_width, d);
557 out_color = vec4<f32>(clamp(pal_color, vec3<f32>(0.0), vec3<f32>(1.0)), aa * in_opacity);"#,
558 a[0], a[1], a[2],
559 b[0], b[1], b[2],
560 c[0], c[1], c[2],
561 d_param[0], d_param[1], d_param[2]
562 )
563 }
564 }
565}
566
567pub fn generate_sdf_shader(sdf_expr: &str, fill: &SdfFill) -> String {
579 let fill_code = generate_fill_wgsl(fill);
580
581 format!(
582 r#"// Auto-generated SDF shader
583// Expression: {sdf_expr}
584
585{SDF_PRIMITIVES_WGSL}
586
587// ---- Bindings ----
588
589struct CameraUniform {{
590 view_proj: mat4x4<f32>,
591}};
592
593@group(0) @binding(0)
594var<uniform> camera: CameraUniform;
595
596struct TimeUniform {{
597 time: f32,
598}};
599
600@group(1) @binding(0)
601var<uniform> time_data: TimeUniform;
602
603// ---- Vertex stage ----
604
605struct VertexInput {{
606 @location(0) position: vec2<f32>,
607 @location(1) uv: vec2<f32>,
608}};
609
610struct InstanceInput {{
611 @location(2) inst_position: vec2<f32>,
612 @location(3) inst_bounds: f32,
613 @location(4) inst_rotation: f32,
614 @location(5) inst_scale: f32,
615 @location(6) inst_opacity: f32,
616 @location(7) inst_pad: vec2<f32>,
617 @location(8) inst_color: vec4<f32>,
618}};
619
620struct VertexOutput {{
621 @builtin(position) clip_position: vec4<f32>,
622 @location(0) local_pos: vec2<f32>,
623 @location(1) v_opacity: f32,
624 @location(2) v_bounds: f32,
625 @location(3) v_color: vec4<f32>,
626 @location(4) v_scale: f32,
627}};
628
629@vertex
630fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {{
631 var out: VertexOutput;
632
633 let scaled_bounds = instance.inst_bounds * instance.inst_scale;
634
635 // Quad vertex in local space: vertex.position is in [-1, 1]
636 // Scale the quad for visual scaling
637 var pos = vertex.position * scaled_bounds;
638
639 // Apply instance rotation around center
640 let cos_r = cos(instance.inst_rotation);
641 let sin_r = sin(instance.inst_rotation);
642 let rotated = vec2<f32>(
643 pos.x * cos_r - pos.y * sin_r,
644 pos.x * sin_r + pos.y * cos_r,
645 );
646
647 // Translate to world position
648 let world_xy = rotated + instance.inst_position;
649 let world = vec4<f32>(world_xy.x, world_xy.y, 0.0, 1.0);
650 out.clip_position = camera.view_proj * world;
651
652 // Pass local-space coordinate to fragment WITHOUT scale
653 // This ensures SDF is evaluated in original coordinate space
654 // Flip Y to match Arcane's screen coordinate system (Y=0 at top, Y increases down)
655 out.local_pos = vec2<f32>(vertex.position.x, -vertex.position.y) * instance.inst_bounds;
656 out.v_opacity = instance.inst_opacity;
657 out.v_bounds = instance.inst_bounds;
658 out.v_scale = instance.inst_scale;
659 out.v_color = instance.inst_color;
660
661 return out;
662}}
663
664// ---- Fragment stage ----
665
666@fragment
667fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {{
668 let p = in.local_pos;
669 let in_bounds = in.v_bounds;
670 let in_opacity = in.v_opacity;
671 let in_color = in.v_color;
672 let in_scale = in.v_scale;
673 let time = time_data.time;
674
675 // Evaluate the SDF expression, then scale the distance for proper anti-aliasing
676 let d = {sdf_expr} * in_scale;
677
678 // Apply fill
679 var out_color: vec4<f32>;
680{fill_code}
681
682 return out_color;
683}}
684"#,
685 sdf_expr = sdf_expr,
686 SDF_PRIMITIVES_WGSL = SDF_PRIMITIVES_WGSL,
687 fill_code = fill_code,
688 )
689}
690
691#[repr(C)]
697#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
698struct CameraUniform {
699 view_proj: [f32; 16],
700}
701
702pub struct SdfPipelineStore {
704 pipelines: HashMap<u64, wgpu::RenderPipeline>,
706 pipeline_layout: wgpu::PipelineLayout,
708 #[allow(dead_code)]
710 camera_bind_group_layout: wgpu::BindGroupLayout,
711 camera_buffer: wgpu::Buffer,
713 camera_bind_group: wgpu::BindGroup,
715 #[allow(dead_code)]
718 time_bind_group_layout: wgpu::BindGroupLayout,
719 time_buffer: wgpu::Buffer,
721 time_bind_group: wgpu::BindGroup,
723 vertex_buffer: wgpu::Buffer,
725 index_buffer: wgpu::Buffer,
727 surface_format: wgpu::TextureFormat,
729}
730
731impl SdfPipelineStore {
732 pub fn new(gpu: &GpuContext) -> Self {
734 Self::new_internal(&gpu.device, gpu.config.format)
735 }
736
737 pub fn new_headless(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
739 Self::new_internal(device, format)
740 }
741
742 fn new_internal(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
743 let camera_bind_group_layout =
745 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
746 label: Some("sdf_camera_bgl"),
747 entries: &[wgpu::BindGroupLayoutEntry {
748 binding: 0,
749 visibility: wgpu::ShaderStages::VERTEX,
750 ty: wgpu::BindingType::Buffer {
751 ty: wgpu::BufferBindingType::Uniform,
752 has_dynamic_offset: false,
753 min_binding_size: None,
754 },
755 count: None,
756 }],
757 });
758
759 let time_bind_group_layout =
761 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
762 label: Some("sdf_time_bgl"),
763 entries: &[wgpu::BindGroupLayoutEntry {
764 binding: 0,
765 visibility: wgpu::ShaderStages::FRAGMENT,
766 ty: wgpu::BindingType::Buffer {
767 ty: wgpu::BufferBindingType::Uniform,
768 has_dynamic_offset: false,
769 min_binding_size: None,
770 },
771 count: None,
772 }],
773 });
774
775 let pipeline_layout =
776 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
777 label: Some("sdf_pipeline_layout"),
778 bind_group_layouts: &[&camera_bind_group_layout, &time_bind_group_layout],
779 push_constant_ranges: &[],
780 });
781
782 let time_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
784 label: Some("sdf_time_buffer"),
785 contents: bytemuck::cast_slice(&[0.0f32]),
786 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
787 });
788
789 let time_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
790 label: Some("sdf_time_bind_group"),
791 layout: &time_bind_group_layout,
792 entries: &[wgpu::BindGroupEntry {
793 binding: 0,
794 resource: time_buffer.as_entire_binding(),
795 }],
796 });
797
798 let camera_uniform = CameraUniform {
800 view_proj: [
801 1.0, 0.0, 0.0, 0.0,
802 0.0, 1.0, 0.0, 0.0,
803 0.0, 0.0, 1.0, 0.0,
804 0.0, 0.0, 0.0, 1.0,
805 ],
806 };
807 let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
808 label: Some("sdf_camera_buffer"),
809 contents: bytemuck::cast_slice(&[camera_uniform]),
810 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
811 });
812
813 let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
814 label: Some("sdf_camera_bind_group"),
815 layout: &camera_bind_group_layout,
816 entries: &[wgpu::BindGroupEntry {
817 binding: 0,
818 resource: camera_buffer.as_entire_binding(),
819 }],
820 });
821
822 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
824 label: Some("sdf_quad_vertex_buffer"),
825 contents: bytemuck::cast_slice(SDF_QUAD_VERTICES),
826 usage: wgpu::BufferUsages::VERTEX,
827 });
828
829 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
830 label: Some("sdf_quad_index_buffer"),
831 contents: bytemuck::cast_slice(SDF_QUAD_INDICES),
832 usage: wgpu::BufferUsages::INDEX,
833 });
834
835 Self {
836 pipelines: HashMap::new(),
837 pipeline_layout,
838 camera_bind_group_layout,
839 camera_buffer,
840 camera_bind_group,
841 time_bind_group_layout,
842 time_buffer,
843 time_bind_group,
844 vertex_buffer,
845 index_buffer,
846 surface_format,
847 }
848 }
849
850 pub fn prepare(&self, queue: &wgpu::Queue, camera: &super::Camera2D, time: f32) {
852 let camera_uniform = CameraUniform {
853 view_proj: camera.view_proj(),
854 };
855 queue.write_buffer(
856 &self.camera_buffer,
857 0,
858 bytemuck::cast_slice(&[camera_uniform]),
859 );
860 queue.write_buffer(&self.time_buffer, 0, bytemuck::cast_slice(&[time]));
861 }
862
863 pub fn set_time(&self, queue: &wgpu::Queue, time: f32) {
865 queue.write_buffer(&self.time_buffer, 0, bytemuck::cast_slice(&[time]));
866 }
867
868 pub fn get_or_create_pipeline(
871 &mut self,
872 device: &wgpu::Device,
873 sdf_expr: &str,
874 fill: &SdfFill,
875 ) -> u64 {
876 let key = compute_pipeline_key(sdf_expr, fill);
877 if self.pipelines.contains_key(&key) {
878 return key;
879 }
880
881 let wgsl = generate_sdf_shader(sdf_expr, fill);
882 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
883 label: Some("sdf_shader"),
884 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
885 });
886
887 let vertex_layout = wgpu::VertexBufferLayout {
889 array_stride: std::mem::size_of::<SdfQuadVertex>() as wgpu::BufferAddress,
890 step_mode: wgpu::VertexStepMode::Vertex,
891 attributes: &[
892 wgpu::VertexAttribute {
893 offset: 0,
894 shader_location: 0,
895 format: wgpu::VertexFormat::Float32x2, },
897 wgpu::VertexAttribute {
898 offset: 8,
899 shader_location: 1,
900 format: wgpu::VertexFormat::Float32x2, },
902 ],
903 };
904
905 let instance_layout = wgpu::VertexBufferLayout {
907 array_stride: std::mem::size_of::<SdfInstance>() as wgpu::BufferAddress,
908 step_mode: wgpu::VertexStepMode::Instance,
909 attributes: &[
910 wgpu::VertexAttribute {
911 offset: 0,
912 shader_location: 2,
913 format: wgpu::VertexFormat::Float32x2, },
915 wgpu::VertexAttribute {
916 offset: 8,
917 shader_location: 3,
918 format: wgpu::VertexFormat::Float32, },
920 wgpu::VertexAttribute {
921 offset: 12,
922 shader_location: 4,
923 format: wgpu::VertexFormat::Float32, },
925 wgpu::VertexAttribute {
926 offset: 16,
927 shader_location: 5,
928 format: wgpu::VertexFormat::Float32, },
930 wgpu::VertexAttribute {
931 offset: 20,
932 shader_location: 6,
933 format: wgpu::VertexFormat::Float32, },
935 wgpu::VertexAttribute {
936 offset: 24,
937 shader_location: 7,
938 format: wgpu::VertexFormat::Float32x2, },
940 wgpu::VertexAttribute {
941 offset: 32,
942 shader_location: 8,
943 format: wgpu::VertexFormat::Float32x4, },
945 ],
946 };
947
948 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
949 label: Some("sdf_render_pipeline"),
950 layout: Some(&self.pipeline_layout),
951 vertex: wgpu::VertexState {
952 module: &shader,
953 entry_point: Some("vs_main"),
954 buffers: &[vertex_layout, instance_layout],
955 compilation_options: Default::default(),
956 },
957 fragment: Some(wgpu::FragmentState {
958 module: &shader,
959 entry_point: Some("fs_main"),
960 targets: &[Some(wgpu::ColorTargetState {
961 format: self.surface_format,
962 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
963 write_mask: wgpu::ColorWrites::ALL,
964 })],
965 compilation_options: Default::default(),
966 }),
967 primitive: wgpu::PrimitiveState {
968 topology: wgpu::PrimitiveTopology::TriangleList,
969 strip_index_format: None,
970 front_face: wgpu::FrontFace::Ccw,
971 cull_mode: None,
972 polygon_mode: wgpu::PolygonMode::Fill,
973 unclipped_depth: false,
974 conservative: false,
975 },
976 depth_stencil: None,
977 multisample: wgpu::MultisampleState::default(),
978 multiview: None,
979 cache: None,
980 });
981
982 self.pipelines.insert(key, pipeline);
983 key
984 }
985
986 pub fn camera_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
988 &self.camera_bind_group_layout
989 }
990
991 pub fn render(
999 &mut self,
1000 device: &wgpu::Device,
1001 encoder: &mut wgpu::CommandEncoder,
1002 target: &wgpu::TextureView,
1003 commands: &[SdfCommand],
1004 clear_color: Option<wgpu::Color>,
1005 ) {
1006 if commands.is_empty() {
1007 return;
1008 }
1009
1010 for cmd in commands {
1012 self.get_or_create_pipeline(device, &cmd.sdf_expr, &cmd.fill);
1013 }
1014
1015 let load_op = match clear_color {
1016 Some(color) => wgpu::LoadOp::Clear(color),
1017 None => wgpu::LoadOp::Load,
1018 };
1019
1020 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1021 label: Some("sdf_render_pass"),
1022 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1023 view: target,
1024 resolve_target: None,
1025 ops: wgpu::Operations {
1026 load: load_op,
1027 store: wgpu::StoreOp::Store,
1028 },
1029 })],
1030 depth_stencil_attachment: None,
1031 timestamp_writes: None,
1032 occlusion_query_set: None,
1033 });
1034
1035 render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
1036 render_pass.set_bind_group(1, &self.time_bind_group, &[]);
1037 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1038 render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
1039
1040 let mut i = 0;
1043 while i < commands.len() {
1044 let key = compute_pipeline_key(&commands[i].sdf_expr, &commands[i].fill);
1045 let batch_start = i;
1046
1047 while i < commands.len()
1049 && compute_pipeline_key(&commands[i].sdf_expr, &commands[i].fill) == key
1050 {
1051 i += 1;
1052 }
1053
1054 let batch = &commands[batch_start..i];
1055 let pipeline = match self.pipelines.get(&key) {
1056 Some(p) => p,
1057 None => continue, };
1059
1060 let instances: Vec<SdfInstance> = batch
1062 .iter()
1063 .map(|cmd| {
1064 let primary_color = primary_color_from_fill(&cmd.fill);
1065 SdfInstance {
1066 position: [cmd.x, cmd.y],
1067 bounds: cmd.bounds,
1068 rotation: cmd.rotation,
1069 scale: cmd.scale,
1070 opacity: cmd.opacity,
1071 _pad: [0.0; 2],
1072 color: primary_color,
1073 }
1074 })
1075 .collect();
1076
1077 let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1078 label: Some("sdf_instance_buffer"),
1079 contents: bytemuck::cast_slice(&instances),
1080 usage: wgpu::BufferUsages::VERTEX,
1081 });
1082
1083 render_pass.set_pipeline(pipeline);
1084 render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
1085 render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
1086 }
1087 }
1088
1089 pub fn pipeline_count(&self) -> usize {
1091 self.pipelines.len()
1092 }
1093
1094 pub fn clear(&mut self) {
1096 self.pipelines.clear();
1097 }
1098}
1099
1100fn primary_color_from_fill(fill: &SdfFill) -> [f32; 4] {
1104 match fill {
1105 SdfFill::Solid { color } => *color,
1106 SdfFill::Outline { color, .. } => *color,
1107 SdfFill::SolidWithOutline { fill, .. } => *fill,
1108 SdfFill::Gradient { from, .. } => *from,
1109 SdfFill::Glow { color, .. } => *color,
1110 SdfFill::CosinePalette { a, .. } => [a[0], a[1], a[2], 1.0],
1111 }
1112}
1113
1114#[cfg(test)]
1119mod tests {
1120 use super::*;
1121
1122 #[test]
1123 fn sdf_instance_is_48_bytes() {
1124 assert_eq!(std::mem::size_of::<SdfInstance>(), 48);
1127 }
1128
1129 #[test]
1130 fn sdf_quad_vertex_is_16_bytes() {
1131 assert_eq!(std::mem::size_of::<SdfQuadVertex>(), 16);
1133 }
1134
1135 #[test]
1136 fn generate_shader_contains_expression() {
1137 let shader = generate_sdf_shader(
1138 "sd_circle(p, 50.0)",
1139 &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] },
1140 );
1141 assert!(shader.contains("sd_circle(p, 50.0)"), "shader should contain the SDF expression");
1142 assert!(shader.contains("fn sd_circle"), "shader should contain primitive definitions");
1143 assert!(shader.contains("fn vs_main"), "shader should contain vertex entry point");
1144 assert!(shader.contains("fn fs_main"), "shader should contain fragment entry point");
1145 }
1146
1147 #[test]
1148 fn generate_shader_includes_all_primitives() {
1149 let shader = generate_sdf_shader(
1150 "sd_circle(p, 10.0)",
1151 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1152 );
1153 assert!(shader.contains("fn sd_circle"));
1154 assert!(shader.contains("fn sd_box"));
1155 assert!(shader.contains("fn sd_rounded_box"));
1156 assert!(shader.contains("fn sd_segment"));
1157 assert!(shader.contains("fn sd_capsule"));
1158 assert!(shader.contains("fn sd_ring"));
1159 assert!(shader.contains("fn sd_ellipse"));
1160 assert!(shader.contains("fn sd_hexagon"));
1161 assert!(shader.contains("fn sd_star5"));
1162 assert!(shader.contains("fn sd_cross"));
1163 }
1164
1165 #[test]
1166 fn generate_shader_includes_all_ops() {
1167 let shader = generate_sdf_shader(
1168 "op_union(sd_circle(p, 10.0), sd_box(p, vec2(5.0, 5.0)))",
1169 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1170 );
1171 assert!(shader.contains("fn op_union"));
1172 assert!(shader.contains("fn op_subtract"));
1173 assert!(shader.contains("fn op_intersect"));
1174 assert!(shader.contains("fn op_smooth_union"));
1175 assert!(shader.contains("fn op_smooth_subtract"));
1176 assert!(shader.contains("fn op_smooth_intersect"));
1177 assert!(shader.contains("fn op_round"));
1178 assert!(shader.contains("fn op_annular"));
1179 assert!(shader.contains("fn op_repeat"));
1180 assert!(shader.contains("fn op_translate"));
1181 assert!(shader.contains("fn op_rotate"));
1182 assert!(shader.contains("fn op_scale"));
1183 }
1184
1185 #[test]
1186 fn generate_shader_includes_camera_and_time_bindings() {
1187 let shader = generate_sdf_shader(
1188 "sd_circle(p, 1.0)",
1189 &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] },
1190 );
1191 assert!(shader.contains("struct CameraUniform"));
1192 assert!(shader.contains("@group(0) @binding(0)"));
1193 assert!(shader.contains("struct TimeUniform"));
1194 assert!(shader.contains("@group(1) @binding(0)"));
1195 assert!(shader.contains("var<uniform> camera: CameraUniform"));
1196 assert!(shader.contains("var<uniform> time_data: TimeUniform"));
1197 }
1198
1199 #[test]
1200 fn fill_solid_generates_smoothstep_aa() {
1201 let code = generate_fill_wgsl(&SdfFill::Solid {
1202 color: [1.0, 0.0, 0.0, 1.0],
1203 });
1204 assert!(code.contains("smoothstep"), "solid fill should use smoothstep for AA");
1205 assert!(code.contains("1.000000, 0.000000, 0.000000, 1.000000"));
1206 }
1207
1208 #[test]
1209 fn fill_outline_generates_abs_distance() {
1210 let code = generate_fill_wgsl(&SdfFill::Outline {
1211 color: [0.0, 1.0, 0.0, 1.0],
1212 thickness: 2.0,
1213 });
1214 assert!(code.contains("abs(d)"), "outline fill should use abs(d)");
1215 assert!(code.contains("2.000000"), "outline fill should include thickness");
1216 }
1217
1218 #[test]
1219 fn fill_glow_generates_exp_falloff() {
1220 let code = generate_fill_wgsl(&SdfFill::Glow {
1221 color: [0.0, 0.5, 1.0, 1.0],
1222 intensity: 3.0,
1223 });
1224 assert!(code.contains("exp("), "glow fill should use exponential falloff");
1225 assert!(code.contains("3.000000"), "glow fill should include intensity");
1226 }
1227
1228 #[test]
1229 fn fill_gradient_generates_direction_mapping() {
1230 let code = generate_fill_wgsl(&SdfFill::Gradient {
1231 from: [1.0, 0.0, 0.0, 1.0],
1232 to: [0.0, 0.0, 1.0, 1.0],
1233 angle: 1.5708,
1234 scale: 1.0,
1235 });
1236 assert!(code.contains("grad_dir"), "gradient fill should compute direction");
1237 assert!(code.contains("mix("), "gradient fill should interpolate colors");
1238 }
1239
1240 #[test]
1241 fn fill_cosine_palette_generates_cosine_function() {
1242 let code = generate_fill_wgsl(&SdfFill::CosinePalette {
1243 a: [0.5, 0.5, 0.5],
1244 b: [0.5, 0.5, 0.5],
1245 c: [1.0, 1.0, 1.0],
1246 d: [0.0, 0.33, 0.67],
1247 });
1248 assert!(code.contains("cos("), "cosine palette should use cos()");
1249 assert!(code.contains("6.283185"), "cosine palette should use 2*pi");
1250 }
1251
1252 #[test]
1253 fn fill_solid_with_outline_generates_both_colors() {
1254 let code = generate_fill_wgsl(&SdfFill::SolidWithOutline {
1255 fill: [1.0, 0.0, 0.0, 1.0],
1256 outline: [1.0, 1.0, 1.0, 1.0],
1257 thickness: 1.5,
1258 });
1259 assert!(code.contains("fill_color"), "should have fill color");
1260 assert!(code.contains("outline_color"), "should have outline color");
1261 assert!(code.contains("1.500000"), "should include thickness");
1262 }
1263
1264 #[test]
1265 fn pipeline_key_differs_for_different_expressions() {
1266 let fill = SdfFill::Solid { color: [1.0; 4] };
1267 let k1 = compute_pipeline_key("sd_circle(p, 10.0)", &fill);
1268 let k2 = compute_pipeline_key("sd_box(p, vec2(5.0, 5.0))", &fill);
1269 assert_ne!(k1, k2, "different expressions should produce different keys");
1270 }
1271
1272 #[test]
1273 fn pipeline_key_differs_for_different_fills() {
1274 let expr = "sd_circle(p, 10.0)";
1275 let k1 = compute_pipeline_key(expr, &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] });
1276 let k2 = compute_pipeline_key(expr, &SdfFill::Solid { color: [0.0, 1.0, 0.0, 1.0] });
1277 assert_ne!(k1, k2, "different fills should produce different keys");
1278 }
1279
1280 #[test]
1281 fn pipeline_key_is_deterministic() {
1282 let fill = SdfFill::Glow { color: [0.0, 0.5, 1.0, 1.0], intensity: 3.0 };
1283 let k1 = compute_pipeline_key("sd_circle(p, 10.0)", &fill);
1284 let k2 = compute_pipeline_key("sd_circle(p, 10.0)", &fill);
1285 assert_eq!(k1, k2, "same input should produce the same key");
1286 }
1287
1288 #[test]
1289 fn compute_fill_hash_varies_by_discriminant() {
1290 let h1 = compute_fill_hash(&SdfFill::Solid { color: [1.0; 4] });
1291 let h2 = compute_fill_hash(&SdfFill::Glow { color: [1.0; 4], intensity: 1.0 });
1292 assert_ne!(h1, h2, "different fill types should hash differently");
1293 }
1294
1295 #[test]
1296 fn primary_color_extraction() {
1297 assert_eq!(
1298 primary_color_from_fill(&SdfFill::Solid { color: [0.1, 0.2, 0.3, 0.4] }),
1299 [0.1, 0.2, 0.3, 0.4],
1300 );
1301 assert_eq!(
1302 primary_color_from_fill(&SdfFill::Outline { color: [0.5, 0.6, 0.7, 0.8], thickness: 1.0 }),
1303 [0.5, 0.6, 0.7, 0.8],
1304 );
1305 assert_eq!(
1306 primary_color_from_fill(&SdfFill::SolidWithOutline {
1307 fill: [0.1, 0.2, 0.3, 0.4],
1308 outline: [0.9, 0.9, 0.9, 1.0],
1309 thickness: 2.0,
1310 }),
1311 [0.1, 0.2, 0.3, 0.4],
1312 );
1313 assert_eq!(
1314 primary_color_from_fill(&SdfFill::Gradient {
1315 from: [1.0, 0.0, 0.0, 1.0],
1316 to: [0.0, 0.0, 1.0, 1.0],
1317 angle: 0.0,
1318 scale: 1.0,
1319 }),
1320 [1.0, 0.0, 0.0, 1.0],
1321 );
1322 assert_eq!(
1323 primary_color_from_fill(&SdfFill::Glow { color: [0.0, 1.0, 0.0, 1.0], intensity: 5.0 }),
1324 [0.0, 1.0, 0.0, 1.0],
1325 );
1326 let cosine = primary_color_from_fill(&SdfFill::CosinePalette {
1327 a: [0.5, 0.5, 0.5],
1328 b: [0.5, 0.5, 0.5],
1329 c: [1.0, 1.0, 1.0],
1330 d: [0.0, 0.33, 0.67],
1331 });
1332 assert_eq!(cosine, [0.5, 0.5, 0.5, 1.0]);
1333 }
1334
1335 #[test]
1336 fn generated_shader_has_valid_structure() {
1337 let shader = generate_sdf_shader(
1339 "op_smooth_union(sd_circle(p, 30.0), sd_box(p, vec2<f32>(20.0, 20.0)), 5.0)",
1340 &SdfFill::SolidWithOutline {
1341 fill: [0.2, 0.4, 0.8, 1.0],
1342 outline: [1.0, 1.0, 1.0, 1.0],
1343 thickness: 2.0,
1344 },
1345 );
1346
1347 let prim_pos = shader.find("fn sd_circle").unwrap();
1349 let binding_pos = shader.find("struct CameraUniform").unwrap();
1350 let vs_pos = shader.find("fn vs_main").unwrap();
1351 let fs_pos = shader.find("fn fs_main").unwrap();
1352
1353 assert!(prim_pos < binding_pos, "primitives should come before bindings");
1354 assert!(binding_pos < vs_pos, "bindings should come before vertex shader");
1355 assert!(vs_pos < fs_pos, "vertex shader should come before fragment shader");
1356 }
1357
1358 #[test]
1359 fn sdf_quad_vertices_are_centered() {
1360 assert_eq!(SDF_QUAD_VERTICES[0].position, [-1.0, -1.0]);
1362 assert_eq!(SDF_QUAD_VERTICES[1].position, [1.0, -1.0]);
1363 assert_eq!(SDF_QUAD_VERTICES[2].position, [1.0, 1.0]);
1364 assert_eq!(SDF_QUAD_VERTICES[3].position, [-1.0, 1.0]);
1365 }
1366
1367 #[test]
1368 fn sdf_quad_indices_form_two_triangles() {
1369 assert_eq!(SDF_QUAD_INDICES.len(), 6);
1370 assert_eq!(SDF_QUAD_INDICES, &[0, 1, 2, 0, 2, 3]);
1371 }
1372
1373 fn validate_wgsl(source: &str) -> Result<(), String> {
1384 match naga::front::wgsl::parse_str(source) {
1385 Ok(_module) => Ok(()),
1386 Err(e) => Err(format!("{e:?}")),
1387 }
1388 }
1389
1390 #[test]
1393 fn wgsl_circle_compiles() {
1394 let shader = generate_sdf_shader(
1395 "sd_circle(p, 50.0)",
1396 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1397 );
1398 let result = validate_wgsl(&shader);
1399 assert!(result.is_ok(), "Circle shader failed: {}", result.unwrap_err());
1400 }
1401
1402 #[test]
1403 fn wgsl_box_compiles() {
1404 let shader = generate_sdf_shader(
1405 "sd_box(p, vec2<f32>(20.0, 10.0))",
1406 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1407 );
1408 let result = validate_wgsl(&shader);
1409 assert!(result.is_ok(), "Box shader failed: {}", result.unwrap_err());
1410 }
1411
1412 #[test]
1413 fn wgsl_rounded_box_compiles() {
1414 let shader = generate_sdf_shader(
1415 "sd_rounded_box(p, vec2<f32>(20.0, 10.0), vec4<f32>(3.0, 3.0, 3.0, 3.0))",
1416 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1417 );
1418 let result = validate_wgsl(&shader);
1419 assert!(result.is_ok(), "Rounded box shader failed: {}", result.unwrap_err());
1420 }
1421
1422 #[test]
1423 fn wgsl_all_primitives_compile() {
1424 let primitives: &[(&str, &str)] = &[
1427 ("sd_circle", "sd_circle(p, 50.0)"),
1428 ("sd_box", "sd_box(p, vec2<f32>(20.0, 10.0))"),
1429 ("sd_rounded_box", "sd_rounded_box(p, vec2<f32>(20.0, 10.0), vec4<f32>(3.0, 3.0, 3.0, 3.0))"),
1430 ("sd_segment", "sd_segment(p, vec2<f32>(0.0, 0.0), vec2<f32>(20.0, 10.0))"),
1431 ("sd_capsule", "sd_capsule(p, vec2<f32>(-10.0, 0.0), vec2<f32>(10.0, 0.0), 5.0)"),
1432 ("sd_equilateral_triangle", "sd_equilateral_triangle(p, 20.0)"),
1433 ("sd_ring", "sd_ring(p, 20.0, 3.0)"),
1434 ("sd_ellipse", "sd_ellipse(p, vec2<f32>(30.0, 15.0))"),
1435 ("sd_hexagon", "sd_hexagon(p, 20.0)"),
1436 ("sd_star5", "sd_star5(p, 15.0, 0.4)"),
1437 ("sd_cross", "sd_cross(p, vec2<f32>(20.0, 5.0), 2.0)"),
1438 ];
1439 for (name, expr) in primitives {
1440 let shader = generate_sdf_shader(expr, &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] });
1441 let result = validate_wgsl(&shader);
1442 assert!(result.is_ok(), "Primitive '{name}' failed to compile.\nExpression: {expr}\nError: {}", result.unwrap_err());
1443 }
1444 }
1445
1446 #[test]
1449 fn wgsl_composition_operators_compile() {
1450 let exprs: &[(&str, &str)] = &[
1451 ("op_union", "op_union(sd_circle(p, 20.0), sd_box(p, vec2<f32>(15.0, 15.0)))"),
1452 ("op_subtract", "op_subtract(sd_circle(p, 20.0), sd_circle(p - vec2<f32>(10.0, 0.0), 15.0))"),
1453 ("op_intersect", "op_intersect(sd_circle(p, 20.0), sd_box(p, vec2<f32>(15.0, 15.0)))"),
1454 ("op_smooth_union", "op_smooth_union(sd_circle(p, 20.0), sd_box(p - vec2<f32>(15.0, 0.0), vec2<f32>(10.0, 10.0)), 5.0)"),
1455 ("op_smooth_subtract", "op_smooth_subtract(sd_circle(p, 20.0), sd_circle(p - vec2<f32>(5.0, 0.0), 15.0), 3.0)"),
1456 ("op_smooth_intersect", "op_smooth_intersect(sd_circle(p, 25.0), sd_box(p, vec2<f32>(15.0, 15.0)), 4.0)"),
1457 ];
1458 for (name, expr) in exprs {
1459 let shader = generate_sdf_shader(expr, &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] });
1460 let result = validate_wgsl(&shader);
1461 assert!(result.is_ok(), "Composition '{name}' failed.\nExpression: {expr}\nError: {}", result.unwrap_err());
1462 }
1463 }
1464
1465 #[test]
1468 fn wgsl_all_fill_types_compile() {
1469 let expr = "sd_circle(p, 30.0)";
1470 let fills: Vec<(&str, SdfFill)> = vec![
1471 ("Solid", SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] }),
1472 ("Outline", SdfFill::Outline { color: [1.0, 0.0, 0.0, 1.0], thickness: 2.0 }),
1473 ("SolidWithOutline", SdfFill::SolidWithOutline {
1474 fill: [0.0, 0.0, 1.0, 1.0],
1475 outline: [1.0, 1.0, 1.0, 1.0],
1476 thickness: 1.5,
1477 }),
1478 ("Gradient", SdfFill::Gradient {
1479 from: [1.0, 0.0, 0.0, 1.0],
1480 to: [0.0, 0.0, 1.0, 1.0],
1481 angle: 1.5707,
1482 scale: 1.0,
1483 }),
1484 ("Glow", SdfFill::Glow { color: [0.0, 1.0, 0.5, 1.0], intensity: 0.8 }),
1485 ("CosinePalette", SdfFill::CosinePalette {
1486 a: [0.5, 0.5, 0.5],
1487 b: [0.5, 0.5, 0.5],
1488 c: [1.0, 1.0, 1.0],
1489 d: [0.0, 0.33, 0.67],
1490 }),
1491 ];
1492 for (name, fill) in &fills {
1493 let shader = generate_sdf_shader(expr, fill);
1494 let result = validate_wgsl(&shader);
1495 assert!(result.is_ok(), "Fill '{name}' failed to compile.\nError: {}", result.unwrap_err());
1496 }
1497 }
1498
1499 #[test]
1502 fn wgsl_deeply_nested_tree_compiles() {
1503 let expr = concat!(
1505 "op_smooth_union(",
1506 "op_smooth_union(",
1507 "op_smooth_union(",
1508 "sd_rounded_box(p, vec2<f32>(8.0, 30.0), vec4<f32>(2.0, 2.0, 2.0, 2.0)), ",
1509 "sd_circle(p - vec2<f32>(0.0, 25.0), 18.0), ",
1510 "4.0), ",
1511 "sd_circle(p - vec2<f32>(-10.0, 20.0), 14.0), ",
1512 "4.0), ",
1513 "sd_circle(p - vec2<f32>(10.0, 20.0), 14.0), ",
1514 "4.0)",
1515 );
1516 let shader = generate_sdf_shader(expr, &SdfFill::Gradient {
1517 from: [0.35, 0.23, 0.1, 1.0],
1518 to: [0.18, 0.54, 0.3, 1.0],
1519 angle: 1.5707,
1520 scale: 1.0,
1521 });
1522 let result = validate_wgsl(&shader);
1523 assert!(result.is_ok(), "Nested tree failed: {}", result.unwrap_err());
1524 }
1525
1526 #[test]
1527 fn wgsl_subtract_creates_crescent() {
1528 let expr = "op_subtract(sd_circle(p, 25.0), sd_circle(p - vec2<f32>(10.0, 5.0), 22.0))";
1530 let shader = generate_sdf_shader(expr, &SdfFill::Glow {
1531 color: [0.9, 0.9, 0.6, 1.0],
1532 intensity: 0.5,
1533 });
1534 let result = validate_wgsl(&shader);
1535 assert!(result.is_ok(), "Crescent moon failed: {}", result.unwrap_err());
1536 }
1537
1538 #[test]
1541 fn wgsl_time_uniform_in_expression() {
1542 let shader = generate_sdf_shader(
1544 "sd_circle(p, 20.0 + sin(time_data.time) * 5.0)",
1545 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1546 );
1547 let result = validate_wgsl(&shader);
1548 assert!(result.is_ok(), "Time uniform shader failed: {}", result.unwrap_err());
1549 }
1550
1551 #[test]
1552 fn wgsl_repeat_domain_compiles() {
1553 let shader = generate_sdf_shader(
1555 "sd_circle(op_repeat(p, vec2<f32>(40.0, 40.0)), 10.0)",
1556 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1557 );
1558 let result = validate_wgsl(&shader);
1559 assert!(result.is_ok(), "Repeat domain failed: {}", result.unwrap_err());
1560 }
1561
1562 #[test]
1563 fn wgsl_rotate_transform_compiles() {
1564 let shader = generate_sdf_shader(
1566 "sd_box(op_rotate(p, 0.785), vec2<f32>(20.0, 10.0))",
1567 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1568 );
1569 let result = validate_wgsl(&shader);
1570 assert!(result.is_ok(), "Rotate transform failed: {}", result.unwrap_err());
1571 }
1572
1573 #[test]
1574 fn wgsl_translate_transform_compiles() {
1575 let shader = generate_sdf_shader(
1577 "sd_circle(op_translate(p, vec2<f32>(15.0, 10.0)), 12.0)",
1578 &SdfFill::Outline { color: [0.0, 1.0, 0.0, 1.0], thickness: 2.0 },
1579 );
1580 let result = validate_wgsl(&shader);
1581 assert!(result.is_ok(), "Translate transform failed: {}", result.unwrap_err());
1582 }
1583
1584 #[test]
1585 fn wgsl_scale_transform_compiles() {
1586 let shader = generate_sdf_shader(
1588 "sd_star5(op_scale(p, 2.0), 15.0, 0.4) * 2.0",
1589 &SdfFill::Solid { color: [1.0, 0.8, 0.0, 1.0] },
1590 );
1591 let result = validate_wgsl(&shader);
1592 assert!(result.is_ok(), "Scale transform failed: {}", result.unwrap_err());
1593 }
1594
1595 #[test]
1598 fn wgsl_round_modifier_compiles() {
1599 let shader = generate_sdf_shader(
1600 "op_round(sd_box(p, vec2<f32>(20.0, 10.0)), 3.0)",
1601 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1602 );
1603 let result = validate_wgsl(&shader);
1604 assert!(result.is_ok(), "Round modifier failed: {}", result.unwrap_err());
1605 }
1606
1607 #[test]
1608 fn wgsl_annular_modifier_compiles() {
1609 let shader = generate_sdf_shader(
1611 "op_annular(sd_circle(p, 20.0), 2.0)",
1612 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1613 );
1614 let result = validate_wgsl(&shader);
1615 assert!(result.is_ok(), "Annular modifier failed: {}", result.unwrap_err());
1616 }
1617
1618 #[test]
1621 fn wgsl_complex_scene_compiles() {
1622 let expr = concat!(
1625 "op_union(",
1626 "sd_circle(op_repeat(p, vec2<f32>(50.0, 50.0)), 8.0), ",
1627 "sd_cross(op_rotate(p, 0.785), vec2<f32>(30.0, 6.0), 2.0)",
1628 ")",
1629 );
1630 let shader = generate_sdf_shader(expr, &SdfFill::CosinePalette {
1631 a: [0.5, 0.5, 0.5],
1632 b: [0.5, 0.5, 0.5],
1633 c: [1.0, 1.0, 1.0],
1634 d: [0.0, 0.33, 0.67],
1635 });
1636 let result = validate_wgsl(&shader);
1637 assert!(result.is_ok(), "Complex scene failed: {}", result.unwrap_err());
1638 }
1639
1640 #[test]
1641 fn wgsl_animated_pulsing_ring() {
1642 let shader = generate_sdf_shader(
1644 "sd_ring(p, 25.0 + sin(time_data.time * 2.0) * 5.0, 3.0)",
1645 &SdfFill::Glow { color: [0.2, 0.8, 1.0, 1.0], intensity: 1.5 },
1646 );
1647 let result = validate_wgsl(&shader);
1648 assert!(result.is_ok(), "Animated ring failed: {}", result.unwrap_err());
1649 }
1650
1651 #[test]
1652 fn wgsl_three_way_smooth_union() {
1653 let expr = concat!(
1655 "op_smooth_union(",
1656 "op_smooth_union(",
1657 "sd_circle(p - vec2<f32>(-15.0, 0.0), 12.0), ",
1658 "sd_circle(p - vec2<f32>(15.0, 0.0), 12.0), ",
1659 "6.0), ",
1660 "sd_circle(p - vec2<f32>(0.0, 15.0), 12.0), ",
1661 "6.0)",
1662 );
1663 let shader = generate_sdf_shader(expr, &SdfFill::SolidWithOutline {
1664 fill: [0.3, 0.3, 0.8, 1.0],
1665 outline: [1.0, 1.0, 1.0, 1.0],
1666 thickness: 1.0,
1667 });
1668 let result = validate_wgsl(&shader);
1669 assert!(result.is_ok(), "Three-way smooth union failed: {}", result.unwrap_err());
1670 }
1671}