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: higher intensity = tighter glow
539 // Inside shape (d <= 0): full opacity
540 // Outside shape (d > 0): exponential decay based on intensity
541 let glow_alpha = exp(-max(d, 0.0) * glow_intensity);
542 out_color = vec4<f32>(glow_color.rgb, glow_color.a * glow_alpha * in_opacity);"#,
543 color[0], color[1], color[2], color[3],
544 intensity
545 )
546 }
547 SdfFill::CosinePalette { a, b, c, d: d_param } => {
548 format!(
549 r#" // Cosine palette fill with adaptive AA (fwidth-based)
550 let pal_a = vec3<f32>({:.6}, {:.6}, {:.6});
551 let pal_b = vec3<f32>({:.6}, {:.6}, {:.6});
552 let pal_c = vec3<f32>({:.6}, {:.6}, {:.6});
553 let pal_d = vec3<f32>({:.6}, {:.6}, {:.6});
554 // t derived from distance, normalized by bounds
555 let pal_t = d / in_bounds;
556 let pal_color = pal_a + pal_b * cos(6.283185 * (pal_c * pal_t + pal_d));
557 let aa_width = fwidth(d) * 0.5;
558 let aa = 1.0 - smoothstep(-aa_width, aa_width, d);
559 out_color = vec4<f32>(clamp(pal_color, vec3<f32>(0.0), vec3<f32>(1.0)), aa * in_opacity);"#,
560 a[0], a[1], a[2],
561 b[0], b[1], b[2],
562 c[0], c[1], c[2],
563 d_param[0], d_param[1], d_param[2]
564 )
565 }
566 }
567}
568
569pub fn generate_sdf_shader(sdf_expr: &str, fill: &SdfFill) -> String {
581 let fill_code = generate_fill_wgsl(fill);
582
583 format!(
584 r#"// Auto-generated SDF shader
585// Expression: {sdf_expr}
586
587{SDF_PRIMITIVES_WGSL}
588
589// ---- Bindings ----
590
591struct CameraUniform {{
592 view_proj: mat4x4<f32>,
593}};
594
595@group(0) @binding(0)
596var<uniform> camera: CameraUniform;
597
598struct TimeUniform {{
599 time: f32,
600}};
601
602@group(1) @binding(0)
603var<uniform> time_data: TimeUniform;
604
605// ---- Vertex stage ----
606
607struct VertexInput {{
608 @location(0) position: vec2<f32>,
609 @location(1) uv: vec2<f32>,
610}};
611
612struct InstanceInput {{
613 @location(2) inst_position: vec2<f32>,
614 @location(3) inst_bounds: f32,
615 @location(4) inst_rotation: f32,
616 @location(5) inst_scale: f32,
617 @location(6) inst_opacity: f32,
618 @location(7) inst_pad: vec2<f32>,
619 @location(8) inst_color: vec4<f32>,
620}};
621
622struct VertexOutput {{
623 @builtin(position) clip_position: vec4<f32>,
624 @location(0) local_pos: vec2<f32>,
625 @location(1) v_opacity: f32,
626 @location(2) v_bounds: f32,
627 @location(3) v_color: vec4<f32>,
628 @location(4) v_scale: f32,
629}};
630
631@vertex
632fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {{
633 var out: VertexOutput;
634
635 let scaled_bounds = instance.inst_bounds * instance.inst_scale;
636
637 // Quad vertex in local space: vertex.position is in [-1, 1]
638 // Scale the quad for visual scaling
639 var pos = vertex.position * scaled_bounds;
640
641 // Apply instance rotation around center
642 let cos_r = cos(instance.inst_rotation);
643 let sin_r = sin(instance.inst_rotation);
644 let rotated = vec2<f32>(
645 pos.x * cos_r - pos.y * sin_r,
646 pos.x * sin_r + pos.y * cos_r,
647 );
648
649 // Translate to world position
650 let world_xy = rotated + instance.inst_position;
651 let world = vec4<f32>(world_xy.x, world_xy.y, 0.0, 1.0);
652 out.clip_position = camera.view_proj * world;
653
654 // Pass local-space coordinate to fragment WITHOUT scale
655 // This ensures SDF is evaluated in original coordinate space
656 // Flip Y to match Arcane's screen coordinate system (Y=0 at top, Y increases down)
657 out.local_pos = vec2<f32>(vertex.position.x, -vertex.position.y) * instance.inst_bounds;
658 out.v_opacity = instance.inst_opacity;
659 out.v_bounds = instance.inst_bounds;
660 out.v_scale = instance.inst_scale;
661 out.v_color = instance.inst_color;
662
663 return out;
664}}
665
666// ---- Fragment stage ----
667
668@fragment
669fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {{
670 let p = in.local_pos;
671 let in_bounds = in.v_bounds;
672 let in_opacity = in.v_opacity;
673 let in_color = in.v_color;
674 let in_scale = in.v_scale;
675 let time = time_data.time;
676
677 // Evaluate the SDF expression, then scale the distance for proper anti-aliasing
678 let d = {sdf_expr} * in_scale;
679
680 // Apply fill
681 var out_color: vec4<f32>;
682{fill_code}
683
684 return out_color;
685}}
686"#,
687 sdf_expr = sdf_expr,
688 SDF_PRIMITIVES_WGSL = SDF_PRIMITIVES_WGSL,
689 fill_code = fill_code,
690 )
691}
692
693#[repr(C)]
699#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
700struct CameraUniform {
701 view_proj: [f32; 16],
702}
703
704pub struct SdfPipelineStore {
706 pipelines: HashMap<u64, wgpu::RenderPipeline>,
708 pipeline_layout: wgpu::PipelineLayout,
710 #[allow(dead_code)]
712 camera_bind_group_layout: wgpu::BindGroupLayout,
713 camera_buffer: wgpu::Buffer,
715 camera_bind_group: wgpu::BindGroup,
717 #[allow(dead_code)]
720 time_bind_group_layout: wgpu::BindGroupLayout,
721 time_buffer: wgpu::Buffer,
723 time_bind_group: wgpu::BindGroup,
725 vertex_buffer: wgpu::Buffer,
727 index_buffer: wgpu::Buffer,
729 surface_format: wgpu::TextureFormat,
731}
732
733impl SdfPipelineStore {
734 pub fn new(gpu: &GpuContext) -> Self {
736 Self::new_internal(&gpu.device, gpu.config.format)
737 }
738
739 pub fn new_headless(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
741 Self::new_internal(device, format)
742 }
743
744 fn new_internal(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
745 let camera_bind_group_layout =
747 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
748 label: Some("sdf_camera_bgl"),
749 entries: &[wgpu::BindGroupLayoutEntry {
750 binding: 0,
751 visibility: wgpu::ShaderStages::VERTEX,
752 ty: wgpu::BindingType::Buffer {
753 ty: wgpu::BufferBindingType::Uniform,
754 has_dynamic_offset: false,
755 min_binding_size: None,
756 },
757 count: None,
758 }],
759 });
760
761 let time_bind_group_layout =
763 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
764 label: Some("sdf_time_bgl"),
765 entries: &[wgpu::BindGroupLayoutEntry {
766 binding: 0,
767 visibility: wgpu::ShaderStages::FRAGMENT,
768 ty: wgpu::BindingType::Buffer {
769 ty: wgpu::BufferBindingType::Uniform,
770 has_dynamic_offset: false,
771 min_binding_size: None,
772 },
773 count: None,
774 }],
775 });
776
777 let pipeline_layout =
778 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
779 label: Some("sdf_pipeline_layout"),
780 bind_group_layouts: &[&camera_bind_group_layout, &time_bind_group_layout],
781 push_constant_ranges: &[],
782 });
783
784 let time_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
786 label: Some("sdf_time_buffer"),
787 contents: bytemuck::cast_slice(&[0.0f32]),
788 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
789 });
790
791 let time_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
792 label: Some("sdf_time_bind_group"),
793 layout: &time_bind_group_layout,
794 entries: &[wgpu::BindGroupEntry {
795 binding: 0,
796 resource: time_buffer.as_entire_binding(),
797 }],
798 });
799
800 let camera_uniform = CameraUniform {
802 view_proj: [
803 1.0, 0.0, 0.0, 0.0,
804 0.0, 1.0, 0.0, 0.0,
805 0.0, 0.0, 1.0, 0.0,
806 0.0, 0.0, 0.0, 1.0,
807 ],
808 };
809 let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
810 label: Some("sdf_camera_buffer"),
811 contents: bytemuck::cast_slice(&[camera_uniform]),
812 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
813 });
814
815 let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
816 label: Some("sdf_camera_bind_group"),
817 layout: &camera_bind_group_layout,
818 entries: &[wgpu::BindGroupEntry {
819 binding: 0,
820 resource: camera_buffer.as_entire_binding(),
821 }],
822 });
823
824 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
826 label: Some("sdf_quad_vertex_buffer"),
827 contents: bytemuck::cast_slice(SDF_QUAD_VERTICES),
828 usage: wgpu::BufferUsages::VERTEX,
829 });
830
831 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
832 label: Some("sdf_quad_index_buffer"),
833 contents: bytemuck::cast_slice(SDF_QUAD_INDICES),
834 usage: wgpu::BufferUsages::INDEX,
835 });
836
837 Self {
838 pipelines: HashMap::new(),
839 pipeline_layout,
840 camera_bind_group_layout,
841 camera_buffer,
842 camera_bind_group,
843 time_bind_group_layout,
844 time_buffer,
845 time_bind_group,
846 vertex_buffer,
847 index_buffer,
848 surface_format,
849 }
850 }
851
852 pub fn prepare(&self, queue: &wgpu::Queue, camera: &super::Camera2D, time: f32) {
854 let camera_uniform = CameraUniform {
855 view_proj: camera.view_proj(),
856 };
857 queue.write_buffer(
858 &self.camera_buffer,
859 0,
860 bytemuck::cast_slice(&[camera_uniform]),
861 );
862 queue.write_buffer(&self.time_buffer, 0, bytemuck::cast_slice(&[time]));
863 }
864
865 pub fn set_time(&self, queue: &wgpu::Queue, time: f32) {
867 queue.write_buffer(&self.time_buffer, 0, bytemuck::cast_slice(&[time]));
868 }
869
870 pub fn get_or_create_pipeline(
873 &mut self,
874 device: &wgpu::Device,
875 sdf_expr: &str,
876 fill: &SdfFill,
877 ) -> u64 {
878 let key = compute_pipeline_key(sdf_expr, fill);
879 if self.pipelines.contains_key(&key) {
880 return key;
881 }
882
883 let wgsl = generate_sdf_shader(sdf_expr, fill);
884 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
885 label: Some("sdf_shader"),
886 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
887 });
888
889 let vertex_layout = wgpu::VertexBufferLayout {
891 array_stride: std::mem::size_of::<SdfQuadVertex>() as wgpu::BufferAddress,
892 step_mode: wgpu::VertexStepMode::Vertex,
893 attributes: &[
894 wgpu::VertexAttribute {
895 offset: 0,
896 shader_location: 0,
897 format: wgpu::VertexFormat::Float32x2, },
899 wgpu::VertexAttribute {
900 offset: 8,
901 shader_location: 1,
902 format: wgpu::VertexFormat::Float32x2, },
904 ],
905 };
906
907 let instance_layout = wgpu::VertexBufferLayout {
909 array_stride: std::mem::size_of::<SdfInstance>() as wgpu::BufferAddress,
910 step_mode: wgpu::VertexStepMode::Instance,
911 attributes: &[
912 wgpu::VertexAttribute {
913 offset: 0,
914 shader_location: 2,
915 format: wgpu::VertexFormat::Float32x2, },
917 wgpu::VertexAttribute {
918 offset: 8,
919 shader_location: 3,
920 format: wgpu::VertexFormat::Float32, },
922 wgpu::VertexAttribute {
923 offset: 12,
924 shader_location: 4,
925 format: wgpu::VertexFormat::Float32, },
927 wgpu::VertexAttribute {
928 offset: 16,
929 shader_location: 5,
930 format: wgpu::VertexFormat::Float32, },
932 wgpu::VertexAttribute {
933 offset: 20,
934 shader_location: 6,
935 format: wgpu::VertexFormat::Float32, },
937 wgpu::VertexAttribute {
938 offset: 24,
939 shader_location: 7,
940 format: wgpu::VertexFormat::Float32x2, },
942 wgpu::VertexAttribute {
943 offset: 32,
944 shader_location: 8,
945 format: wgpu::VertexFormat::Float32x4, },
947 ],
948 };
949
950 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
951 label: Some("sdf_render_pipeline"),
952 layout: Some(&self.pipeline_layout),
953 vertex: wgpu::VertexState {
954 module: &shader,
955 entry_point: Some("vs_main"),
956 buffers: &[vertex_layout, instance_layout],
957 compilation_options: Default::default(),
958 },
959 fragment: Some(wgpu::FragmentState {
960 module: &shader,
961 entry_point: Some("fs_main"),
962 targets: &[Some(wgpu::ColorTargetState {
963 format: self.surface_format,
964 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
965 write_mask: wgpu::ColorWrites::ALL,
966 })],
967 compilation_options: Default::default(),
968 }),
969 primitive: wgpu::PrimitiveState {
970 topology: wgpu::PrimitiveTopology::TriangleList,
971 strip_index_format: None,
972 front_face: wgpu::FrontFace::Ccw,
973 cull_mode: None,
974 polygon_mode: wgpu::PolygonMode::Fill,
975 unclipped_depth: false,
976 conservative: false,
977 },
978 depth_stencil: None,
979 multisample: wgpu::MultisampleState::default(),
980 multiview: None,
981 cache: None,
982 });
983
984 self.pipelines.insert(key, pipeline);
985 key
986 }
987
988 pub fn camera_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
990 &self.camera_bind_group_layout
991 }
992
993 pub fn render(
1001 &mut self,
1002 device: &wgpu::Device,
1003 encoder: &mut wgpu::CommandEncoder,
1004 target: &wgpu::TextureView,
1005 commands: &[SdfCommand],
1006 clear_color: Option<wgpu::Color>,
1007 ) {
1008 if commands.is_empty() {
1009 return;
1010 }
1011
1012 for cmd in commands {
1014 self.get_or_create_pipeline(device, &cmd.sdf_expr, &cmd.fill);
1015 }
1016
1017 let load_op = match clear_color {
1018 Some(color) => wgpu::LoadOp::Clear(color),
1019 None => wgpu::LoadOp::Load,
1020 };
1021
1022 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1023 label: Some("sdf_render_pass"),
1024 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1025 view: target,
1026 resolve_target: None,
1027 ops: wgpu::Operations {
1028 load: load_op,
1029 store: wgpu::StoreOp::Store,
1030 },
1031 })],
1032 depth_stencil_attachment: None,
1033 timestamp_writes: None,
1034 occlusion_query_set: None,
1035 });
1036
1037 render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
1038 render_pass.set_bind_group(1, &self.time_bind_group, &[]);
1039 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1040 render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
1041
1042 let mut i = 0;
1045 while i < commands.len() {
1046 let key = compute_pipeline_key(&commands[i].sdf_expr, &commands[i].fill);
1047 let batch_start = i;
1048
1049 while i < commands.len()
1051 && compute_pipeline_key(&commands[i].sdf_expr, &commands[i].fill) == key
1052 {
1053 i += 1;
1054 }
1055
1056 let batch = &commands[batch_start..i];
1057 let pipeline = match self.pipelines.get(&key) {
1058 Some(p) => p,
1059 None => continue, };
1061
1062 let instances: Vec<SdfInstance> = batch
1064 .iter()
1065 .map(|cmd| {
1066 let primary_color = primary_color_from_fill(&cmd.fill);
1067 SdfInstance {
1068 position: [cmd.x, cmd.y],
1069 bounds: cmd.bounds,
1070 rotation: cmd.rotation,
1071 scale: cmd.scale,
1072 opacity: cmd.opacity,
1073 _pad: [0.0; 2],
1074 color: primary_color,
1075 }
1076 })
1077 .collect();
1078
1079 let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1080 label: Some("sdf_instance_buffer"),
1081 contents: bytemuck::cast_slice(&instances),
1082 usage: wgpu::BufferUsages::VERTEX,
1083 });
1084
1085 render_pass.set_pipeline(pipeline);
1086 render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
1087 render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
1088 }
1089 }
1090
1091 pub fn pipeline_count(&self) -> usize {
1093 self.pipelines.len()
1094 }
1095
1096 pub fn clear(&mut self) {
1098 self.pipelines.clear();
1099 }
1100}
1101
1102fn primary_color_from_fill(fill: &SdfFill) -> [f32; 4] {
1106 match fill {
1107 SdfFill::Solid { color } => *color,
1108 SdfFill::Outline { color, .. } => *color,
1109 SdfFill::SolidWithOutline { fill, .. } => *fill,
1110 SdfFill::Gradient { from, .. } => *from,
1111 SdfFill::Glow { color, .. } => *color,
1112 SdfFill::CosinePalette { a, .. } => [a[0], a[1], a[2], 1.0],
1113 }
1114}
1115
1116#[cfg(test)]
1121mod tests {
1122 use super::*;
1123
1124 #[test]
1125 fn sdf_instance_is_48_bytes() {
1126 assert_eq!(std::mem::size_of::<SdfInstance>(), 48);
1129 }
1130
1131 #[test]
1132 fn sdf_quad_vertex_is_16_bytes() {
1133 assert_eq!(std::mem::size_of::<SdfQuadVertex>(), 16);
1135 }
1136
1137 #[test]
1138 fn generate_shader_contains_expression() {
1139 let shader = generate_sdf_shader(
1140 "sd_circle(p, 50.0)",
1141 &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] },
1142 );
1143 assert!(shader.contains("sd_circle(p, 50.0)"), "shader should contain the SDF expression");
1144 assert!(shader.contains("fn sd_circle"), "shader should contain primitive definitions");
1145 assert!(shader.contains("fn vs_main"), "shader should contain vertex entry point");
1146 assert!(shader.contains("fn fs_main"), "shader should contain fragment entry point");
1147 }
1148
1149 #[test]
1150 fn generate_shader_includes_all_primitives() {
1151 let shader = generate_sdf_shader(
1152 "sd_circle(p, 10.0)",
1153 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1154 );
1155 assert!(shader.contains("fn sd_circle"));
1156 assert!(shader.contains("fn sd_box"));
1157 assert!(shader.contains("fn sd_rounded_box"));
1158 assert!(shader.contains("fn sd_segment"));
1159 assert!(shader.contains("fn sd_capsule"));
1160 assert!(shader.contains("fn sd_ring"));
1161 assert!(shader.contains("fn sd_ellipse"));
1162 assert!(shader.contains("fn sd_hexagon"));
1163 assert!(shader.contains("fn sd_star5"));
1164 assert!(shader.contains("fn sd_cross"));
1165 }
1166
1167 #[test]
1168 fn generate_shader_includes_all_ops() {
1169 let shader = generate_sdf_shader(
1170 "op_union(sd_circle(p, 10.0), sd_box(p, vec2(5.0, 5.0)))",
1171 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1172 );
1173 assert!(shader.contains("fn op_union"));
1174 assert!(shader.contains("fn op_subtract"));
1175 assert!(shader.contains("fn op_intersect"));
1176 assert!(shader.contains("fn op_smooth_union"));
1177 assert!(shader.contains("fn op_smooth_subtract"));
1178 assert!(shader.contains("fn op_smooth_intersect"));
1179 assert!(shader.contains("fn op_round"));
1180 assert!(shader.contains("fn op_annular"));
1181 assert!(shader.contains("fn op_repeat"));
1182 assert!(shader.contains("fn op_translate"));
1183 assert!(shader.contains("fn op_rotate"));
1184 assert!(shader.contains("fn op_scale"));
1185 }
1186
1187 #[test]
1188 fn generate_shader_includes_camera_and_time_bindings() {
1189 let shader = generate_sdf_shader(
1190 "sd_circle(p, 1.0)",
1191 &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] },
1192 );
1193 assert!(shader.contains("struct CameraUniform"));
1194 assert!(shader.contains("@group(0) @binding(0)"));
1195 assert!(shader.contains("struct TimeUniform"));
1196 assert!(shader.contains("@group(1) @binding(0)"));
1197 assert!(shader.contains("var<uniform> camera: CameraUniform"));
1198 assert!(shader.contains("var<uniform> time_data: TimeUniform"));
1199 }
1200
1201 #[test]
1202 fn fill_solid_generates_smoothstep_aa() {
1203 let code = generate_fill_wgsl(&SdfFill::Solid {
1204 color: [1.0, 0.0, 0.0, 1.0],
1205 });
1206 assert!(code.contains("smoothstep"), "solid fill should use smoothstep for AA");
1207 assert!(code.contains("1.000000, 0.000000, 0.000000, 1.000000"));
1208 }
1209
1210 #[test]
1211 fn fill_outline_generates_abs_distance() {
1212 let code = generate_fill_wgsl(&SdfFill::Outline {
1213 color: [0.0, 1.0, 0.0, 1.0],
1214 thickness: 2.0,
1215 });
1216 assert!(code.contains("abs(d)"), "outline fill should use abs(d)");
1217 assert!(code.contains("2.000000"), "outline fill should include thickness");
1218 }
1219
1220 #[test]
1221 fn fill_glow_generates_exp_falloff() {
1222 let code = generate_fill_wgsl(&SdfFill::Glow {
1223 color: [0.0, 0.5, 1.0, 1.0],
1224 intensity: 3.0,
1225 });
1226 assert!(code.contains("glow_intensity"), "glow fill should use intensity parameter");
1227 assert!(code.contains("3.000000"), "glow fill should include intensity value");
1228 }
1229
1230 #[test]
1231 fn fill_gradient_generates_direction_mapping() {
1232 let code = generate_fill_wgsl(&SdfFill::Gradient {
1233 from: [1.0, 0.0, 0.0, 1.0],
1234 to: [0.0, 0.0, 1.0, 1.0],
1235 angle: 1.5708,
1236 scale: 1.0,
1237 });
1238 assert!(code.contains("grad_dir"), "gradient fill should compute direction");
1239 assert!(code.contains("mix("), "gradient fill should interpolate colors");
1240 }
1241
1242 #[test]
1243 fn fill_cosine_palette_generates_cosine_function() {
1244 let code = generate_fill_wgsl(&SdfFill::CosinePalette {
1245 a: [0.5, 0.5, 0.5],
1246 b: [0.5, 0.5, 0.5],
1247 c: [1.0, 1.0, 1.0],
1248 d: [0.0, 0.33, 0.67],
1249 });
1250 assert!(code.contains("cos("), "cosine palette should use cos()");
1251 assert!(code.contains("6.283185"), "cosine palette should use 2*pi");
1252 }
1253
1254 #[test]
1255 fn fill_solid_with_outline_generates_both_colors() {
1256 let code = generate_fill_wgsl(&SdfFill::SolidWithOutline {
1257 fill: [1.0, 0.0, 0.0, 1.0],
1258 outline: [1.0, 1.0, 1.0, 1.0],
1259 thickness: 1.5,
1260 });
1261 assert!(code.contains("fill_color"), "should have fill color");
1262 assert!(code.contains("outline_color"), "should have outline color");
1263 assert!(code.contains("1.500000"), "should include thickness");
1264 }
1265
1266 #[test]
1267 fn pipeline_key_differs_for_different_expressions() {
1268 let fill = SdfFill::Solid { color: [1.0; 4] };
1269 let k1 = compute_pipeline_key("sd_circle(p, 10.0)", &fill);
1270 let k2 = compute_pipeline_key("sd_box(p, vec2(5.0, 5.0))", &fill);
1271 assert_ne!(k1, k2, "different expressions should produce different keys");
1272 }
1273
1274 #[test]
1275 fn pipeline_key_differs_for_different_fills() {
1276 let expr = "sd_circle(p, 10.0)";
1277 let k1 = compute_pipeline_key(expr, &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] });
1278 let k2 = compute_pipeline_key(expr, &SdfFill::Solid { color: [0.0, 1.0, 0.0, 1.0] });
1279 assert_ne!(k1, k2, "different fills should produce different keys");
1280 }
1281
1282 #[test]
1283 fn pipeline_key_is_deterministic() {
1284 let fill = SdfFill::Glow { color: [0.0, 0.5, 1.0, 1.0], intensity: 3.0 };
1285 let k1 = compute_pipeline_key("sd_circle(p, 10.0)", &fill);
1286 let k2 = compute_pipeline_key("sd_circle(p, 10.0)", &fill);
1287 assert_eq!(k1, k2, "same input should produce the same key");
1288 }
1289
1290 #[test]
1291 fn compute_fill_hash_varies_by_discriminant() {
1292 let h1 = compute_fill_hash(&SdfFill::Solid { color: [1.0; 4] });
1293 let h2 = compute_fill_hash(&SdfFill::Glow { color: [1.0; 4], intensity: 1.0 });
1294 assert_ne!(h1, h2, "different fill types should hash differently");
1295 }
1296
1297 #[test]
1298 fn primary_color_extraction() {
1299 assert_eq!(
1300 primary_color_from_fill(&SdfFill::Solid { color: [0.1, 0.2, 0.3, 0.4] }),
1301 [0.1, 0.2, 0.3, 0.4],
1302 );
1303 assert_eq!(
1304 primary_color_from_fill(&SdfFill::Outline { color: [0.5, 0.6, 0.7, 0.8], thickness: 1.0 }),
1305 [0.5, 0.6, 0.7, 0.8],
1306 );
1307 assert_eq!(
1308 primary_color_from_fill(&SdfFill::SolidWithOutline {
1309 fill: [0.1, 0.2, 0.3, 0.4],
1310 outline: [0.9, 0.9, 0.9, 1.0],
1311 thickness: 2.0,
1312 }),
1313 [0.1, 0.2, 0.3, 0.4],
1314 );
1315 assert_eq!(
1316 primary_color_from_fill(&SdfFill::Gradient {
1317 from: [1.0, 0.0, 0.0, 1.0],
1318 to: [0.0, 0.0, 1.0, 1.0],
1319 angle: 0.0,
1320 scale: 1.0,
1321 }),
1322 [1.0, 0.0, 0.0, 1.0],
1323 );
1324 assert_eq!(
1325 primary_color_from_fill(&SdfFill::Glow { color: [0.0, 1.0, 0.0, 1.0], intensity: 5.0 }),
1326 [0.0, 1.0, 0.0, 1.0],
1327 );
1328 let cosine = primary_color_from_fill(&SdfFill::CosinePalette {
1329 a: [0.5, 0.5, 0.5],
1330 b: [0.5, 0.5, 0.5],
1331 c: [1.0, 1.0, 1.0],
1332 d: [0.0, 0.33, 0.67],
1333 });
1334 assert_eq!(cosine, [0.5, 0.5, 0.5, 1.0]);
1335 }
1336
1337 #[test]
1338 fn generated_shader_has_valid_structure() {
1339 let shader = generate_sdf_shader(
1341 "op_smooth_union(sd_circle(p, 30.0), sd_box(p, vec2<f32>(20.0, 20.0)), 5.0)",
1342 &SdfFill::SolidWithOutline {
1343 fill: [0.2, 0.4, 0.8, 1.0],
1344 outline: [1.0, 1.0, 1.0, 1.0],
1345 thickness: 2.0,
1346 },
1347 );
1348
1349 let prim_pos = shader.find("fn sd_circle").unwrap();
1351 let binding_pos = shader.find("struct CameraUniform").unwrap();
1352 let vs_pos = shader.find("fn vs_main").unwrap();
1353 let fs_pos = shader.find("fn fs_main").unwrap();
1354
1355 assert!(prim_pos < binding_pos, "primitives should come before bindings");
1356 assert!(binding_pos < vs_pos, "bindings should come before vertex shader");
1357 assert!(vs_pos < fs_pos, "vertex shader should come before fragment shader");
1358 }
1359
1360 #[test]
1361 fn sdf_quad_vertices_are_centered() {
1362 assert_eq!(SDF_QUAD_VERTICES[0].position, [-1.0, -1.0]);
1364 assert_eq!(SDF_QUAD_VERTICES[1].position, [1.0, -1.0]);
1365 assert_eq!(SDF_QUAD_VERTICES[2].position, [1.0, 1.0]);
1366 assert_eq!(SDF_QUAD_VERTICES[3].position, [-1.0, 1.0]);
1367 }
1368
1369 #[test]
1370 fn sdf_quad_indices_form_two_triangles() {
1371 assert_eq!(SDF_QUAD_INDICES.len(), 6);
1372 assert_eq!(SDF_QUAD_INDICES, &[0, 1, 2, 0, 2, 3]);
1373 }
1374
1375 fn validate_wgsl(source: &str) -> Result<(), String> {
1386 match naga::front::wgsl::parse_str(source) {
1387 Ok(_module) => Ok(()),
1388 Err(e) => Err(format!("{e:?}")),
1389 }
1390 }
1391
1392 #[test]
1395 fn wgsl_circle_compiles() {
1396 let shader = generate_sdf_shader(
1397 "sd_circle(p, 50.0)",
1398 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1399 );
1400 let result = validate_wgsl(&shader);
1401 assert!(result.is_ok(), "Circle shader failed: {}", result.unwrap_err());
1402 }
1403
1404 #[test]
1405 fn wgsl_box_compiles() {
1406 let shader = generate_sdf_shader(
1407 "sd_box(p, vec2<f32>(20.0, 10.0))",
1408 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1409 );
1410 let result = validate_wgsl(&shader);
1411 assert!(result.is_ok(), "Box shader failed: {}", result.unwrap_err());
1412 }
1413
1414 #[test]
1415 fn wgsl_rounded_box_compiles() {
1416 let shader = generate_sdf_shader(
1417 "sd_rounded_box(p, vec2<f32>(20.0, 10.0), vec4<f32>(3.0, 3.0, 3.0, 3.0))",
1418 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1419 );
1420 let result = validate_wgsl(&shader);
1421 assert!(result.is_ok(), "Rounded box shader failed: {}", result.unwrap_err());
1422 }
1423
1424 #[test]
1425 fn wgsl_all_primitives_compile() {
1426 let primitives: &[(&str, &str)] = &[
1429 ("sd_circle", "sd_circle(p, 50.0)"),
1430 ("sd_box", "sd_box(p, vec2<f32>(20.0, 10.0))"),
1431 ("sd_rounded_box", "sd_rounded_box(p, vec2<f32>(20.0, 10.0), vec4<f32>(3.0, 3.0, 3.0, 3.0))"),
1432 ("sd_segment", "sd_segment(p, vec2<f32>(0.0, 0.0), vec2<f32>(20.0, 10.0))"),
1433 ("sd_capsule", "sd_capsule(p, vec2<f32>(-10.0, 0.0), vec2<f32>(10.0, 0.0), 5.0)"),
1434 ("sd_equilateral_triangle", "sd_equilateral_triangle(p, 20.0)"),
1435 ("sd_ring", "sd_ring(p, 20.0, 3.0)"),
1436 ("sd_ellipse", "sd_ellipse(p, vec2<f32>(30.0, 15.0))"),
1437 ("sd_hexagon", "sd_hexagon(p, 20.0)"),
1438 ("sd_star5", "sd_star5(p, 15.0, 0.4)"),
1439 ("sd_cross", "sd_cross(p, vec2<f32>(20.0, 5.0), 2.0)"),
1440 ];
1441 for (name, expr) in primitives {
1442 let shader = generate_sdf_shader(expr, &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] });
1443 let result = validate_wgsl(&shader);
1444 assert!(result.is_ok(), "Primitive '{name}' failed to compile.\nExpression: {expr}\nError: {}", result.unwrap_err());
1445 }
1446 }
1447
1448 #[test]
1451 fn wgsl_composition_operators_compile() {
1452 let exprs: &[(&str, &str)] = &[
1453 ("op_union", "op_union(sd_circle(p, 20.0), sd_box(p, vec2<f32>(15.0, 15.0)))"),
1454 ("op_subtract", "op_subtract(sd_circle(p, 20.0), sd_circle(p - vec2<f32>(10.0, 0.0), 15.0))"),
1455 ("op_intersect", "op_intersect(sd_circle(p, 20.0), sd_box(p, vec2<f32>(15.0, 15.0)))"),
1456 ("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)"),
1457 ("op_smooth_subtract", "op_smooth_subtract(sd_circle(p, 20.0), sd_circle(p - vec2<f32>(5.0, 0.0), 15.0), 3.0)"),
1458 ("op_smooth_intersect", "op_smooth_intersect(sd_circle(p, 25.0), sd_box(p, vec2<f32>(15.0, 15.0)), 4.0)"),
1459 ];
1460 for (name, expr) in exprs {
1461 let shader = generate_sdf_shader(expr, &SdfFill::Solid { color: [1.0, 0.0, 0.0, 1.0] });
1462 let result = validate_wgsl(&shader);
1463 assert!(result.is_ok(), "Composition '{name}' failed.\nExpression: {expr}\nError: {}", result.unwrap_err());
1464 }
1465 }
1466
1467 #[test]
1470 fn wgsl_all_fill_types_compile() {
1471 let expr = "sd_circle(p, 30.0)";
1472 let fills: Vec<(&str, SdfFill)> = vec![
1473 ("Solid", SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] }),
1474 ("Outline", SdfFill::Outline { color: [1.0, 0.0, 0.0, 1.0], thickness: 2.0 }),
1475 ("SolidWithOutline", SdfFill::SolidWithOutline {
1476 fill: [0.0, 0.0, 1.0, 1.0],
1477 outline: [1.0, 1.0, 1.0, 1.0],
1478 thickness: 1.5,
1479 }),
1480 ("Gradient", SdfFill::Gradient {
1481 from: [1.0, 0.0, 0.0, 1.0],
1482 to: [0.0, 0.0, 1.0, 1.0],
1483 angle: 1.5707,
1484 scale: 1.0,
1485 }),
1486 ("Glow", SdfFill::Glow { color: [0.0, 1.0, 0.5, 1.0], intensity: 0.8 }),
1487 ("CosinePalette", SdfFill::CosinePalette {
1488 a: [0.5, 0.5, 0.5],
1489 b: [0.5, 0.5, 0.5],
1490 c: [1.0, 1.0, 1.0],
1491 d: [0.0, 0.33, 0.67],
1492 }),
1493 ];
1494 for (name, fill) in &fills {
1495 let shader = generate_sdf_shader(expr, fill);
1496 let result = validate_wgsl(&shader);
1497 assert!(result.is_ok(), "Fill '{name}' failed to compile.\nError: {}", result.unwrap_err());
1498 }
1499 }
1500
1501 #[test]
1504 fn wgsl_deeply_nested_tree_compiles() {
1505 let expr = concat!(
1507 "op_smooth_union(",
1508 "op_smooth_union(",
1509 "op_smooth_union(",
1510 "sd_rounded_box(p, vec2<f32>(8.0, 30.0), vec4<f32>(2.0, 2.0, 2.0, 2.0)), ",
1511 "sd_circle(p - vec2<f32>(0.0, 25.0), 18.0), ",
1512 "4.0), ",
1513 "sd_circle(p - vec2<f32>(-10.0, 20.0), 14.0), ",
1514 "4.0), ",
1515 "sd_circle(p - vec2<f32>(10.0, 20.0), 14.0), ",
1516 "4.0)",
1517 );
1518 let shader = generate_sdf_shader(expr, &SdfFill::Gradient {
1519 from: [0.35, 0.23, 0.1, 1.0],
1520 to: [0.18, 0.54, 0.3, 1.0],
1521 angle: 1.5707,
1522 scale: 1.0,
1523 });
1524 let result = validate_wgsl(&shader);
1525 assert!(result.is_ok(), "Nested tree failed: {}", result.unwrap_err());
1526 }
1527
1528 #[test]
1529 fn wgsl_subtract_creates_crescent() {
1530 let expr = "op_subtract(sd_circle(p, 25.0), sd_circle(p - vec2<f32>(10.0, 5.0), 22.0))";
1532 let shader = generate_sdf_shader(expr, &SdfFill::Glow {
1533 color: [0.9, 0.9, 0.6, 1.0],
1534 intensity: 0.5,
1535 });
1536 let result = validate_wgsl(&shader);
1537 assert!(result.is_ok(), "Crescent moon failed: {}", result.unwrap_err());
1538 }
1539
1540 #[test]
1543 fn wgsl_time_uniform_in_expression() {
1544 let shader = generate_sdf_shader(
1546 "sd_circle(p, 20.0 + sin(time_data.time) * 5.0)",
1547 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1548 );
1549 let result = validate_wgsl(&shader);
1550 assert!(result.is_ok(), "Time uniform shader failed: {}", result.unwrap_err());
1551 }
1552
1553 #[test]
1554 fn wgsl_repeat_domain_compiles() {
1555 let shader = generate_sdf_shader(
1557 "sd_circle(op_repeat(p, vec2<f32>(40.0, 40.0)), 10.0)",
1558 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1559 );
1560 let result = validate_wgsl(&shader);
1561 assert!(result.is_ok(), "Repeat domain failed: {}", result.unwrap_err());
1562 }
1563
1564 #[test]
1565 fn wgsl_rotate_transform_compiles() {
1566 let shader = generate_sdf_shader(
1568 "sd_box(op_rotate(p, 0.785), vec2<f32>(20.0, 10.0))",
1569 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1570 );
1571 let result = validate_wgsl(&shader);
1572 assert!(result.is_ok(), "Rotate transform failed: {}", result.unwrap_err());
1573 }
1574
1575 #[test]
1576 fn wgsl_translate_transform_compiles() {
1577 let shader = generate_sdf_shader(
1579 "sd_circle(op_translate(p, vec2<f32>(15.0, 10.0)), 12.0)",
1580 &SdfFill::Outline { color: [0.0, 1.0, 0.0, 1.0], thickness: 2.0 },
1581 );
1582 let result = validate_wgsl(&shader);
1583 assert!(result.is_ok(), "Translate transform failed: {}", result.unwrap_err());
1584 }
1585
1586 #[test]
1587 fn wgsl_scale_transform_compiles() {
1588 let shader = generate_sdf_shader(
1590 "sd_star5(op_scale(p, 2.0), 15.0, 0.4) * 2.0",
1591 &SdfFill::Solid { color: [1.0, 0.8, 0.0, 1.0] },
1592 );
1593 let result = validate_wgsl(&shader);
1594 assert!(result.is_ok(), "Scale transform failed: {}", result.unwrap_err());
1595 }
1596
1597 #[test]
1600 fn wgsl_round_modifier_compiles() {
1601 let shader = generate_sdf_shader(
1602 "op_round(sd_box(p, vec2<f32>(20.0, 10.0)), 3.0)",
1603 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1604 );
1605 let result = validate_wgsl(&shader);
1606 assert!(result.is_ok(), "Round modifier failed: {}", result.unwrap_err());
1607 }
1608
1609 #[test]
1610 fn wgsl_annular_modifier_compiles() {
1611 let shader = generate_sdf_shader(
1613 "op_annular(sd_circle(p, 20.0), 2.0)",
1614 &SdfFill::Solid { color: [1.0, 1.0, 1.0, 1.0] },
1615 );
1616 let result = validate_wgsl(&shader);
1617 assert!(result.is_ok(), "Annular modifier failed: {}", result.unwrap_err());
1618 }
1619
1620 #[test]
1623 fn wgsl_complex_scene_compiles() {
1624 let expr = concat!(
1627 "op_union(",
1628 "sd_circle(op_repeat(p, vec2<f32>(50.0, 50.0)), 8.0), ",
1629 "sd_cross(op_rotate(p, 0.785), vec2<f32>(30.0, 6.0), 2.0)",
1630 ")",
1631 );
1632 let shader = generate_sdf_shader(expr, &SdfFill::CosinePalette {
1633 a: [0.5, 0.5, 0.5],
1634 b: [0.5, 0.5, 0.5],
1635 c: [1.0, 1.0, 1.0],
1636 d: [0.0, 0.33, 0.67],
1637 });
1638 let result = validate_wgsl(&shader);
1639 assert!(result.is_ok(), "Complex scene failed: {}", result.unwrap_err());
1640 }
1641
1642 #[test]
1643 fn wgsl_animated_pulsing_ring() {
1644 let shader = generate_sdf_shader(
1646 "sd_ring(p, 25.0 + sin(time_data.time * 2.0) * 5.0, 3.0)",
1647 &SdfFill::Glow { color: [0.2, 0.8, 1.0, 1.0], intensity: 1.5 },
1648 );
1649 let result = validate_wgsl(&shader);
1650 assert!(result.is_ok(), "Animated ring failed: {}", result.unwrap_err());
1651 }
1652
1653 #[test]
1654 fn wgsl_three_way_smooth_union() {
1655 let expr = concat!(
1657 "op_smooth_union(",
1658 "op_smooth_union(",
1659 "sd_circle(p - vec2<f32>(-15.0, 0.0), 12.0), ",
1660 "sd_circle(p - vec2<f32>(15.0, 0.0), 12.0), ",
1661 "6.0), ",
1662 "sd_circle(p - vec2<f32>(0.0, 15.0), 12.0), ",
1663 "6.0)",
1664 );
1665 let shader = generate_sdf_shader(expr, &SdfFill::SolidWithOutline {
1666 fill: [0.3, 0.3, 0.8, 1.0],
1667 outline: [1.0, 1.0, 1.0, 1.0],
1668 thickness: 1.0,
1669 });
1670 let result = validate_wgsl(&shader);
1671 assert!(result.is_ok(), "Three-way smooth union failed: {}", result.unwrap_err());
1672 }
1673}