graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/shapes.rs (18.4K)
1 //! The four shapes that were kernel subnets — Sphere, Box, Plane, Extrude —
2 //! as native generators. Phase 7 step 3 of `shapeshifter.md`.
3 //!
4 //! Each was a subnet of `input → opencl → output` whose kernel ran under
5 //! `if (id == 0)`: one work item doing loops, then a weld by position on the
6 //! way back that threw away every shared point the loop had known about.
7 //! Native, each builds welded points and real primitives directly — a quad
8 //! stays a quad — costs no JIT compile, and needs no OpenCL runtime at all.
9 //! The parameter surfaces are the templates' own, so a saved instance keeps
10 //! its values through `nativize_kernel_subnets` in `merge_template_defs`.
11 //!
12 //! The Sphere's three methods (UV, Icosphere, Cube) weld by a QUANTIZED
13 //! position key rather than by trusting bit-identical arithmetic across
14 //! faces: the kernel summed barycentric weights in one fixed expression so
15 //! shared corners landed on the same bits, then welded at 1e-4 anyway. A
16 //! quantized key is the same guarantee stated once.
17
18 use crate::app::FsNode;
19 use crate::detail::{AttribData, Detail, CD, DEFAULT_COLOR};
20 use crate::geometry::{node_param_f32, node_param_str, node_param_vec3, point_normals, sphere_detail};
21 use glam::Vec3;
22 use std::collections::HashMap;
23
24 /// The golden ratio: the icosahedron's corners sit on three orthogonal
25 /// golden rectangles.
26 const T: f32 = 1.618_034;
27
28 /// Points welded by quantized position, so a corner two faces share is one
29 /// point whichever face names it first.
30 struct Welder {
31 map: HashMap<[i64; 3], u32>,
32 d: Detail,
33 }
34
35 impl Welder {
36 fn new() -> Self {
37 Welder { map: HashMap::new(), d: Detail::new() }
38 }
39
40 fn point(&mut self, p: Vec3) -> u32 {
41 let key = [
42 (p.x as f64 * 1e5).round() as i64,
43 (p.y as f64 * 1e5).round() as i64,
44 (p.z as f64 * 1e5).round() as i64,
45 ];
46 if let Some(&i) = self.map.get(&key) {
47 return i;
48 }
49 let i = self.d.add_point(p);
50 self.map.insert(key, i);
51 i
52 }
53
54 /// A primitive turned OUTWARD about the origin: the shapes here are
55 /// convex about their centre, so a face whose normal points toward its
56 /// own centroid is inside out, whatever table it came from.
57 fn outward_prim(&mut self, pts: &[u32]) {
58 let a = self.d.pos(pts[0] as usize);
59 let b = self.d.pos(pts[1] as usize);
60 let c = self.d.pos(pts[2] as usize);
61 let n = (b - a).cross(c - a);
62 let centroid: Vec3 = pts.iter().map(|&p| self.d.pos(p as usize)).sum();
63 if n.dot(centroid) < 0.0 {
64 let rev: Vec<u32> = pts.iter().rev().copied().collect();
65 self.d.add_prim(&rev);
66 } else {
67 self.d.add_prim(pts);
68 }
69 }
70 }
71
72 /// Whether a sphere node carries its own Center parameters. A bare `sphere`
73 /// node built by hand (the tests' `ref_node`) has none and is placed by
74 /// index, the way Line and Points still are; every node that came through a
75 /// template or a load does.
76 pub fn sphere_has_center(target: &FsNode) -> bool {
77 target.params.iter().any(|p| p.name == "Center X")
78 }
79
80 /// The Sphere node, by Method: UV (Rows x Columns), Icosphere (20 faces
81 /// each split into Frequency^2 triangles), Cube (a Resolution x Resolution
82 /// grid on each face, pushed out through the spherified-cube map). Welded
83 /// point counts are the closed forms: `2 + (rows - 1) * cols`,
84 /// `10 f^2 + 2` and `6 r^2 + 2`. Cube builds QUADS — the kernel fanned them.
85 pub fn sphere_node_detail(target: &FsNode, legacy_center: Option<Vec3>) -> Detail {
86 let method = node_param_str(target, "Method", "UV").to_lowercase();
87 let radius = node_param_f32(target, "Radius", 0.5).max(1e-4);
88 let center = legacy_center.unwrap_or_else(|| {
89 Vec3::new(
90 node_param_f32(target, "Center X", 0.0),
91 node_param_f32(target, "Center Y", 0.55),
92 node_param_f32(target, "Center Z", 0.0),
93 )
94 });
95 let colored = node_param_str(target, "Color", "true") != "false";
96 let unit = match method.as_str() {
97 "icosphere" => icosphere_unit(node_param_f32(target, "Frequency", 4.0).round().clamp(1.0, 16.0) as usize),
98 "cube" => cube_sphere_unit(node_param_f32(target, "Resolution", 8.0).round().clamp(1.0, 64.0) as usize),
99 _ => {
100 let rows = node_param_f32(target, "Rows", 16.0).round().clamp(2.0, 128.0) as usize;
101 let cols = node_param_f32(target, "Columns", 24.0).round().clamp(3.0, 128.0) as usize;
102 let mut d = sphere_detail(center, radius, rows, cols);
103 finish_sphere(&mut d, center, colored);
104 return d;
105 }
106 };
107 let mut d = unit;
108 for p in 0..d.num_points() {
109 let u = d.pos(p);
110 d.set_pos(p, center + u * radius);
111 }
112 finish_sphere(&mut d, center, colored);
113 d
114 }
115
116 /// `Norm`, `UV` and `Cd` from the surface normal, as the sphere has always
117 /// carried them. Colour is the kernel's: the SIGNED normal folded into
118 /// 0..1, world-anchored so a point keeps its colour as the sphere turns.
119 fn finish_sphere(d: &mut Detail, center: Vec3, colored: bool) {
120 let n: Vec<Vec3> = (0..d.num_points()).map(|p| (d.pos(p) - center).normalize_or_zero()).collect();
121 let norms = n.iter().map(|n| n.to_array()).collect();
122 let uvs = n
123 .iter()
124 .map(|n| [0.5 + n.z.atan2(n.x) / std::f32::consts::TAU, 0.5 - n.y.clamp(-1.0, 1.0).asin() / std::f32::consts::PI])
125 .collect();
126 let cds = n
127 .iter()
128 .map(|n| if colored { [0.5 + n.x * 0.5, 0.5 + n.y * 0.5, 0.5 + n.z * 0.5] } else { DEFAULT_COLOR })
129 .collect();
130 let points = d.points_mut();
131 let _ = points.insert("Norm", AttribData::Float3(norms));
132 let _ = points.insert("UV", AttribData::Float2(uvs));
133 let _ = points.insert(CD, AttribData::Float3(cds));
134 }
135
136 /// The unit icosphere: the icosahedron's 20 faces, each split into
137 /// `freq^2` triangles by integer barycentric weights, every corner pushed
138 /// onto the sphere. Row i of a face holds `freq - i` upright triangles and
139 /// `freq - i - 1` inverted ones.
140 fn icosphere_unit(freq: usize) -> Detail {
141 let v: [Vec3; 12] = [
142 Vec3::new(-1.0, T, 0.0),
143 Vec3::new(1.0, T, 0.0),
144 Vec3::new(-1.0, -T, 0.0),
145 Vec3::new(1.0, -T, 0.0),
146 Vec3::new(0.0, -1.0, T),
147 Vec3::new(0.0, 1.0, T),
148 Vec3::new(0.0, -1.0, -T),
149 Vec3::new(0.0, 1.0, -T),
150 Vec3::new(T, 0.0, -1.0),
151 Vec3::new(T, 0.0, 1.0),
152 Vec3::new(-T, 0.0, -1.0),
153 Vec3::new(-T, 0.0, 1.0),
154 ];
155 let faces: [[usize; 3]; 20] = [
156 [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
157 [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
158 [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
159 [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1],
160 ];
161 let f = freq.max(1);
162 let mut w = Welder::new();
163 for [a, b, c] in faces {
164 let corner = |w: &mut Welder, wb: usize, wc: usize| {
165 let wa = f - wb - wc;
166 let p = (v[a] * wa as f32 + v[b] * wb as f32 + v[c] * wc as f32) / f as f32;
167 w.point(p.normalize())
168 };
169 for i in 0..f {
170 for j in 0..(f - i) {
171 let p0 = corner(&mut w, i, j);
172 let p1 = corner(&mut w, i + 1, j);
173 let p2 = corner(&mut w, i, j + 1);
174 w.outward_prim(&[p0, p1, p2]);
175 if j + 1 < f - i {
176 let q0 = corner(&mut w, i + 1, j);
177 let q1 = corner(&mut w, i + 1, j + 1);
178 let q2 = corner(&mut w, i, j + 1);
179 w.outward_prim(&[q0, q1, q2]);
180 }
181 }
182 }
183 }
184 w.d
185 }
186
187 /// The unit cube sphere: a `res x res` grid on each face of the cube
188 /// spanning -1..1, every point pushed onto the sphere by the spherified-cube
189 /// map — not a bare normalize, which crowds the corners and stretches the
190 /// face centres. One quad per cell.
191 fn cube_sphere_unit(res: usize) -> Detail {
192 let r = res.max(1);
193 let mut w = Welder::new();
194 for face in 0..6 {
195 let axis = face / 2;
196 let sign = if face % 2 == 0 { 1.0 } else { -1.0 };
197 let at = |w: &mut Welder, i: usize, j: usize| {
198 let u = -1.0 + 2.0 * i as f32 / r as f32;
199 let v = -1.0 + 2.0 * j as f32 / r as f32;
200 let (x, y, z) = match axis {
201 0 => (sign, u, v),
202 1 => (v, sign, u),
203 _ => (u, v, sign),
204 };
205 let (x2, y2, z2) = (x * x, y * y, z * z);
206 w.point(Vec3::new(
207 x * (1.0 - y2 * 0.5 - z2 * 0.5 + y2 * z2 / 3.0).sqrt(),
208 y * (1.0 - z2 * 0.5 - x2 * 0.5 + z2 * x2 / 3.0).sqrt(),
209 z * (1.0 - x2 * 0.5 - y2 * 0.5 + x2 * y2 / 3.0).sqrt(),
210 ))
211 };
212 for i in 0..r {
213 for j in 0..r {
214 let q = [at(&mut w, i, j), at(&mut w, i + 1, j), at(&mut w, i + 1, j + 1), at(&mut w, i, j + 1)];
215 w.outward_prim(&q);
216 }
217 }
218 }
219 w.d
220 }
221
222 /// An axis-aligned box: eight shared corners, six quads wound
223 /// counter-clockwise seen from outside, normals on the VERTICES (three faces
224 /// meet at a corner with three different normals), one colour.
225 pub fn cuboid_detail(center: Vec3, half: Vec3, color: [f32; 3]) -> Detail {
226 let mut d = Detail::new();
227 let (x, y, z) = (half.x, half.y, half.z);
228 let corners = [
229 center + Vec3::new(-x, -y, -z),
230 center + Vec3::new(x, -y, -z),
231 center + Vec3::new(x, y, -z),
232 center + Vec3::new(-x, y, -z),
233 center + Vec3::new(-x, -y, z),
234 center + Vec3::new(x, -y, z),
235 center + Vec3::new(x, y, z),
236 center + Vec3::new(-x, y, z),
237 ];
238 for c in corners {
239 d.add_point(c);
240 }
241 let faces: [([u32; 4], Vec3); 6] = [
242 ([3, 2, 1, 0], Vec3::NEG_Z),
243 ([6, 7, 4, 5], Vec3::Z),
244 ([7, 3, 0, 4], Vec3::NEG_X),
245 ([2, 6, 5, 1], Vec3::X),
246 ([7, 6, 2, 3], Vec3::Y),
247 ([1, 5, 4, 0], Vec3::NEG_Y),
248 ];
249 let mut norms = Vec::with_capacity(24);
250 for (quad, normal) in &faces {
251 d.add_prim(quad);
252 norms.extend(std::iter::repeat(normal.to_array()).take(4));
253 }
254 let _ = d.verts_mut().insert("Norm", AttribData::Float3(norms));
255 let _ = d.verts_mut().insert("UV", AttribData::Float2(vec![[0.0, 0.0]; 24]));
256 let _ = d.points_mut().insert(CD, AttribData::Float3(vec![color; 8]));
257 d
258 }
259
260 /// The Box node: a cube of Scale about Center — or, with Wireframe on, its
261 /// twelve edges as thin bars and its eight corners as small cubes, which is
262 /// what the kernel drew and what a reference frame wants.
263 pub fn box_node_detail(target: &FsNode) -> Detail {
264 let scale = node_param_f32(target, "Scale", 1.0).max(1e-4);
265 let wireframe = node_param_str(target, "Wireframe", "false") != "false";
266 let center = node_param_vec3(target, "Center", Vec3::new(0.0, 0.55, 0.0));
267 let color = [0.8, 0.2, 0.2];
268 let h = 0.5 * scale;
269 if !wireframe {
270 return cuboid_detail(center, Vec3::splat(h), color);
271 }
272 let (t_line, t_corner) = (0.008 * scale, 0.012 * scale);
273 let signs = [-1.0f32, 1.0];
274 let mut out = Detail::new();
275 for &sx in &signs {
276 for &sy in &signs {
277 for &sz in &signs {
278 out.merge(&cuboid_detail(center + Vec3::new(sx * h, sy * h, sz * h), Vec3::splat(t_corner), color));
279 }
280 }
281 }
282 for &sa in &signs {
283 for &sb in &signs {
284 out.merge(&cuboid_detail(center + Vec3::new(0.0, sa * h, sb * h), Vec3::new(h, t_line, t_line), color));
285 out.merge(&cuboid_detail(center + Vec3::new(sa * h, 0.0, sb * h), Vec3::new(t_line, h, t_line), color));
286 out.merge(&cuboid_detail(center + Vec3::new(sa * h, sb * h, 0.0), Vec3::new(t_line, t_line, h), color));
287 }
288 }
289 out
290 }
291
292 /// The Plane node: a Width x Length sheet of Columns x Rows quads in the XZ
293 /// plane about Center, wound counter-clockwise seen from +Y. Colour is the
294 /// kernel's gradient across the sheet, or the default grey with Color off.
295 /// Grid is the same sheet with a float3 Center and no gradient; two nodes
296 /// for history's sake, and this one is the older.
297 pub fn plane_node_detail(target: &FsNode) -> Detail {
298 let width = node_param_f32(target, "Width", 1.0).max(1e-4);
299 let length = node_param_f32(target, "Length", 1.0).max(1e-4);
300 let cols = node_param_f32(target, "Columns", 16.0).round().clamp(1.0, 500.0) as usize;
301 let rows = node_param_f32(target, "Rows", 16.0).round().clamp(1.0, 500.0) as usize;
302 let center = Vec3::new(
303 node_param_f32(target, "Center X", 0.0),
304 node_param_f32(target, "Center Y", 0.0),
305 node_param_f32(target, "Center Z", 0.0),
306 );
307 let colored = node_param_str(target, "Color", "true") != "false";
308
309 let mut d = Detail::new();
310 let mut cds = Vec::with_capacity((rows + 1) * (cols + 1));
311 let mut uvs = Vec::with_capacity((rows + 1) * (cols + 1));
312 for r in 0..=rows {
313 for c in 0..=cols {
314 let fx = c as f32 / cols as f32;
315 let fz = r as f32 / rows as f32;
316 d.add_point(center + Vec3::new((fx - 0.5) * width, 0.0, (fz - 0.5) * length));
317 uvs.push([fx, fz]);
318 cds.push(if colored { [0.35 + fx * 0.35, 0.45 + fz * 0.35, 0.85] } else { DEFAULT_COLOR });
319 }
320 }
321 let at = |r: usize, c: usize| (r * (cols + 1) + c) as u32;
322 for r in 0..rows {
323 for c in 0..cols {
324 d.add_prim(&[at(r, c), at(r + 1, c), at(r + 1, c + 1), at(r, c + 1)]);
325 }
326 }
327 let n = d.num_points();
328 let points = d.points_mut();
329 let _ = points.insert("Norm", AttribData::Float3(vec![[0.0, 1.0, 0.0]; n]));
330 let _ = points.insert("UV", AttribData::Float2(uvs));
331 let _ = points.insert(CD, AttribData::Float3(cds));
332 d
333 }
334
335 /// Extrude, as a WHOLE: every point moves along its point normal by
336 /// `distance`, the input's primitives become the top, and one quad wall
337 /// rises from each BOUNDARY edge — an edge one primitive uses. With Keep
338 /// Base the original primitives stay, wound the other way, so a sheet
339 /// becomes a closed slab and a closed surface a two-skinned shell.
340 ///
341 /// The kernel extruded each triangle on its own and welded the pieces back
342 /// together afterwards, which put a wall along every interior edge too; a
343 /// surface extruded that way was a bed of prisms. A point shared by two
344 /// faces has one top here, which is what "extrude the surface" means, and
345 /// what makes the result closed when the input was a closed sheet.
346 ///
347 /// Point attributes and groups carry to the top copies, primitive
348 /// attributes to the top and base copies; walls are fresh. Wall corners
349 /// share points with the top and base, so the kernel's 15% darker walls —
350 /// a per-corner colour a soup could hold — have no place to live and are
351 /// gone.
352 pub fn extrude_detail(d: &Detail, distance: f32, keep_base: bool) -> Detail {
353 let n = d.num_points();
354 if n == 0 || d.num_prims() == 0 {
355 return d.clone();
356 }
357 let normals = point_normals(d);
358 let mut out = Detail::new();
359 let base: Vec<[f32; 3]> = d.positions().to_vec();
360 let top: Vec<[f32; 3]> = (0..n).map(|p| (d.pos(p) + normals[p] * distance).to_array()).collect();
361 out.add_points(&base);
362 out.add_points(&top);
363 // Base points keep their identities; the tops are new.
364 let mut ids = d.ids().to_vec();
365 let next = ids.iter().max().map_or(0, |m| m + 1);
366 ids.extend((0..n as u64).map(|i| next + i));
367 let _ = out.set_ids(ids, next + n as u64);
368
369 let n32 = n as u32;
370 let mut src: Vec<Option<usize>> = Vec::new();
371 let mut directed: Vec<(u32, u32)> = Vec::new();
372 let mut uses: HashMap<(u32, u32), usize> = HashMap::new();
373 for pr in 0..d.num_prims() {
374 let pts = d.prim_points(pr);
375 if pts.len() < 3 {
376 continue;
377 }
378 let lifted: Vec<u32> = pts.iter().map(|&p| p + n32).collect();
379 out.add_prim(&lifted);
380 src.push(Some(pr));
381 for k in 0..pts.len() {
382 let (a, b) = (pts[k], pts[(k + 1) % pts.len()]);
383 let key = (a.min(b), a.max(b));
384 let count = uses.entry(key).or_insert(0);
385 if *count == 0 {
386 directed.push((a, b));
387 }
388 *count += 1;
389 }
390 }
391 // Walls on the boundary, wound so that for a counter-clockwise top and
392 // a positive distance the outside faces out: `(b - a) x N` is the
393 // outward direction of edge a→b on a counter-clockwise face.
394 for (a, b) in directed {
395 if uses[&(a.min(b), a.max(b))] == 1 {
396 out.add_prim(&[a, b, b + n32, a + n32]);
397 src.push(None);
398 }
399 }
400 if keep_base {
401 for pr in 0..d.num_prims() {
402 let pts = d.prim_points(pr);
403 if pts.len() < 3 {
404 continue;
405 }
406 let rev: Vec<u32> = pts.iter().rev().copied().collect();
407 out.add_prim(&rev);
408 src.push(Some(pr));
409 }
410 }
411
412 // Attributes: points doubled, primitives by source.
413 for name in d.points().names() {
414 let data = d.points().get(name).unwrap();
415 let mut nd = AttribData::zeroed(data.ty(), 2 * n);
416 for p in 0..n {
417 if let Some(v) = data.get(p) {
418 let _ = nd.set(p, v);
419 let _ = nd.set(p + n, v);
420 }
421 }
422 let kind = d.points().kind(name);
423 let _ = out.points_mut().insert(name, nd);
424 out.points_mut().set_kind(name, kind);
425 }
426 for g in d.points().group_names() {
427 out.points_mut().create_group(g);
428 for m in d.points().group_members(g) {
429 out.points_mut().add_to_group(g, m as usize);
430 out.points_mut().add_to_group(g, m as usize + n);
431 }
432 }
433 let np = out.num_prims();
434 for name in d.prims().names() {
435 let data = d.prims().get(name).unwrap();
436 let mut nd = AttribData::zeroed(data.ty(), np);
437 for (i, s) in src.iter().enumerate() {
438 if let Some(v) = s.and_then(|pr| data.get(pr)) {
439 let _ = nd.set(i, v);
440 }
441 }
442 let kind = d.prims().kind(name);
443 let _ = out.prims_mut().insert(name, nd);
444 out.prims_mut().set_kind(name, kind);
445 }
446 for g in d.prims().group_names() {
447 out.prims_mut().create_group(g);
448 for (i, s) in src.iter().enumerate() {
449 if s.is_some_and(|pr| d.prims().in_group(g, pr)) {
450 out.prims_mut().add_to_group(g, i);
451 }
452 }
453 }
454 for name in d.detail().names() {
455 let _ = out.detail_mut().insert(name, d.detail().get(name).unwrap().clone());
456 }
457 out
458 }