1use bytemuck::{Pod, Zeroable};
2
3use crate::domain::{DomainError, DomainSpace, Field, FieldKind, FieldProgram, Pose};
4use crate::entity::Entity;
5use crate::store::{Change, Cursor, Store};
6
7pub const OP_SPHERE: u32 = 1;
8pub const OP_BOX: u32 = 2;
9pub const OP_HALFSPACE: u32 = 3;
10pub const OP_HYPERSPHERE: u32 = 4;
11pub const OP_HALFSPACE4: u32 = 5;
12pub const OP_UNION: u32 = 6;
13pub const OP_INTERSECTION: u32 = 7;
14pub const OP_SUBTRACTION: u32 = 8;
15pub const OP_SMOOTH_UNION: u32 = 9;
16pub const OP_PUSH_POSE: u32 = 10;
17pub const OP_POP_POSE: u32 = 11;
18
19pub const MAX_STACK: usize = 32;
20pub const MAX_POSE_DEPTH: usize = 8;
21pub const MAX_PROGRAM_WORDS: usize = 1 << 20;
22
23pub const FIELD_FAR: f32 = 1e9;
24
25pub const FIELD_PROGRAM_ERROR: f32 = 1e-4;
27
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum FieldOp {
30 Sphere { radius: f32 },
31 Box { half_extents: [f32; 3] },
32 HalfSpace { normal: [f32; 3], offset: f32 },
33 HyperSphere { radius: f32 },
34 HalfSpace4 { normal: [f32; 4], offset: f32 },
35 Union,
36 Intersection,
37 Subtraction,
38 SmoothUnion { radius: f32 },
39 Transform,
40}
41
42impl FieldOp {
43 pub fn operands(self) -> usize {
44 match self {
45 FieldOp::Sphere { .. }
46 | FieldOp::Box { .. }
47 | FieldOp::HalfSpace { .. }
48 | FieldOp::HyperSphere { .. }
49 | FieldOp::HalfSpace4 { .. } => 0,
50 FieldOp::Transform => 1,
51 FieldOp::Union
52 | FieldOp::Intersection
53 | FieldOp::Subtraction
54 | FieldOp::SmoothUnion { .. } => 2,
55 }
56 }
57
58 pub fn opcode(self) -> u32 {
59 match self {
60 FieldOp::Sphere { .. } => OP_SPHERE,
61 FieldOp::Box { .. } => OP_BOX,
62 FieldOp::HalfSpace { .. } => OP_HALFSPACE,
63 FieldOp::HyperSphere { .. } => OP_HYPERSPHERE,
64 FieldOp::HalfSpace4 { .. } => OP_HALFSPACE4,
65 FieldOp::Union => OP_UNION,
66 FieldOp::Intersection => OP_INTERSECTION,
67 FieldOp::Subtraction => OP_SUBTRACTION,
68 FieldOp::SmoothUnion { .. } => OP_SMOOTH_UNION,
69 FieldOp::Transform => OP_PUSH_POSE,
70 }
71 }
72
73 pub fn result_kind(self, operands: &[FieldKind]) -> FieldKind {
74 let weakest = operands
75 .iter()
76 .copied()
77 .fold(FieldKind::ExactDistance, FieldKind::weaker);
78 match self {
79 FieldOp::Union
80 | FieldOp::Intersection
81 | FieldOp::Subtraction
82 | FieldOp::SmoothUnion { .. } => weakest.weaker(FieldKind::ConservativeBound),
83 _ => weakest,
84 }
85 }
86}
87
88#[repr(C)]
89#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
90pub struct FieldPrimitive {
91 pub frame: [[f32; 4]; 4],
92 pub translation: [f32; 4],
93 pub params: [f32; 4],
94}
95
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
97pub struct FieldCounts {
98 pub primitive_evals: u64,
99 pub instructions: u64,
100 pub node_visits: u64,
101 pub node_skips: u64,
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum FieldError {
106 Opcode(u32),
107 Truncated,
108 StackOverflow,
109 StackUnderflow,
110 PoseDepth,
111 Primitive(u32),
112}
113
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
115pub struct FieldCost {
116 pub changed_inputs: u32,
117 pub affected_dependencies: u32,
118 pub program_layout: u32,
119 pub index_maintenance: u32,
120 pub full_rebuild: bool,
121}
122
123impl FieldCost {
124 pub fn is_idle(self) -> bool {
125 self == FieldCost::default()
126 }
127}
128
129#[derive(Clone, Copy)]
131struct FieldInterval {
132 lo: f64,
133 hi: f64,
134}
135
136impl FieldInterval {
137 const WHOLE: Self = Self {
138 lo: f64::NEG_INFINITY,
139 hi: f64::INFINITY,
140 };
141
142 fn exact(value: f32) -> Self {
143 if value.is_finite() {
144 Self {
145 lo: value as f64,
146 hi: value as f64,
147 }
148 } else {
149 Self::WHOLE
150 }
151 }
152
153 fn exact_f64(value: f64) -> Self {
154 if value.is_finite() {
155 Self {
156 lo: value,
157 hi: value,
158 }
159 } else {
160 Self::WHOLE
161 }
162 }
163
164 fn rounded(lo: f64, hi: f64) -> Self {
165 if lo.is_nan() || hi.is_nan() || lo > hi || !lo.is_finite() || !hi.is_finite() {
166 Self::WHOLE
167 } else {
168 Self {
169 lo: lo.next_down(),
170 hi: hi.next_up(),
171 }
172 }
173 }
174
175 fn add(self, other: Self) -> Self {
176 Self::rounded(self.lo + other.lo, self.hi + other.hi)
177 }
178
179 fn sub(self, other: Self) -> Self {
180 Self::rounded(self.lo - other.hi, self.hi - other.lo)
181 }
182
183 fn mul(self, other: Self) -> Self {
184 let products = [
185 self.lo * other.lo,
186 self.lo * other.hi,
187 self.hi * other.lo,
188 self.hi * other.hi,
189 ];
190 if products.iter().any(|value| value.is_nan()) {
191 return Self::WHOLE;
192 }
193 let lo = products.into_iter().fold(f64::INFINITY, f64::min);
194 let hi = products.into_iter().fold(f64::NEG_INFINITY, f64::max);
195 Self::rounded(lo, hi)
196 }
197
198 fn div(self, other: Self) -> Self {
199 if other.lo <= 0.0 && other.hi >= 0.0 {
200 return Self::WHOLE;
201 }
202 self.mul(Self::rounded(1.0 / other.hi, 1.0 / other.lo))
203 }
204
205 fn neg(self) -> Self {
206 Self {
207 lo: -self.hi,
208 hi: -self.lo,
209 }
210 }
211
212 fn abs(self) -> Self {
213 if self.lo >= 0.0 {
214 self
215 } else if self.hi <= 0.0 {
216 self.neg()
217 } else {
218 Self {
219 lo: 0.0,
220 hi: (-self.lo).max(self.hi),
221 }
222 }
223 }
224
225 fn min(self, other: Self) -> Self {
226 Self {
227 lo: self.lo.min(other.lo),
228 hi: self.hi.min(other.hi),
229 }
230 }
231
232 fn max(self, other: Self) -> Self {
233 Self {
234 lo: self.lo.max(other.lo),
235 hi: self.hi.max(other.hi),
236 }
237 }
238
239 fn clamp(self, lo: f64, hi: f64) -> Self {
240 Self {
241 lo: self.lo.clamp(lo, hi),
242 hi: self.hi.clamp(lo, hi),
243 }
244 }
245
246 fn sqrt(self) -> Self {
247 if self.hi < 0.0 {
248 return Self::WHOLE;
249 }
250 Self::rounded(self.lo.max(0.0).sqrt(), self.hi.sqrt())
251 }
252}
253
254#[derive(Clone, Copy)]
255struct FrameBounds {
256 deviation: f64,
257 singular_min: f64,
258 singular_max: f64,
259 inverse_max: f64,
260}
261
262#[derive(Clone, Copy)]
263struct MetricBounds {
264 min: f64,
265 max: f64,
266}
267
268impl MetricBounds {
269 const IDENTITY: Self = Self { min: 1.0, max: 1.0 };
270
271 fn through(self, frame: [[f32; 4]; 4]) -> Option<Self> {
272 let frame = frame_bounds(frame)?;
273 let min = FieldInterval::exact_f64(self.min)
274 .mul(FieldInterval::exact_f64(frame.singular_min))
275 .lo;
276 let max = FieldInterval::exact_f64(self.max)
277 .mul(FieldInterval::exact_f64(frame.singular_max))
278 .hi;
279 (min > 0.0 && min.is_finite() && max.is_finite()).then_some(Self { min, max })
280 }
281
282 fn scaled(self, scale: FieldInterval) -> Option<Self> {
283 let min = FieldInterval::exact_f64(self.min).mul(scale).lo;
284 let max = FieldInterval::exact_f64(self.max).mul(scale).hi;
285 (min > 0.0 && min.is_finite() && max.is_finite()).then_some(Self { min, max })
286 }
287}
288
289fn upper_f32(value: f64) -> f32 {
290 if value.is_nan() || value > f32::MAX as f64 {
291 return f32::INFINITY;
292 }
293 if value < -(f32::MAX as f64) {
294 return -f32::MAX;
295 }
296 let rounded = value as f32;
297 if rounded as f64 >= value {
298 rounded
299 } else {
300 rounded.next_up()
301 }
302}
303
304fn lower_f32(value: f64) -> f32 {
305 if value.is_nan() || value < -(f32::MAX as f64) {
306 return f32::NEG_INFINITY;
307 }
308 if value > f32::MAX as f64 {
309 return f32::MAX;
310 }
311 let rounded = value as f32;
312 if rounded as f64 <= value {
313 rounded
314 } else {
315 rounded.next_down()
316 }
317}
318
319#[repr(C)]
320#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
321pub struct FieldNode {
322 pub center: [f32; 4],
323 pub radius: f32,
324 pub scale: f32,
325 pub start: u32,
326 pub end: u32,
327 pub escape: u32,
328}
329
330impl FieldNode {
331 pub fn is_bounded(self) -> bool {
332 self.radius >= 0.0 && self.scale > 0.0
333 }
334
335 pub fn is_leaf(self) -> bool {
336 self.end != 0
337 }
338
339 pub fn lower_bound(self, point: [f32; 4]) -> f32 {
340 let mut sum = FieldInterval::exact(0.0);
341 for (at, center) in point.iter().zip(self.center) {
342 let d = FieldInterval::exact(*at).sub(FieldInterval::exact(center));
343 sum = sum.add(d.mul(d));
344 }
345 let gap = sum.sqrt().sub(FieldInterval::exact(self.radius));
346 if gap.lo <= 0.0 {
347 f32::NEG_INFINITY
348 } else {
349 lower_f32(FieldInterval::exact(self.scale).mul(gap).lo)
350 }
351 }
352}
353
354#[derive(Clone, Copy, Debug, PartialEq)]
355struct Ball {
356 center: [f32; 4],
357 radius: f32,
358 scale: f32,
359}
360
361const UNBOUNDED: Ball = Ball {
362 center: [0.0; 4],
363 radius: -1.0,
364 scale: 0.0,
365};
366
367impl Ball {
368 fn bounded(self) -> bool {
369 self.radius >= 0.0 && self.scale > 0.0
370 }
371
372 fn expanded(self, by: f32) -> Self {
373 if !self.bounded() {
374 return UNBOUNDED;
375 }
376 let expansion = FieldInterval::exact(by.abs()).div(FieldInterval::exact(self.scale));
377 let radius = upper_f32(FieldInterval::exact(self.radius).add(expansion).hi);
378 if !radius.is_finite() {
379 return UNBOUNDED;
380 }
381 Ball {
382 center: self.center,
383 radius,
384 scale: self.scale,
385 }
386 }
387
388 fn smaller(self, other: Self) -> Self {
389 match (self.bounded(), other.bounded()) {
390 (true, true) if other.radius < self.radius => other,
391 (true, _) => self,
392 (false, true) => other,
393 _ => UNBOUNDED,
394 }
395 }
396
397 fn enclosing(self, other: Self) -> Self {
398 if !self.bounded() || !other.bounded() {
399 return UNBOUNDED;
400 }
401 let mut center = [0.0; 4];
402 for (axis, component) in center.iter_mut().enumerate() {
403 *component = ((self.center[axis] as f64 + other.center[axis] as f64) * 0.5) as f32;
404 }
405 let radius_for = |ball: Self| {
406 ball.center
407 .into_iter()
408 .zip(center)
409 .fold(FieldInterval::exact(0.0), |sum, (from, to)| {
410 let delta = FieldInterval::exact(from).sub(FieldInterval::exact(to));
411 sum.add(delta.mul(delta))
412 })
413 .sqrt()
414 .add(FieldInterval::exact(ball.radius))
415 };
416 let radius = upper_f32(radius_for(self).max(radius_for(other)).hi);
417 if !radius.is_finite() || center.iter().any(|component| !component.is_finite()) {
418 UNBOUNDED
419 } else {
420 Ball {
421 center,
422 radius,
423 scale: self.scale.min(other.scale),
424 }
425 }
426 }
427
428 fn through(self, prim: &FieldPrimitive) -> Self {
429 let Some(frame) = frame_bounds(prim.frame) else {
430 return UNBOUNDED;
431 };
432 if !self.bounded() {
433 return UNBOUNDED;
434 }
435 let mut center = [0.0; 4];
436 let mut center_error_sq = FieldInterval::exact(0.0);
437 for (axis, component) in center.iter_mut().enumerate() {
438 let mut exact = FieldInterval::exact(prim.translation[axis]);
439 for (component, row) in self.center.into_iter().zip(prim.frame) {
440 exact =
441 exact.add(FieldInterval::exact(row[axis]).mul(FieldInterval::exact(component)));
442 }
443 *component = ((exact.lo + exact.hi) * 0.5) as f32;
444 let error = FieldInterval::exact(*component).sub(exact).abs();
445 center_error_sq = center_error_sq.add(error.mul(error));
446 }
447 let center_norm = self
448 .center
449 .into_iter()
450 .fold(FieldInterval::exact(0.0), |sum, component| {
451 let component = FieldInterval::exact(component);
452 sum.add(component.mul(component))
453 })
454 .sqrt();
455 let inverse_norm = FieldInterval::exact_f64(frame.inverse_max);
456 let delta = FieldInterval::exact_f64(frame.deviation);
457 let radius = inverse_norm
458 .mul(FieldInterval::exact(self.radius))
459 .add(inverse_norm.mul(delta).mul(center_norm))
460 .add(center_error_sq.sqrt());
461 let radius = upper_f32(radius.hi);
462 let scale = lower_f32(
463 FieldInterval::exact(self.scale)
464 .mul(FieldInterval::exact_f64(frame.singular_min))
465 .lo,
466 );
467 if !radius.is_finite()
468 || !scale.is_finite()
469 || scale <= 0.0
470 || center.iter().any(|component| !component.is_finite())
471 {
472 UNBOUNDED
473 } else {
474 Ball {
475 center,
476 radius,
477 scale,
478 }
479 }
480 }
481}
482
483fn frame_bounds(frame: [[f32; 4]; 4]) -> Option<FrameBounds> {
485 let mut delta = 0.0f64;
486 for (left_index, left_column) in frame.iter().enumerate() {
487 let mut row_sum = FieldInterval::exact(0.0);
488 for (right_index, right_column) in frame.iter().enumerate() {
489 let mut gram = FieldInterval::exact(0.0);
490 for (&left, &right) in left_column.iter().zip(right_column) {
491 gram = gram.add(FieldInterval::exact(left).mul(FieldInterval::exact(right)));
492 }
493 row_sum = row_sum.add(
494 FieldInterval::exact(if left_index == right_index { 1.0 } else { 0.0 })
495 .sub(gram)
496 .abs(),
497 );
498 }
499 delta = delta.max(row_sum.hi);
500 }
501 if !delta.is_finite() || delta >= 1.0 {
502 return None;
503 }
504 let remaining = FieldInterval::exact_f64(1.0)
505 .sub(FieldInterval::exact_f64(delta))
506 .sqrt();
507 let inverse_norm = FieldInterval::exact_f64(1.0).div(remaining).hi;
508 let singular_min = remaining.lo;
509 let singular_max = FieldInterval::exact_f64(1.0)
510 .add(FieldInterval::exact_f64(delta))
511 .sqrt()
512 .hi;
513 (inverse_norm.is_finite() && singular_min > 0.0 && singular_max.is_finite()).then_some(
514 FrameBounds {
515 deviation: delta,
516 singular_min,
517 singular_max,
518 inverse_max: inverse_norm,
519 },
520 )
521}
522
523fn primitive_ball(op: u32, prim: &FieldPrimitive, extruded_w: bool) -> Ball {
524 let r = prim.params;
525 let local = match op {
526 OP_HYPERSPHERE => Ball {
527 center: [0.0; 4],
528 radius: r[0].abs(),
529 scale: 1.0,
530 },
531 OP_SPHERE if !extruded_w => Ball {
532 center: [0.0; 4],
533 radius: r[0].abs(),
534 scale: 1.0,
535 },
536 OP_BOX if !extruded_w => Ball {
537 center: [0.0; 4],
538 radius: upper_f32(
539 r[..3]
540 .iter()
541 .fold(FieldInterval::exact(0.0), |sum, &component| {
542 let component = FieldInterval::exact(component);
543 sum.add(component.mul(component))
544 })
545 .sqrt()
546 .hi,
547 ),
548 scale: 1.0,
549 },
550 _ => UNBOUNDED,
551 };
552 local.through(prim)
553}
554
555fn subtree_ball(primitives: &[FieldPrimitive], range: &[u32], extruded_w: bool) -> Ball {
556 if !range.len().is_multiple_of(2) {
557 return UNBOUNDED;
558 }
559 let mut stack = [UNBOUNDED; MAX_STACK];
560 let mut poses = [0u32; MAX_POSE_DEPTH];
561 let mut sp = 0usize;
562 let mut pp = 0usize;
563 for word in range.chunks_exact(2) {
564 let (op, arg) = (word[0], word[1]);
565 match op {
566 OP_SPHERE | OP_BOX | OP_HALFSPACE | OP_HYPERSPHERE | OP_HALFSPACE4 => {
567 let Some(prim) = primitives.get(arg as usize) else {
568 return UNBOUNDED;
569 };
570 if sp == MAX_STACK {
571 return UNBOUNDED;
572 }
573 stack[sp] = primitive_ball(op, prim, extruded_w);
574 sp += 1;
575 }
576 OP_UNION | OP_INTERSECTION | OP_SUBTRACTION | OP_SMOOTH_UNION => {
577 if sp < 2 {
578 return UNBOUNDED;
579 }
580 let b = stack[sp - 1];
581 let a = stack[sp - 2];
582 sp -= 1;
583 stack[sp - 1] = match op {
584 OP_UNION => a.enclosing(b),
585 OP_INTERSECTION => a.smaller(b),
586 OP_SUBTRACTION => a,
587 _ => a.enclosing(b).expanded(f32::from_bits(arg)),
588 };
589 }
590 OP_PUSH_POSE => {
591 if pp == MAX_POSE_DEPTH {
592 return UNBOUNDED;
593 }
594 poses[pp] = arg;
595 pp += 1;
596 }
597 OP_POP_POSE => {
598 if pp == 0 || sp == 0 {
599 return UNBOUNDED;
600 }
601 pp -= 1;
602 let Some(prim) = primitives.get(poses[pp] as usize) else {
603 return UNBOUNDED;
604 };
605 stack[sp - 1] = stack[sp - 1].through(prim);
606 }
607 _ => return UNBOUNDED,
608 }
609 }
610 if sp == 1 {
611 stack[0]
612 } else {
613 UNBOUNDED
614 }
615}
616
617#[derive(Clone, Copy, Debug)]
618struct Cut {
619 node: u32,
620 start: u32,
621 end: u32,
622 implicit: bool,
623 ball: Ball,
624}
625
626fn widest_axis(leaves: &[Cut]) -> usize {
627 let mut widest = 0;
628 let mut spread = -1.0f32;
629 for axis in 0..4 {
630 let mut lo = f32::INFINITY;
631 let mut hi = f32::NEG_INFINITY;
632 for leaf in leaves {
633 lo = lo.min(leaf.ball.center[axis]);
634 hi = hi.max(leaf.ball.center[axis]);
635 }
636 if hi - lo > spread {
637 spread = hi - lo;
638 widest = axis;
639 }
640 }
641 widest
642}
643
644fn build_tree(leaves: &mut [Cut], out: &mut Vec<FieldNode>) {
645 if leaves.len() == 1 {
646 let leaf = leaves[0];
647 out.push(FieldNode {
648 center: leaf.ball.center,
649 radius: leaf.ball.radius,
650 scale: leaf.ball.scale,
651 start: leaf.start,
652 end: leaf.end,
653 escape: 0,
654 });
655 let at = out.len() - 1;
656 out[at].escape = out.len() as u32;
657 return;
658 }
659 let at = out.len();
660 out.push(FieldNode::default());
661 let axis = widest_axis(leaves);
662 let mid = leaves.len() / 2;
663 leaves.select_nth_unstable_by(mid, |a, b| {
664 a.ball.center[axis]
665 .total_cmp(&b.ball.center[axis])
666 .then(a.start.cmp(&b.start))
667 });
668 let (left, right) = leaves.split_at_mut(mid);
669 build_tree(left, out);
670 let right_at = out[at + 1].escape as usize;
671 build_tree(right, out);
672 let ball = ball_of(out[at + 1]).enclosing(ball_of(out[right_at]));
673 out[at] = FieldNode {
674 center: ball.center,
675 radius: ball.radius,
676 scale: ball.scale,
677 start: 0,
678 end: 0,
679 escape: out.len() as u32,
680 };
681}
682
683fn ball_of(node: FieldNode) -> Ball {
684 Ball {
685 center: node.center,
686 radius: node.radius,
687 scale: node.scale,
688 }
689}
690
691fn local(prim: &FieldPrimitive, p: [f32; 4]) -> [f32; 4] {
692 let d = [
693 p[0] - prim.translation[0],
694 p[1] - prim.translation[1],
695 p[2] - prim.translation[2],
696 p[3] - prim.translation[3],
697 ];
698 let mut out = [0.0; 4];
699 for (slot, column) in out.iter_mut().zip(prim.frame) {
700 *slot = column[0] * d[0] + column[1] * d[1] + column[2] * d[2] + column[3] * d[3];
701 }
702 out
703}
704
705fn local_interval(prim: &FieldPrimitive, p: [FieldInterval; 4]) -> [FieldInterval; 4] {
706 let d = [
707 p[0].sub(FieldInterval::exact(prim.translation[0])),
708 p[1].sub(FieldInterval::exact(prim.translation[1])),
709 p[2].sub(FieldInterval::exact(prim.translation[2])),
710 p[3].sub(FieldInterval::exact(prim.translation[3])),
711 ];
712 let mut out = [FieldInterval::exact(0.0); 4];
713 for (slot, column) in out.iter_mut().zip(prim.frame) {
714 *slot = FieldInterval::exact(column[0])
715 .mul(d[0])
716 .add(FieldInterval::exact(column[1]).mul(d[1]))
717 .add(FieldInterval::exact(column[2]).mul(d[2]))
718 .add(FieldInterval::exact(column[3]).mul(d[3]));
719 }
720 out
721}
722
723fn primitive_distance(op: u32, prim: &FieldPrimitive, p: [f32; 4]) -> f32 {
724 let q = local(prim, p);
725 let r = prim.params;
726 match op {
727 OP_SPHERE => (q[0] * q[0] + q[1] * q[1] + q[2] * q[2]).sqrt() - r[0],
728 OP_BOX => {
729 let d = [q[0].abs() - r[0], q[1].abs() - r[1], q[2].abs() - r[2]];
730 let outside =
731 (d[0].max(0.0).powi(2) + d[1].max(0.0).powi(2) + d[2].max(0.0).powi(2)).sqrt();
732 outside + d[0].max(d[1]).max(d[2]).min(0.0)
733 }
734 OP_HALFSPACE => q[0] * r[0] + q[1] * r[1] + q[2] * r[2],
735 OP_HYPERSPHERE => (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt() - r[0],
736 OP_HALFSPACE4 => q[0] * r[0] + q[1] * r[1] + q[2] * r[2] + q[3] * r[3],
737 _ => FIELD_FAR,
738 }
739}
740
741fn primitive_interval(op: u32, prim: &FieldPrimitive, p: [FieldInterval; 4]) -> FieldInterval {
742 let q = local_interval(prim, p);
743 let r = prim.params.map(FieldInterval::exact);
744 match op {
745 OP_SPHERE => q[..3]
746 .iter()
747 .fold(FieldInterval::exact(0.0), |sum, &component| {
748 sum.add(component.mul(component))
749 })
750 .sqrt()
751 .sub(r[0]),
752 OP_BOX => {
753 let d = [
754 q[0].abs().sub(r[0]),
755 q[1].abs().sub(r[1]),
756 q[2].abs().sub(r[2]),
757 ];
758 let zero = FieldInterval::exact(0.0);
759 let outside = d
760 .iter()
761 .fold(zero, |sum, &component| {
762 let positive = component.max(zero);
763 sum.add(positive.mul(positive))
764 })
765 .sqrt();
766 outside.add(d[0].max(d[1]).max(d[2]).min(zero))
767 }
768 OP_HALFSPACE => q[0].mul(r[0]).add(q[1].mul(r[1])).add(q[2].mul(r[2])),
769 OP_HYPERSPHERE => q
770 .iter()
771 .fold(FieldInterval::exact(0.0), |sum, &component| {
772 sum.add(component.mul(component))
773 })
774 .sqrt()
775 .sub(r[0]),
776 OP_HALFSPACE4 => q[0]
777 .mul(r[0])
778 .add(q[1].mul(r[1]))
779 .add(q[2].mul(r[2]))
780 .add(q[3].mul(r[3])),
781 _ => FieldInterval::exact(FIELD_FAR),
782 }
783}
784
785fn primitive_metric_interval(
786 op: u32,
787 prim: &FieldPrimitive,
788 p: [FieldInterval; 4],
789 metric: MetricBounds,
790) -> FieldInterval {
791 let value = primitive_interval(op, prim, p);
792 let Some(mut metric) = metric.through(prim.frame) else {
793 return FieldInterval::WHOLE;
794 };
795 if matches!(op, OP_HALFSPACE | OP_HALFSPACE4) {
796 let dimensions = if op == OP_HALFSPACE { 3 } else { 4 };
797 let normal = prim.params[..dimensions]
798 .iter()
799 .fold(FieldInterval::exact(0.0), |sum, &component| {
800 let component = FieldInterval::exact(component);
801 sum.add(component.mul(component))
802 })
803 .sqrt();
804 let Some(scaled) = metric.scaled(normal) else {
805 return FieldInterval::WHOLE;
806 };
807 metric = scaled;
808 }
809 value.div(FieldInterval {
810 lo: metric.min,
811 hi: metric.max,
812 })
813}
814
815fn smooth_min(a: f32, b: f32, k: f32) -> f32 {
817 let h = (0.5 + 0.5 * (b - a) / k).clamp(0.0, 1.0);
818 (b * (1.0 - h) + a * h) - k * h * (1.0 - h)
819}
820
821fn smooth_min_interval(a: FieldInterval, b: FieldInterval, k: FieldInterval) -> FieldInterval {
822 let one = FieldInterval::exact(1.0);
823 let half = FieldInterval::exact(0.5);
824 let h = half.add(half.mul(b.sub(a)).div(k)).clamp(0.0, 1.0);
825 b.mul(one.sub(h))
826 .add(a.mul(h))
827 .sub(k.mul(h).mul(one.sub(h)))
828}
829
830fn run<const COUNT: bool>(
831 program: &[u32],
832 primitives: &[FieldPrimitive],
833 point: [f32; 4],
834 counts: &mut FieldCounts,
835) -> Result<f32, FieldError> {
836 if program.is_empty() {
837 return Ok(FIELD_FAR);
838 }
839 if !program.len().is_multiple_of(2) {
840 return Err(FieldError::Truncated);
841 }
842 let mut stack = [0.0f32; MAX_STACK];
843 let mut points = [[0.0f32; 4]; MAX_POSE_DEPTH];
844 let mut sp = 0usize;
845 let mut pp = 0usize;
846 let mut p = point;
847 for word in program.chunks_exact(2) {
848 let (op, arg) = (word[0], word[1]);
849 if COUNT {
850 counts.instructions += 1;
851 }
852 match op {
853 OP_SPHERE | OP_BOX | OP_HALFSPACE | OP_HYPERSPHERE | OP_HALFSPACE4 => {
854 let prim = primitives
855 .get(arg as usize)
856 .ok_or(FieldError::Primitive(arg))?;
857 if COUNT {
858 counts.primitive_evals += 1;
859 }
860 if sp == MAX_STACK {
861 return Err(FieldError::StackOverflow);
862 }
863 stack[sp] = primitive_distance(op, prim, p);
864 sp += 1;
865 }
866 OP_UNION | OP_INTERSECTION | OP_SUBTRACTION | OP_SMOOTH_UNION => {
867 if sp < 2 {
868 return Err(FieldError::StackUnderflow);
869 }
870 let b = stack[sp - 1];
871 let a = stack[sp - 2];
872 sp -= 1;
873 stack[sp - 1] = match op {
874 OP_UNION => a.min(b),
875 OP_INTERSECTION => a.max(b),
876 OP_SUBTRACTION => a.max(-b),
877 _ => smooth_min(a, b, f32::from_bits(arg)),
878 };
879 }
880 OP_PUSH_POSE => {
881 let prim = primitives
882 .get(arg as usize)
883 .ok_or(FieldError::Primitive(arg))?;
884 if pp == MAX_POSE_DEPTH {
885 return Err(FieldError::PoseDepth);
886 }
887 points[pp] = p;
888 pp += 1;
889 p = local(prim, p);
890 }
891 OP_POP_POSE => {
892 if pp == 0 {
893 return Err(FieldError::PoseDepth);
894 }
895 pp -= 1;
896 p = points[pp];
897 }
898 other => return Err(FieldError::Opcode(other)),
899 }
900 }
901 if sp != 1 {
902 return Err(FieldError::StackUnderflow);
903 }
904 Ok(stack[0])
905}
906
907fn run_interval(
908 program: &[u32],
909 primitives: &[FieldPrimitive],
910 point: [f32; 4],
911 exact_distance: bool,
912) -> Result<FieldInterval, FieldError> {
913 if program.is_empty() {
914 return Ok(FieldInterval::exact(FIELD_FAR));
915 }
916 if !program.len().is_multiple_of(2) {
917 return Err(FieldError::Truncated);
918 }
919 let mut stack = [FieldInterval::exact(0.0); MAX_STACK];
920 let mut points = [[FieldInterval::exact(0.0); 4]; MAX_POSE_DEPTH];
921 let mut metrics = [Some(MetricBounds::IDENTITY); MAX_POSE_DEPTH];
922 let mut sp = 0usize;
923 let mut pp = 0usize;
924 let mut p = point.map(FieldInterval::exact);
925 let mut metric = Some(MetricBounds::IDENTITY);
926 for word in program.chunks_exact(2) {
927 let (op, arg) = (word[0], word[1]);
928 match op {
929 OP_SPHERE | OP_BOX | OP_HALFSPACE | OP_HYPERSPHERE | OP_HALFSPACE4 => {
930 let prim = primitives
931 .get(arg as usize)
932 .ok_or(FieldError::Primitive(arg))?;
933 if sp == MAX_STACK {
934 return Err(FieldError::StackOverflow);
935 }
936 stack[sp] = if exact_distance {
937 match metric {
938 Some(metric) => primitive_metric_interval(op, prim, p, metric),
939 None => FieldInterval::WHOLE,
940 }
941 } else {
942 primitive_interval(op, prim, p)
943 };
944 sp += 1;
945 }
946 OP_UNION | OP_INTERSECTION | OP_SUBTRACTION | OP_SMOOTH_UNION => {
947 if sp < 2 {
948 return Err(FieldError::StackUnderflow);
949 }
950 let b = stack[sp - 1];
951 let a = stack[sp - 2];
952 sp -= 1;
953 stack[sp - 1] = match (exact_distance, op) {
954 (true, _) => FieldInterval::WHOLE,
955 (false, OP_UNION) => a.min(b),
956 (false, OP_INTERSECTION) => a.max(b),
957 (false, OP_SUBTRACTION) => a.max(b.neg()),
958 (false, _) => {
959 smooth_min_interval(a, b, FieldInterval::exact(f32::from_bits(arg)))
960 }
961 };
962 }
963 OP_PUSH_POSE => {
964 let prim = primitives
965 .get(arg as usize)
966 .ok_or(FieldError::Primitive(arg))?;
967 if pp == MAX_POSE_DEPTH {
968 return Err(FieldError::PoseDepth);
969 }
970 points[pp] = p;
971 metrics[pp] = metric;
972 pp += 1;
973 p = local_interval(prim, p);
974 metric = metric.and_then(|metric| metric.through(prim.frame));
975 }
976 OP_POP_POSE => {
977 if pp == 0 {
978 return Err(FieldError::PoseDepth);
979 }
980 pp -= 1;
981 p = points[pp];
982 metric = metrics[pp];
983 }
984 other => return Err(FieldError::Opcode(other)),
985 }
986 }
987 if sp != 1 {
988 return Err(FieldError::StackUnderflow);
989 }
990 Ok(stack[0])
991}
992
993fn forward_error(
994 program: &[u32],
995 primitives: &[FieldPrimitive],
996 point: [f32; 4],
997 value: f32,
998 kind: FieldKind,
999) -> Result<f32, FieldError> {
1000 let interval = run_interval(program, primitives, point, kind == FieldKind::ExactDistance)?;
1001 if !value.is_finite() || !interval.lo.is_finite() || !interval.hi.is_finite() {
1002 return Ok(f32::INFINITY);
1003 }
1004 let error = FieldInterval::exact(value).sub(interval).abs().hi;
1005 Ok(upper_f32(error).max(FIELD_PROGRAM_ERROR))
1006}
1007
1008fn traverse<const COUNT: bool>(
1009 program: &[u32],
1010 primitives: &[FieldPrimitive],
1011 nodes: &[FieldNode],
1012 point: [f32; 4],
1013 tolerance: f32,
1014 counts: &mut FieldCounts,
1015) -> Result<f32, FieldError> {
1016 if nodes.is_empty() {
1017 return run::<COUNT>(program, primitives, point, counts);
1018 }
1019 let mut best = f32::INFINITY;
1020 let mut index = 0usize;
1021 while index < nodes.len() {
1022 let node = nodes[index];
1023 if COUNT {
1024 counts.node_visits += 1;
1025 }
1026 if node.is_bounded() && node.lower_bound(point) > best + tolerance {
1027 if COUNT {
1028 counts.node_skips += 1;
1029 }
1030 index = node.escape as usize;
1031 continue;
1032 }
1033 if node.is_leaf() {
1034 let range = program
1035 .get(node.start as usize..node.end as usize)
1036 .ok_or(FieldError::Truncated)?;
1037 best = best.min(run::<COUNT>(range, primitives, point, counts)?);
1038 }
1039 index += 1;
1040 }
1041 Ok(best)
1042}
1043
1044pub fn evaluate(
1045 program: &[u32],
1046 primitives: &[FieldPrimitive],
1047 point: [f32; 4],
1048) -> Result<f32, FieldError> {
1049 run::<false>(program, primitives, point, &mut FieldCounts::default())
1050}
1051
1052pub fn evaluate_counted(
1053 program: &[u32],
1054 primitives: &[FieldPrimitive],
1055 point: [f32; 4],
1056 counts: &mut FieldCounts,
1057) -> Result<f32, FieldError> {
1058 run::<true>(program, primitives, point, counts)
1059}
1060
1061pub fn evaluate_bounded(
1063 program: &[u32],
1064 primitives: &[FieldPrimitive],
1065 nodes: &[FieldNode],
1066 point: [f32; 4],
1067 tolerance: f32,
1068) -> Result<f32, FieldError> {
1069 traverse::<false>(
1070 program,
1071 primitives,
1072 nodes,
1073 point,
1074 tolerance,
1075 &mut FieldCounts::default(),
1076 )
1077}
1078
1079pub fn evaluate_bounded_counted(
1080 program: &[u32],
1081 primitives: &[FieldPrimitive],
1082 nodes: &[FieldNode],
1083 point: [f32; 4],
1084 tolerance: f32,
1085 counts: &mut FieldCounts,
1086) -> Result<f32, FieldError> {
1087 traverse::<true>(program, primitives, nodes, point, tolerance, counts)
1088}
1089
1090pub(crate) fn evaluate_error(
1091 program: &[u32],
1092 primitives: &[FieldPrimitive],
1093 nodes: &[FieldNode],
1094 point: [f32; 4],
1095 kind: FieldKind,
1096) -> Result<f32, FieldError> {
1097 let value = traverse::<false>(
1098 program,
1099 primitives,
1100 nodes,
1101 point,
1102 0.0,
1103 &mut FieldCounts::default(),
1104 )?;
1105 forward_error(program, primitives, point, value, kind)
1106}
1107
1108const NONE: u32 = u32::MAX;
1109const MAX_OPERANDS: usize = 2;
1110
1111#[derive(Clone, Copy)]
1112struct Node {
1113 entity: Entity,
1114 op: FieldOp,
1115 declared: FieldKind,
1116 first_parent: u32,
1117 edge_start: u32,
1118 edge_len: u32,
1119 record: u32,
1120 marked: u32,
1121 pending: u32,
1122}
1123
1124#[derive(Clone, Copy)]
1125struct Frame {
1126 node: u32,
1127 cursor: u32,
1128 start: u32,
1129 cullable: bool,
1130}
1131
1132#[derive(Clone, Copy)]
1133struct Edge {
1134 operand: u32,
1135 operator: u32,
1136 next: u32,
1137 live: bool,
1138}
1139
1140const EMPTY_EDGE: Edge = Edge {
1141 operand: NONE,
1142 operator: NONE,
1143 next: NONE,
1144 live: false,
1145};
1146
1147pub(crate) struct FieldCompiler {
1148 program: FieldProgram,
1149 staged: FieldProgram,
1150 nodes: Vec<Node>,
1151 edges: Vec<Edge>,
1152 slots: Vec<u32>,
1153 roots: Vec<u32>,
1154 dfs: Vec<Frame>,
1155 cuts: Vec<Cut>,
1156 ordered: Vec<Cut>,
1157 kinds: Vec<FieldKind>,
1158 work: Vec<u32>,
1159 touched: Vec<u32>,
1160 patches: Vec<(usize, FieldPrimitive)>,
1161 fields_cursor: Cursor,
1162 poses_cursor: Cursor,
1163 dirty_stamp: u32,
1164 compiled: bool,
1165}
1166
1167impl Default for FieldCompiler {
1168 fn default() -> Self {
1169 Self::new()
1170 }
1171}
1172
1173impl FieldCompiler {
1174 pub(crate) fn new() -> Self {
1175 Self {
1176 program: FieldProgram::default(),
1177 staged: FieldProgram::default(),
1178 nodes: Vec::new(),
1179 edges: Vec::new(),
1180 slots: Vec::new(),
1181 roots: Vec::new(),
1182 dfs: Vec::new(),
1183 cuts: Vec::new(),
1184 ordered: Vec::new(),
1185 kinds: Vec::new(),
1186 work: Vec::new(),
1187 touched: Vec::new(),
1188 patches: Vec::new(),
1189 fields_cursor: Cursor::default(),
1190 poses_cursor: Cursor::default(),
1191 dirty_stamp: 0,
1192 compiled: false,
1193 }
1194 }
1195
1196 pub(crate) fn program(&self) -> &FieldProgram {
1197 &self.program
1198 }
1199
1200 pub(crate) fn invalidate(&mut self) {
1201 self.compiled = false;
1202 self.fields_cursor = Cursor::default();
1203 self.poses_cursor = Cursor::default();
1204 }
1205
1206 fn node_of(&self, entity: Entity) -> Option<u32> {
1207 let node = *self.slots.get(entity.key().slot() as usize)?;
1208 (node != NONE && self.nodes[node as usize].entity == entity).then_some(node)
1209 }
1210
1211 fn set_slot(&mut self, entity: Entity, node: u32) {
1212 let slot = entity.key().slot() as usize;
1213 if slot >= self.slots.len() {
1214 self.slots.resize(slot + 1, NONE);
1215 }
1216 self.slots[slot] = node;
1217 }
1218
1219 fn has_live_parent(&self, node: u32) -> bool {
1220 self.nodes[node as usize].first_parent != NONE
1221 }
1222
1223 fn link(&mut self, operator: u32, operands: &[Entity]) -> Result<(), DomainError> {
1224 let entity = self.nodes[operator as usize].entity;
1225 if operands.len() != self.nodes[operator as usize].op.operands() {
1226 return Err(DomainError::FieldArity(entity));
1227 }
1228 let edge_start = operator as usize * MAX_OPERANDS;
1229 self.nodes[operator as usize].edge_start = edge_start as u32;
1230 self.nodes[operator as usize].edge_len = operands.len() as u32;
1231 for (offset, &operand) in operands.iter().enumerate() {
1232 let target = self.node_of(operand).ok_or(DomainError::Stale(operand))?;
1233 let edge = (edge_start + offset) as u32;
1234 self.edges[edge as usize] = Edge {
1235 operand: target,
1236 operator,
1237 next: self.nodes[target as usize].first_parent,
1238 live: true,
1239 };
1240 self.nodes[target as usize].first_parent = edge;
1241 }
1242 Ok(())
1243 }
1244
1245 fn detach_parent(&mut self, operand: u32, remove: u32) {
1246 let mut previous = NONE;
1247 let mut edge = self.nodes[operand as usize].first_parent;
1248 while edge != NONE {
1249 let next = self.edges[edge as usize].next;
1250 if edge == remove {
1251 if previous == NONE {
1252 self.nodes[operand as usize].first_parent = next;
1253 } else {
1254 self.edges[previous as usize].next = next;
1255 }
1256 return;
1257 }
1258 previous = edge;
1259 edge = next;
1260 }
1261 }
1262
1263 fn unlink(&mut self, operator: u32) {
1264 let node = self.nodes[operator as usize];
1265 for edge in node.edge_start..node.edge_start + node.edge_len {
1266 let operand = self.edges[edge as usize].operand;
1267 self.detach_parent(operand, edge);
1268 self.edges[edge as usize] = EMPTY_EDGE;
1269 }
1270 self.nodes[operator as usize].edge_len = 0;
1271 }
1272
1273 fn operands_match(&self, operator: u32, operands: &[Entity]) -> bool {
1274 let node = self.nodes[operator as usize];
1275 if node.edge_len as usize != operands.len() {
1276 return false;
1277 }
1278 (0..node.edge_len).all(|i| {
1279 let edge = self.edges[(node.edge_start + i) as usize];
1280 edge.live && self.nodes[edge.operand as usize].entity == operands[i as usize]
1281 })
1282 }
1283
1284 fn rebuild_index(&mut self, fields: &Store<Field>) -> Result<u32, DomainError> {
1285 self.nodes.clear();
1286 self.edges.clear();
1287 self.slots.clear();
1288 for (entity, field) in fields.iter() {
1289 validate_op(field.op)?;
1290 let node = self.nodes.len() as u32;
1291 self.nodes.push(Node {
1292 entity,
1293 op: field.op,
1294 declared: field.kind,
1295 first_parent: NONE,
1296 edge_start: NONE,
1297 edge_len: 0,
1298 record: NONE,
1299 marked: 0,
1300 pending: 0,
1301 });
1302 self.set_slot(entity, node);
1303 }
1304 self.edges
1305 .resize(self.nodes.len() * MAX_OPERANDS, EMPTY_EDGE);
1306 let mut work = 0;
1307 for index in 0..self.nodes.len() as u32 {
1308 let entity = self.nodes[index as usize].entity;
1309 let field = fields.get(entity).ok_or(DomainError::Stale(entity))?;
1310 work += field.operands.len() as u32;
1311 self.link(index, &field.operands)?;
1312 }
1313 Ok(work)
1314 }
1315
1316 fn mark_ancestors(&mut self) -> u32 {
1317 self.dirty_stamp = match self.dirty_stamp.checked_add(1) {
1318 Some(stamp) => stamp,
1319 None => {
1320 for node in &mut self.nodes {
1321 node.marked = 0;
1322 }
1323 1
1324 }
1325 };
1326 let stamp = self.dirty_stamp;
1327 self.work.clear();
1328 for i in 0..self.touched.len() {
1329 let node = self.touched[i];
1330 if self.nodes[node as usize].marked != stamp {
1331 self.nodes[node as usize].marked = stamp;
1332 self.work.push(node);
1333 }
1334 }
1335 let mut affected = 0;
1336 let mut read = 0;
1337 while read < self.work.len() {
1338 let node = self.work[read];
1339 read += 1;
1340 let mut edge = self.nodes[node as usize].first_parent;
1341 while edge != NONE {
1342 let e = self.edges[edge as usize];
1343 if e.live && self.nodes[e.operator as usize].marked != stamp {
1344 self.nodes[e.operator as usize].marked = stamp;
1345 affected += 1;
1346 self.work.push(e.operator);
1347 }
1348 edge = e.next;
1349 }
1350 }
1351 affected
1352 }
1353
1354 fn record_for<S: DomainSpace>(
1355 &mut self,
1356 node: u32,
1357 space: &S,
1358 poses: &Store<Pose<S>>,
1359 ) -> Result<u32, DomainError> {
1360 if self.nodes[node as usize].record != NONE {
1361 return Ok(self.nodes[node as usize].record);
1362 }
1363 let (entity, op) = {
1364 let node = self.nodes[node as usize];
1365 (node.entity, node.op)
1366 };
1367 let record = self.program.primitives.len() as u32;
1368 self.program
1369 .primitives
1370 .push(primitive_record(space, poses, entity, op)?);
1371 self.nodes[node as usize].record = record;
1372 Ok(record)
1373 }
1374
1375 fn patch<S: DomainSpace>(
1376 &mut self,
1377 space: &S,
1378 poses: &Store<Pose<S>>,
1379 ) -> Result<u32, DomainError> {
1380 self.patches.clear();
1381 for i in 0..self.touched.len() {
1382 let node = self.nodes[self.touched[i] as usize];
1383 if node.record == NONE {
1384 continue;
1385 }
1386 self.patches.push((
1387 node.record as usize,
1388 primitive_record(space, poses, node.entity, node.op)?,
1389 ));
1390 }
1391 for &(record, primitive) in &self.patches {
1392 self.program.primitives[record] = primitive;
1393 }
1394 Ok(self.patches.len() as u32)
1395 }
1396
1397 fn emit<S: DomainSpace>(
1398 &mut self,
1399 space: &S,
1400 poses: &Store<Pose<S>>,
1401 ) -> Result<u32, DomainError> {
1402 std::mem::swap(&mut self.program, &mut self.staged);
1403 let result = self.emit_staged(space, poses);
1404 if result.is_err() {
1405 std::mem::swap(&mut self.program, &mut self.staged);
1406 }
1407 result
1408 }
1409
1410 fn emit_staged<S: DomainSpace>(
1411 &mut self,
1412 space: &S,
1413 poses: &Store<Pose<S>>,
1414 ) -> Result<u32, DomainError> {
1415 self.program.program.clear();
1416 self.program.primitives.clear();
1417 for node in &mut self.nodes {
1418 node.record = NONE;
1419 node.pending = node.edge_len;
1420 }
1421 self.check_acyclic()?;
1422 let mut roots = std::mem::take(&mut self.roots);
1423 roots.clear();
1424 roots.extend((0..self.nodes.len() as u32).filter(|&node| !self.has_live_parent(node)));
1425 roots.sort_unstable_by_key(|&node| self.nodes[node as usize].entity);
1426 self.roots = roots;
1427
1428 self.kinds.clear();
1429 self.dfs.clear();
1430 self.cuts.clear();
1431 let mut height = 0u32;
1432 let mut peak = 0u32;
1433 let mut pose_depth = 0usize;
1434 for index in 0..self.roots.len() {
1435 self.dfs.push(Frame {
1436 node: self.roots[index],
1437 cursor: 0,
1438 start: self.program.program.len() as u32,
1439 cullable: true,
1440 });
1441 while !self.dfs.is_empty() {
1442 let top = self.dfs.len() - 1;
1443 let frame = self.dfs[top];
1444 let (node, cursor) = (frame.node, frame.cursor);
1445 let op = self.nodes[node as usize].op;
1446 if cursor == 0 && matches!(op, FieldOp::Transform) {
1447 let record = self.record_for(node, space, poses)?;
1448 self.push_instruction(OP_PUSH_POSE, record)?;
1449 pose_depth += 1;
1450 if pose_depth > MAX_POSE_DEPTH {
1451 return Err(DomainError::Unsupported("field pose depth"));
1452 }
1453 }
1454 if (cursor as usize) < op.operands() {
1455 self.dfs[top].cursor = cursor + 1;
1456 let start = self.nodes[node as usize].edge_start;
1457 let child = self.edges[(start + cursor) as usize].operand;
1458 self.dfs.push(Frame {
1459 node: child,
1460 cursor: 0,
1461 start: self.program.program.len() as u32,
1462 cullable: frame.cullable && matches!(op, FieldOp::Union),
1463 });
1464 continue;
1465 }
1466 match op {
1467 FieldOp::Transform => {
1468 self.push_instruction(OP_POP_POSE, 0)?;
1469 pose_depth -= 1;
1470 let operand = self.kinds.pop().unwrap_or(FieldKind::ExactDistance);
1471 self.kinds.push(op.result_kind(&[operand]));
1472 }
1473 FieldOp::Union
1474 | FieldOp::Intersection
1475 | FieldOp::Subtraction
1476 | FieldOp::SmoothUnion { .. } => {
1477 let argument = match op {
1478 FieldOp::SmoothUnion { radius } => radius.to_bits(),
1479 _ => 0,
1480 };
1481 self.push_instruction(op.opcode(), argument)?;
1482 let right = self.kinds.pop().unwrap_or(FieldKind::ExactDistance);
1483 let left = self.kinds.pop().unwrap_or(FieldKind::ExactDistance);
1484 self.kinds.push(op.result_kind(&[left, right]));
1485 height -= 1;
1486 }
1487 _ => {
1488 let record = self.record_for(node, space, poses)?;
1489 self.push_instruction(op.opcode(), record)?;
1490 self.kinds.push(self.nodes[node as usize].declared);
1491 height += 1;
1492 peak = peak.max(height);
1493 }
1494 }
1495 if frame.cullable && !matches!(op, FieldOp::Union) {
1496 self.cuts.push(Cut {
1497 node,
1498 start: frame.start,
1499 end: self.program.program.len() as u32,
1500 implicit: self.kinds.last().copied() == Some(FieldKind::Implicit),
1501 ball: UNBOUNDED,
1502 });
1503 }
1504 self.dfs.pop();
1505 }
1506 if index > 0 {
1507 self.push_instruction(OP_UNION, 0)?;
1508 let right = self.kinds.pop().unwrap_or(FieldKind::ExactDistance);
1509 let left = self.kinds.pop().unwrap_or(FieldKind::ExactDistance);
1510 self.kinds.push(FieldOp::Union.result_kind(&[left, right]));
1511 height -= 1;
1512 }
1513 }
1514 if peak as usize > MAX_STACK {
1515 return Err(DomainError::Unsupported("field stack depth"));
1516 }
1517 self.program.stack = peak;
1518 self.program.kind = self.kinds.pop().unwrap_or(FieldKind::ConservativeBound);
1519 Ok((self.program.program.len() / 2) as u32 + self.program.primitives.len() as u32)
1520 }
1521
1522 fn rebuild_bounds(&mut self, extruded_w: bool, changed_only: bool) -> u32 {
1523 for cut in &mut self.cuts {
1524 if changed_only && self.nodes[cut.node as usize].marked != self.dirty_stamp {
1525 continue;
1526 }
1527 cut.ball = if cut.implicit {
1528 UNBOUNDED
1529 } else {
1530 match self
1531 .program
1532 .program
1533 .get(cut.start as usize..cut.end as usize)
1534 {
1535 Some(range) => subtree_ball(&self.program.primitives, range, extruded_w),
1536 None => UNBOUNDED,
1537 }
1538 };
1539 }
1540 let mut ordered = std::mem::take(&mut self.ordered);
1541 ordered.clear();
1542 ordered.extend(self.cuts.iter().copied().filter(|cut| cut.ball.bounded()));
1543 let bounded = ordered.len();
1544 ordered.extend(self.cuts.iter().copied().filter(|cut| !cut.ball.bounded()));
1545 self.program.nodes.clear();
1546 if bounded > 0 {
1547 build_tree(&mut ordered[..bounded], &mut self.program.nodes);
1548 for cut in &ordered[bounded..] {
1549 let escape = self.program.nodes.len() as u32 + 1;
1550 self.program.nodes.push(FieldNode {
1551 center: [0.0; 4],
1552 radius: UNBOUNDED.radius,
1553 scale: UNBOUNDED.scale,
1554 start: cut.start,
1555 end: cut.end,
1556 escape,
1557 });
1558 }
1559 }
1560 self.ordered = ordered;
1561 self.program.nodes.len() as u32
1562 }
1563
1564 fn check_acyclic(&mut self) -> Result<(), DomainError> {
1566 self.work.clear();
1567 for index in 0..self.nodes.len() as u32 {
1568 if self.nodes[index as usize].pending == 0 {
1569 self.work.push(index);
1570 }
1571 }
1572 let mut read = 0;
1573 while read < self.work.len() {
1574 let node = self.work[read];
1575 read += 1;
1576 let mut edge = self.nodes[node as usize].first_parent;
1577 while edge != NONE {
1578 let e = self.edges[edge as usize];
1579 if e.live {
1580 self.nodes[e.operator as usize].pending -= 1;
1581 if self.nodes[e.operator as usize].pending == 0 {
1582 self.work.push(e.operator);
1583 }
1584 }
1585 edge = e.next;
1586 }
1587 }
1588 match self.nodes.iter().find(|node| node.pending != 0) {
1589 Some(node) => Err(DomainError::FieldCycle(node.entity)),
1590 None => Ok(()),
1591 }
1592 }
1593
1594 fn push_instruction(&mut self, op: u32, argument: u32) -> Result<(), DomainError> {
1595 if self.program.program.len() + 2 > MAX_PROGRAM_WORDS {
1596 return Err(DomainError::Unsupported("field program size"));
1597 }
1598 self.program.program.push(op);
1599 self.program.program.push(argument);
1600 Ok(())
1601 }
1602
1603 pub(crate) fn compile<S: DomainSpace>(
1604 &mut self,
1605 space: &S,
1606 fields: &Store<Field>,
1607 poses: &Store<Pose<S>>,
1608 ) -> Result<FieldCost, DomainError> {
1609 if !space.is_chart_flat() {
1610 self.compiled = false;
1611 return Err(DomainError::Unsupported("curved field chart"));
1612 }
1613 let dimension = space.chart_dimension();
1614 let structural = fields.changed_since(&mut self.fields_cursor);
1615 let placed = poses.changed_since(&mut self.poses_cursor);
1616 if self.compiled && !structural && !placed {
1617 return Ok(FieldCost::default());
1618 }
1619 if dimension < 4
1620 && (!self.compiled || structural)
1621 && fields.iter().any(|(_, field)| {
1622 matches!(
1623 field.op,
1624 FieldOp::HyperSphere { .. } | FieldOp::HalfSpace4 { .. }
1625 )
1626 })
1627 {
1628 return Err(DomainError::Unsupported("field primitive dimension"));
1629 }
1630
1631 let mut cost = FieldCost::default();
1632 let mut rebuild = !self.compiled;
1633 self.compiled = false;
1634 self.touched.clear();
1635 if structural {
1636 let changes = fields.changes(&mut self.fields_cursor);
1637 rebuild |= changes.is_resync();
1638 for change in changes {
1639 match change {
1640 Change::Row(entity, _) => match self.node_of(entity) {
1641 Some(node) => {
1642 self.touched.push(node);
1643 cost.changed_inputs += 1;
1644 }
1645 None => {
1646 cost.changed_inputs += 1;
1647 rebuild = true;
1648 }
1649 },
1650 Change::Removed(_) => {
1651 cost.changed_inputs += 1;
1652 rebuild = true;
1653 }
1654 }
1655 }
1656 }
1657 let edited = self.touched.len();
1658 if placed {
1659 let changes = poses.changes(&mut self.poses_cursor);
1660 for change in changes {
1661 match change {
1662 Change::Row(entity, _) => {
1663 if let Some(node) = self.node_of(entity) {
1664 self.touched.push(node);
1665 cost.changed_inputs += 1;
1666 }
1667 }
1668 Change::Removed(removal) => {
1669 if let Some(node) = self.node_of(removal.entity) {
1670 self.touched.push(node);
1671 cost.changed_inputs += 1;
1672 }
1673 }
1674 }
1675 }
1676 }
1677
1678 if rebuild {
1679 cost.index_maintenance = self.rebuild_index(fields)?;
1680 cost.full_rebuild = true;
1681 self.touched.clear();
1682 } else {
1683 for i in 0..edited {
1684 let node = self.touched[i];
1685 let entity = self.nodes[node as usize].entity;
1686 let field = fields.get(entity).ok_or(DomainError::Stale(entity))?;
1687 validate_op(field.op)?;
1688 self.nodes[node as usize].op = field.op;
1689 self.nodes[node as usize].declared = field.kind;
1690 if !self.operands_match(node, &field.operands) {
1691 cost.index_maintenance += self.nodes[node as usize].edge_len;
1692 cost.index_maintenance += field.operands.len() as u32;
1693 self.unlink(node);
1694 self.link(node, &field.operands)?;
1695 }
1696 }
1697 }
1698
1699 cost.affected_dependencies = self.mark_ancestors();
1700 cost.program_layout = if rebuild || edited > 0 {
1701 self.emit(space, poses)?
1702 } else {
1703 self.patch(space, poses)?
1704 };
1705 cost.index_maintenance += self.rebuild_bounds(dimension > 3, !rebuild && edited == 0);
1706 self.program.dimension = dimension;
1707 self.compiled = true;
1708 Ok(cost)
1709 }
1710}
1711
1712fn primitive_record<S: DomainSpace>(
1713 space: &S,
1714 poses: &Store<Pose<S>>,
1715 entity: Entity,
1716 op: FieldOp,
1717) -> Result<FieldPrimitive, DomainError> {
1718 validate_op(op)?;
1719 let pose = poses.get(entity).ok_or(DomainError::Stale(entity))?;
1720 let chart = space.chart_pose(pose);
1721 let mut record = FieldPrimitive {
1722 frame: chart.frame,
1723 translation: chart.coordinates,
1724 params: [0.0; 4],
1725 };
1726 match op {
1727 FieldOp::Sphere { radius } | FieldOp::HyperSphere { radius } => record.params[0] = radius,
1728 FieldOp::Box { half_extents } => {
1729 record.params[..3].copy_from_slice(&half_extents);
1730 }
1731 FieldOp::HalfSpace { normal, offset } => {
1732 let plane = unit_plane([normal[0], normal[1], normal[2], 0.0], offset)?;
1733 record.params = plane.0;
1734 shift_along(&mut record, plane.0, plane.1);
1735 }
1736 FieldOp::HalfSpace4 { normal, offset } => {
1737 let plane = unit_plane(normal, offset)?;
1738 record.params = plane.0;
1739 shift_along(&mut record, plane.0, plane.1);
1740 }
1741 FieldOp::Transform => {}
1742 _ => return Err(DomainError::FieldArity(entity)),
1743 }
1744 if record
1745 .frame
1746 .iter()
1747 .flatten()
1748 .chain(&record.translation)
1749 .chain(&record.params)
1750 .any(|component| !component.is_finite())
1751 {
1752 return Err(DomainError::InvalidCoordinate("field primitive"));
1753 }
1754 Ok(record)
1755}
1756
1757fn validate_op(op: FieldOp) -> Result<(), DomainError> {
1758 match op {
1759 FieldOp::Sphere { radius } | FieldOp::HyperSphere { radius }
1760 if !radius.is_finite() || radius <= 0.0 =>
1761 {
1762 Err(DomainError::InvalidCoordinate("field radius"))
1763 }
1764 FieldOp::Box { half_extents }
1765 if half_extents
1766 .into_iter()
1767 .any(|extent| !extent.is_finite() || extent <= 0.0) =>
1768 {
1769 Err(DomainError::InvalidCoordinate("field half extent"))
1770 }
1771 FieldOp::HalfSpace { offset, .. } | FieldOp::HalfSpace4 { offset, .. }
1772 if !offset.is_finite() =>
1773 {
1774 Err(DomainError::InvalidCoordinate("field offset"))
1775 }
1776 FieldOp::SmoothUnion { radius } if !radius.is_finite() || radius <= 0.0 => {
1777 Err(DomainError::InvalidCoordinate("field smoothing radius"))
1778 }
1779 _ => Ok(()),
1780 }
1781}
1782
1783fn unit_plane(normal: [f32; 4], offset: f32) -> Result<([f32; 4], f32), DomainError> {
1784 let length = normal
1785 .iter()
1786 .map(|component| component * component)
1787 .sum::<f32>()
1788 .sqrt();
1789 if !length.is_finite() || length < 1e-6 || !offset.is_finite() {
1790 return Err(DomainError::InvalidCoordinate("field normal"));
1791 }
1792 Ok((normal.map(|component| component / length), offset / length))
1793}
1794
1795fn shift_along(record: &mut FieldPrimitive, normal: [f32; 4], offset: f32) {
1796 for axis in 0..4 {
1797 let mut sum = 0.0;
1798 for (component, column) in normal.iter().zip(record.frame) {
1799 sum += column[axis] * component;
1800 }
1801 record.translation[axis] += sum * offset;
1802 }
1803}
1804
1805#[cfg(test)]
1806mod tests {
1807 use loam_math::EuclideanR3;
1808 use loam_time::alloc::bytes_allocated_by;
1809
1810 use super::*;
1811 use crate::domain::{Domain, DomainBuilder, DomainId, TypedDomain};
1812 use crate::entity::{Entities, Entity, Epoch, RuntimeId, SceneId};
1813 use crate::store::DEFAULT_LOG_CAPACITY;
1814 use crate::view::Vec3;
1815
1816 fn test_entities() -> Entities {
1817 Entities::new(SceneId {
1818 runtime: RuntimeId::allocate(),
1819 epoch: Epoch::default(),
1820 })
1821 }
1822
1823 struct Fixture {
1824 entities: Entities,
1825 domain: TypedDomain<EuclideanR3>,
1826 }
1827
1828 impl Fixture {
1829 fn new() -> Self {
1830 let entities = test_entities();
1831 let domain = DomainBuilder::new("fields", EuclideanR3)
1832 .tracked(DEFAULT_LOG_CAPACITY)
1833 .fields()
1834 .build(DomainId::new(0), entities.scene());
1835 Self { entities, domain }
1836 }
1837
1838 fn spawn(&mut self, at: Vec3, kind: FieldKind, op: FieldOp, operands: &[Entity]) -> Entity {
1839 let entity = self.entities.spawn();
1840 self.domain
1841 .attach_pose(entity, Pose::at(at))
1842 .expect("pose row");
1843 self.domain
1844 .attach_field(
1845 entity,
1846 Field {
1847 kind,
1848 op,
1849 operands: operands.to_vec(),
1850 },
1851 )
1852 .expect("field row");
1853 entity
1854 }
1855
1856 fn compile(&mut self) -> Result<FieldCost, DomainError> {
1857 self.domain.compile_fields()
1858 }
1859
1860 fn compile_with(&self, compiler: &mut FieldCompiler) -> Result<FieldCost, DomainError> {
1861 compiler.compile(
1862 &EuclideanR3,
1863 self.domain.fields().expect("field store"),
1864 self.domain.poses(),
1865 )
1866 }
1867
1868 fn move_to(&mut self, entity: Entity, to: Vec3) {
1869 self.domain.set_point(entity, to).expect("pose row");
1870 }
1871 }
1872
1873 fn sphere(radius: f32) -> FieldOp {
1874 FieldOp::Sphere { radius }
1875 }
1876
1877 #[test]
1878 fn a_missing_operand_is_refused_and_names_the_operand() {
1879 let mut fixture = Fixture::new();
1880 let ghost = fixture.entities.spawn();
1881 fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(1.0), &[]);
1882 let root = fixture.spawn(
1883 Vec3::ZERO,
1884 FieldKind::ExactDistance,
1885 FieldOp::Transform,
1886 &[ghost],
1887 );
1888 assert_eq!(fixture.compile(), Err(DomainError::Stale(ghost)));
1889 assert_ne!(root, ghost);
1890 }
1891
1892 #[test]
1893 fn a_failed_compile_repeats_then_recovers_without_replacing_the_valid_program() {
1894 let mut fixture = Fixture::new();
1895 let leaf = fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(1.0), &[]);
1896 let up = fixture.spawn(
1897 Vec3::ZERO,
1898 FieldKind::ExactDistance,
1899 FieldOp::Union,
1900 &[leaf, leaf],
1901 );
1902 let down = fixture.spawn(
1903 Vec3::ZERO,
1904 FieldKind::ExactDistance,
1905 FieldOp::Union,
1906 &[up, leaf],
1907 );
1908 fixture.compile().expect("valid compile");
1909 let point = [3.0, 0.0, 0.0, 0.0];
1910 let valid = fixture.domain.field_program().evaluate(point).unwrap();
1911
1912 fixture.domain.field_mut(up).unwrap().operands = vec![down, leaf];
1913 assert_eq!(fixture.compile(), Err(DomainError::FieldCycle(up)));
1914 assert_eq!(fixture.compile(), Err(DomainError::FieldCycle(up)));
1915 assert_eq!(
1916 fixture.domain.field_program().evaluate(point).unwrap(),
1917 valid
1918 );
1919
1920 fixture.domain.field_mut(up).unwrap().operands = vec![leaf, leaf];
1921 assert!(fixture.compile().expect("repaired compile").full_rebuild);
1922 assert_eq!(
1923 fixture.domain.field_program().evaluate(point).unwrap(),
1924 valid
1925 );
1926
1927 let mut fields = Store::tracked(DEFAULT_LOG_CAPACITY);
1928 let mut poses = Store::tracked(DEFAULT_LOG_CAPACITY);
1929 fields.bind(fixture.entities.scene());
1930 poses.bind(fixture.entities.scene());
1931 fields
1932 .insert(
1933 &fixture.entities,
1934 leaf,
1935 Field {
1936 kind: FieldKind::ExactDistance,
1937 op: sphere(1.0),
1938 operands: Vec::new(),
1939 },
1940 )
1941 .unwrap();
1942 poses
1943 .insert(&fixture.entities, leaf, Pose::at(Vec3::ZERO))
1944 .unwrap();
1945 let mut compiler = FieldCompiler::new();
1946 compiler.compile(&EuclideanR3, &fields, &poses).unwrap();
1947 let pose_valid = compiler.program().evaluate(point).unwrap();
1948 poses.remove(leaf).unwrap();
1949 assert_eq!(
1950 compiler.compile(&EuclideanR3, &fields, &poses),
1951 Err(DomainError::Stale(leaf))
1952 );
1953 assert_eq!(
1954 compiler.compile(&EuclideanR3, &fields, &poses),
1955 Err(DomainError::Stale(leaf))
1956 );
1957 assert_eq!(compiler.program().evaluate(point).unwrap(), pose_valid);
1958 }
1959
1960 #[test]
1961 fn moving_one_primitive_marks_only_the_operators_above_it() {
1962 let mut fixture = Fixture::new();
1963 let left = fixture.spawn(-Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
1964 let right = fixture.spawn(Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
1965 let far = fixture.spawn(Vec3::Y * 4.0, FieldKind::ExactDistance, sphere(0.5), &[]);
1966 let inner = fixture.spawn(
1967 Vec3::ZERO,
1968 FieldKind::ExactDistance,
1969 FieldOp::Union,
1970 &[left, right],
1971 );
1972 fixture.spawn(
1973 Vec3::ZERO,
1974 FieldKind::ExactDistance,
1975 FieldOp::Union,
1976 &[inner, far],
1977 );
1978 assert!(fixture.compile().expect("first compile").full_rebuild);
1979 assert!(fixture.compile().expect("settle").is_idle());
1980
1981 fixture.move_to(left, Vec3::new(-2.0, 0.0, 0.0));
1982 let cost = fixture.compile().expect("incremental compile");
1983 assert_eq!(
1984 cost,
1985 FieldCost {
1986 changed_inputs: 1,
1987 affected_dependencies: 2,
1988 program_layout: 1,
1989 index_maintenance: 5,
1990 full_rebuild: false,
1991 }
1992 );
1993
1994 fixture.move_to(far, Vec3::new(0.0, 5.0, 0.0));
1995 let cost = fixture.compile().expect("incremental compile");
1996 assert_eq!(cost.affected_dependencies, 1);
1997 }
1998
1999 #[test]
2000 fn an_unchanged_store_compiles_with_no_work_and_no_rebuild() {
2001 let mut fixture = Fixture::new();
2002 let leaf = fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(1.0), &[]);
2003 fixture.spawn(
2004 Vec3::ZERO,
2005 FieldKind::ExactDistance,
2006 FieldOp::Transform,
2007 &[leaf],
2008 );
2009 assert!(fixture.compile().expect("first compile").full_rebuild);
2010 let words = fixture.domain.field_program().program.len();
2011 for _ in 0..4 {
2012 assert!(fixture.compile().expect("idle compile").is_idle());
2013 }
2014 assert_eq!(fixture.domain.field_program().program.len(), words);
2015 }
2016
2017 #[test]
2018 fn repeated_operand_edits_keep_index_storage_and_work_bounded() {
2019 let mut fixture = Fixture::new();
2020 let mut compiler = FieldCompiler::new();
2021 let left = fixture.spawn(-Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
2022 let right = fixture.spawn(Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
2023 let root = fixture.spawn(
2024 Vec3::ZERO,
2025 FieldKind::ExactDistance,
2026 FieldOp::Union,
2027 &[left, right],
2028 );
2029 fixture.compile_with(&mut compiler).expect("first compile");
2030 fixture.compile_with(&mut compiler).expect("settle");
2031
2032 let edge_slots = compiler.edges.len();
2033 for step in 0..128 {
2034 fixture.domain.field_mut(root).expect("root row").operands = if step % 2 == 0 {
2035 vec![right, left]
2036 } else {
2037 vec![left, right]
2038 };
2039 let cost = fixture.compile_with(&mut compiler).expect("operand edit");
2040 assert!(!cost.full_rebuild);
2041 assert_eq!(cost.index_maintenance, 7);
2042 assert!(cost.program_layout > 0);
2043 assert_eq!(compiler.edges.len(), edge_slots);
2044 }
2045
2046 fixture.move_to(left, -Vec3::Y);
2047 assert_eq!(
2048 fixture
2049 .compile_with(&mut compiler)
2050 .unwrap()
2051 .affected_dependencies,
2052 1
2053 );
2054 }
2055
2056 #[test]
2057 fn a_smooth_union_of_exact_leaves_reports_a_conservative_bound() {
2058 let mut fixture = Fixture::new();
2059 let left = fixture.spawn(-Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
2060 let right = fixture.spawn(Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
2061 fixture.spawn(
2062 Vec3::ZERO,
2063 FieldKind::ExactDistance,
2064 FieldOp::SmoothUnion { radius: 0.4 },
2065 &[left, right],
2066 );
2067 fixture.compile().expect("compile");
2068 let program = fixture.domain.field_program();
2069 assert_eq!(program.kind, FieldKind::ConservativeBound);
2070 assert_eq!(program.stack, 2);
2071
2072 let closed_form = |p: Vec3| {
2073 let a = (p - Vec3::new(-1.0, 0.0, 0.0)).length() - 0.5;
2074 let b = (p - Vec3::new(1.0, 0.0, 0.0)).length() - 0.5;
2075 smooth_min(a, b, 0.4)
2076 };
2077 for probe in [
2078 Vec3::ZERO,
2079 Vec3::new(0.3, 0.2, 0.0),
2080 Vec3::new(-1.4, 0.0, 0.1),
2081 ] {
2082 let (value, kind) = program
2083 .evaluate([probe.x, probe.y, probe.z, 0.0])
2084 .expect("well-formed program");
2085 assert_eq!(kind, FieldKind::ConservativeBound);
2086 assert!(
2087 (value - closed_form(probe)).abs() < 1e-6,
2088 "{probe:?}: interpreter {value} against closed form {}",
2089 closed_form(probe)
2090 );
2091 }
2092 }
2093
2094 #[test]
2095 fn invalid_field_dimensions_and_zero_smoothing_are_refused() {
2096 for op in [
2097 sphere(0.0),
2098 sphere(f32::INFINITY),
2099 FieldOp::Box {
2100 half_extents: [1.0, -1.0, 1.0],
2101 },
2102 FieldOp::HalfSpace {
2103 normal: Vec3::Y.to_array(),
2104 offset: f32::NAN,
2105 },
2106 ] {
2107 let mut fixture = Fixture::new();
2108 fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, op, &[]);
2109 assert!(matches!(
2110 fixture.compile(),
2111 Err(DomainError::InvalidCoordinate(_))
2112 ));
2113 }
2114
2115 let mut fixture = Fixture::new();
2116 let left = fixture.spawn(-Vec3::X, FieldKind::ExactDistance, sphere(1.0), &[]);
2117 let right = fixture.spawn(Vec3::X, FieldKind::ExactDistance, sphere(1.0), &[]);
2118 fixture.spawn(
2119 Vec3::ZERO,
2120 FieldKind::ExactDistance,
2121 FieldOp::SmoothUnion { radius: 0.0 },
2122 &[left, right],
2123 );
2124 assert_eq!(
2125 fixture.compile(),
2126 Err(DomainError::InvalidCoordinate("field smoothing radius"))
2127 );
2128 }
2129
2130 #[test]
2131 fn a_pose_transform_places_its_whole_subtree() {
2132 let mut fixture = Fixture::new();
2133 let leaf = fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(0.5), &[]);
2134 fixture.spawn(
2135 Vec3::new(3.0, 0.0, 0.0),
2136 FieldKind::ExactDistance,
2137 FieldOp::Transform,
2138 &[leaf],
2139 );
2140 fixture.compile().expect("compile");
2141 let program = fixture.domain.field_program();
2142 assert!((program.evaluate([3.0, 0.0, 0.0, 0.0]).expect("eval").0 + 0.5).abs() < 1e-6);
2143 assert!((program.evaluate([0.0, 0.0, 0.0, 0.0]).expect("eval").0 - 2.5).abs() < 1e-6);
2144 }
2145
2146 #[test]
2147 fn a_posed_half_space_matches_the_domain_isometry() {
2148 use loam_math::{EuclideanR4, Iso4Flat, IsometryGroup, Rotor4};
2149
2150 let half = std::f32::consts::FRAC_1_SQRT_2;
2151 let pose = Iso4Flat {
2152 rotation: Rotor4 {
2153 s: half,
2154 xy: half,
2155 ..Rotor4::IDENTITY
2156 },
2157 translation: crate::view::Vec4::new(0.3, -0.2, 1.5, 0.4),
2158 };
2159 let normal = [0.0, 2.0, 0.0, 0.0];
2160 let offset = 0.5;
2161
2162 let mut entities = test_entities();
2163 let mut domain = DomainBuilder::new("r4", EuclideanR4)
2164 .tracked(DEFAULT_LOG_CAPACITY)
2165 .fields()
2166 .build(DomainId::new(0), entities.scene());
2167 let entity = entities.spawn();
2168 domain
2169 .attach_pose(entity, Pose::from(pose))
2170 .expect("pose row");
2171 domain
2172 .attach_field(
2173 entity,
2174 Field {
2175 kind: FieldKind::ExactDistance,
2176 op: FieldOp::HalfSpace4 { normal, offset },
2177 operands: Vec::new(),
2178 },
2179 )
2180 .expect("field row");
2181 domain.compile_fields().expect("compile");
2182
2183 let space = EuclideanR4;
2184 let inverse = space.iso_inverse(pose);
2185 let unit = crate::view::Vec4::from_array(normal).normalize();
2186 for point in [
2187 crate::view::Vec4::ZERO,
2188 crate::view::Vec4::new(1.0, -0.5, 0.25, 0.75),
2189 crate::view::Vec4::new(-2.0, 3.0, -1.0, 0.0),
2190 ] {
2191 let expected = space.iso_apply(inverse, point).dot(unit) - offset / 2.0;
2192 let value = domain
2193 .field_program()
2194 .evaluate(point.to_array())
2195 .expect("eval")
2196 .0;
2197 assert!(
2198 (value - expected).abs() < 1e-5,
2199 "{point:?}: record transform gave {value}, the isometry gives {expected}"
2200 );
2201 }
2202 }
2203
2204 #[test]
2205 fn a_warmed_incremental_compile_allocates_nothing() {
2206 let mut fixture = Fixture::new();
2207 let leaves: Vec<Entity> = (0..32)
2208 .map(|i| {
2209 fixture.spawn(
2210 Vec3::new(i as f32, 0.0, 0.0),
2211 FieldKind::ExactDistance,
2212 sphere(0.4),
2213 &[],
2214 )
2215 })
2216 .collect();
2217 let mut level = leaves.clone();
2218 while level.len() > 1 {
2219 level = level
2220 .chunks(2)
2221 .map(|pair| {
2222 fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, FieldOp::Union, pair)
2223 })
2224 .collect();
2225 }
2226 for _ in 0..4 {
2227 fixture.move_to(leaves[0], Vec3::new(0.5, 0.0, 0.0));
2228 fixture.compile().expect("warm up");
2229 }
2230 let bytes = bytes_allocated_by(|| {
2231 for step in 0..32 {
2232 fixture.move_to(
2233 leaves[step % leaves.len()],
2234 Vec3::new(step as f32, 1.0, 0.0),
2235 );
2236 let cost = fixture.compile().expect("incremental compile");
2237 assert!(!cost.full_rebuild);
2238 assert_eq!(cost.program_layout, 1);
2239 }
2240 })
2241 .expect("the counting allocator is installed");
2242 assert_eq!(
2243 bytes, 0,
2244 "32 warmed incremental compiles allocated {bytes} bytes"
2245 );
2246 }
2247
2248 #[test]
2249 fn a_smooth_union_bound_that_drops_its_radius_culls_the_blend_surface() {
2250 let mut fixture = Fixture::new();
2251 let left = fixture.spawn(-Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
2252 let right = fixture.spawn(Vec3::X, FieldKind::ExactDistance, sphere(0.5), &[]);
2253 let blend = fixture.spawn(
2254 Vec3::ZERO,
2255 FieldKind::ExactDistance,
2256 FieldOp::SmoothUnion { radius: 5.0 },
2257 &[left, right],
2258 );
2259 let decoy = fixture.spawn(
2260 Vec3::new(-9.0, 4.0, 0.0),
2261 FieldKind::ExactDistance,
2262 sphere(6.55),
2263 &[],
2264 );
2265 fixture.spawn(
2266 Vec3::ZERO,
2267 FieldKind::ExactDistance,
2268 FieldOp::Union,
2269 &[blend, decoy],
2270 );
2271 fixture.compile().expect("compile");
2272
2273 let program = fixture.domain.field_program();
2274 let probe = [0.0, 4.0, 0.0, 0.0];
2275 let unculled = program.evaluate(probe).expect("eval").0;
2276 let culled = program.evaluate_bounded(probe, 0.0).expect("eval").0;
2277 assert!(
2278 (unculled - (17.0f32.sqrt() - 1.75)).abs() < 1e-5,
2279 "the blend value moved: {unculled}"
2280 );
2281 assert_eq!(
2282 culled, unculled,
2283 "the blend bulge reaches {unculled}, outside the unexpanded enclosing ball"
2284 );
2285 }
2286
2287 #[test]
2288 fn a_posed_subtrees_bound_moves_with_its_transform() {
2289 let mut fixture = Fixture::new();
2290 let leaf = fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(0.5), &[]);
2291 let placed = fixture.spawn(
2292 Vec3::new(10.0, 0.0, 0.0),
2293 FieldKind::ExactDistance,
2294 FieldOp::Transform,
2295 &[leaf],
2296 );
2297 let decoy = fixture.spawn(
2298 Vec3::new(-20.0, 0.0, 0.0),
2299 FieldKind::ExactDistance,
2300 sphere(28.0),
2301 &[],
2302 );
2303 fixture.spawn(
2304 Vec3::ZERO,
2305 FieldKind::ExactDistance,
2306 FieldOp::Union,
2307 &[decoy, placed],
2308 );
2309 fixture.compile().expect("compile");
2310
2311 let program = fixture.domain.field_program();
2312 let probe = [10.0, 0.0, 0.0, 0.0];
2313 let culled = program.evaluate_bounded(probe, 0.0).expect("eval").0;
2314 assert!(
2315 (culled + 0.5).abs() < 1e-6,
2316 "the posed sphere reads {culled}, not -0.5, so its bound stayed at the origin"
2317 );
2318 }
2319
2320 #[test]
2321 fn moving_a_shared_leaf_after_stamp_rollover_refreshes_each_dependent_cut_bound() {
2322 let mut fixture = Fixture::new();
2323 let mut compiler = FieldCompiler::new();
2324 let leaf = fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(0.5), &[]);
2325 let left = fixture.spawn(
2326 Vec3::new(-100.0, 0.0, 0.0),
2327 FieldKind::ExactDistance,
2328 FieldOp::Transform,
2329 &[leaf],
2330 );
2331 let right = fixture.spawn(
2332 Vec3::new(100.0, 0.0, 0.0),
2333 FieldKind::ExactDistance,
2334 FieldOp::Transform,
2335 &[leaf],
2336 );
2337 let left_decoy = fixture.spawn(
2338 Vec3::new(-120.0, 0.0, 0.0),
2339 FieldKind::ExactDistance,
2340 sphere(28.0),
2341 &[],
2342 );
2343 let right_decoy = fixture.spawn(
2344 Vec3::new(80.0, 0.0, 0.0),
2345 FieldKind::ExactDistance,
2346 sphere(28.0),
2347 &[],
2348 );
2349 let west = fixture.spawn(
2350 Vec3::ZERO,
2351 FieldKind::ExactDistance,
2352 FieldOp::Union,
2353 &[left_decoy, left],
2354 );
2355 let east = fixture.spawn(
2356 Vec3::ZERO,
2357 FieldKind::ExactDistance,
2358 FieldOp::Union,
2359 &[right_decoy, right],
2360 );
2361 fixture.spawn(
2362 Vec3::ZERO,
2363 FieldKind::ExactDistance,
2364 FieldOp::Union,
2365 &[west, east],
2366 );
2367 fixture
2368 .compile_with(&mut compiler)
2369 .expect("initial compile");
2370 fixture.move_to(left, Vec3::new(-99.0, 0.0, 0.0));
2371 fixture
2372 .compile_with(&mut compiler)
2373 .expect("prime parent stamp");
2374 compiler.dirty_stamp = u32::MAX;
2375 fixture.move_to(leaf, Vec3::X * 10.0);
2376 fixture
2377 .compile_with(&mut compiler)
2378 .expect("move shared leaf");
2379
2380 let program = compiler.program();
2381 for point in [[-89.0, 0.0, 0.0, 0.0], [110.0, 0.0, 0.0, 0.0]] {
2382 let value = program.evaluate_bounded(point, 0.0).expect("eval").0;
2383 assert!((value + 0.5).abs() < 1e-6, "{point:?} reads {value}");
2384 }
2385 }
2386
2387 #[test]
2388 fn an_unbounded_subtree_stays_in_the_traversal_and_still_contributes() {
2389 let mut fixture = Fixture::new();
2390 let ball = fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(0.5), &[]);
2391 let ground = fixture.spawn(
2392 Vec3::ZERO,
2393 FieldKind::ExactDistance,
2394 FieldOp::HalfSpace {
2395 normal: [0.0, 1.0, 0.0],
2396 offset: 0.0,
2397 },
2398 &[],
2399 );
2400 fixture.spawn(
2401 Vec3::ZERO,
2402 FieldKind::ExactDistance,
2403 FieldOp::Union,
2404 &[ball, ground],
2405 );
2406 fixture.compile().expect("compile");
2407
2408 let program = fixture.domain.field_program();
2409 let probe = [0.0, -3.0, 0.0, 0.0];
2410 let culled = program.evaluate_bounded(probe, 0.0).expect("eval").0;
2411 assert!(
2412 (culled + 3.0).abs() < 1e-6,
2413 "the half-space reads {culled}, not -3.0, so the unbounded subtree was dropped"
2414 );
2415 }
2416
2417 #[test]
2418 fn a_skipped_subtree_never_changes_the_value_the_unculled_program_returns() {
2419 let mut fixture = Fixture::new();
2420 let mut roots = Vec::new();
2421 for i in 0..64 {
2422 let t = i as f32;
2423 roots.push(fixture.spawn(
2424 Vec3::new((t * 0.37).sin() * 40.0, (t * 0.71).cos() * 40.0, t * 0.9),
2425 FieldKind::ExactDistance,
2426 sphere(0.4),
2427 &[],
2428 ));
2429 }
2430 let minuend = fixture.spawn(
2431 Vec3::new(2.0, 0.0, 0.0),
2432 FieldKind::ExactDistance,
2433 sphere(1.0),
2434 &[],
2435 );
2436 let subtrahend = fixture.spawn(
2437 Vec3::new(0.0, 30.0, 0.0),
2438 FieldKind::ExactDistance,
2439 sphere(1.0),
2440 &[],
2441 );
2442 roots.push(fixture.spawn(
2443 Vec3::ZERO,
2444 FieldKind::ExactDistance,
2445 FieldOp::Subtraction,
2446 &[minuend, subtrahend],
2447 ));
2448 let inner = fixture.spawn(
2449 Vec3::new(5.0, 0.0, 0.0),
2450 FieldKind::ExactDistance,
2451 sphere(1.0),
2452 &[],
2453 );
2454 let outer = fixture.spawn(
2455 Vec3::new(5.0, 0.0, 0.0),
2456 FieldKind::ExactDistance,
2457 sphere(3.0),
2458 &[],
2459 );
2460 roots.push(fixture.spawn(
2461 Vec3::ZERO,
2462 FieldKind::ExactDistance,
2463 FieldOp::Intersection,
2464 &[inner, outer],
2465 ));
2466 let mut level = roots;
2467 while level.len() > 1 {
2468 level = level
2469 .chunks(2)
2470 .map(|pair| {
2471 if pair.len() == 1 {
2472 pair[0]
2473 } else {
2474 fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, FieldOp::Union, pair)
2475 }
2476 })
2477 .collect();
2478 }
2479 fixture.compile().expect("compile");
2480
2481 let program = fixture.domain.field_program();
2482 let mut counts = FieldCounts::default();
2483 for i in 0..64 {
2484 let t = i as f32;
2485 let probe = [
2486 (t * 0.29).cos() * 12.0,
2487 (t * 0.53).sin() * 12.0,
2488 t * 0.5 - 8.0,
2489 0.0,
2490 ];
2491 let unculled = program.evaluate(probe).expect("eval").0;
2492 let culled = evaluate_bounded_counted(
2493 &program.program,
2494 &program.primitives,
2495 &program.nodes,
2496 probe,
2497 0.0,
2498 &mut counts,
2499 )
2500 .expect("eval");
2501 assert_eq!(culled, unculled, "probe {probe:?} changed under culling");
2502 }
2503 assert!(
2504 counts.node_skips > 0,
2505 "no subtree was skipped, so the traversal was never exercised"
2506 );
2507 }
2508
2509 #[test]
2510 fn distance_past_the_old_sentinel_matches_a_f64_oracle_with_reported_error() {
2511 use loam_shape::field::DistanceField;
2512
2513 let mut fixture = Fixture::new();
2514 fixture.compile().expect("empty compile");
2515 let empty = fixture.domain.field_program();
2516 assert_eq!(empty.field_kind(), FieldKind::ConservativeBound);
2517 assert_eq!(empty.evaluate([0.0; 4]).unwrap().0, FIELD_FAR);
2518
2519 fixture.spawn(Vec3::ZERO, FieldKind::ExactDistance, sphere(1.0), &[]);
2520 fixture.compile().expect("compile");
2521 let program = fixture.domain.field_program();
2522 let point = [2.0e9, 0.0, 0.0, 0.0];
2523 let unculled = program.evaluate(point).expect("eval").0;
2524 let culled = program.evaluate_bounded(point, 0.0).expect("eval").0;
2525 let oracle = 2_000_000_000.0f64 - 1.0;
2526
2527 assert!(culled > FIELD_FAR);
2528 assert_eq!(culled, unculled);
2529 assert!((culled as f64 - oracle).abs() <= program.error_at(point) as f64);
2530 }
2531
2532 #[test]
2533 fn an_implicit_union_of_exact_spheres_refuses_an_interior_witness() {
2534 use loam_shape::field::DistanceField;
2535
2536 let mut fixture = Fixture::new();
2537 fixture.spawn(-Vec3::X * 0.5, FieldKind::ExactDistance, sphere(1.0), &[]);
2538 fixture.spawn(Vec3::X * 0.5, FieldKind::ExactDistance, sphere(1.0), &[]);
2539 fixture.compile().expect("compile");
2540 let program = fixture.domain.field_program();
2541 assert_eq!(program.field_kind(), FieldKind::ConservativeBound);
2542
2543 let probe = Vec3::new(0.0, 0.2, 0.0);
2544 let bound = program.distance(probe.extend(0.0).to_array());
2545 let distance = 0.2 - 0.75f32.sqrt();
2546 assert!((bound + 0.461_483_5).abs() < 1e-6);
2547 assert!((bound - distance).abs() > FIELD_PROGRAM_ERROR);
2548 let gradient = program.gradient(probe.extend(0.0).to_array());
2549 let normal = Vec3::from_slice(&gradient[..3]).normalize();
2550 let witness = probe - normal * bound;
2551 assert!(
2552 program.distance(witness.extend(0.0).to_array()) < -FIELD_PROGRAM_ERROR,
2553 "the projected witness {witness:?} reached the union boundary"
2554 );
2555
2556 #[cfg(feature = "physics")]
2557 {
2558 use loam_physics::euclidean_r3::sphere_body_r3;
2559 use loam_physics::field_contact::sphere_against_field;
2560 use loam_physics::{FieldRefusal, World};
2561
2562 let mut world = World::new(EuclideanR3);
2563 let body = world.push_body(sphere_body_r3(probe, Vec3::ZERO, 0.1, 1.0).expect("body"));
2564 assert_eq!(
2565 sphere_against_field(
2566 &world.bodies()[body],
2567 world.geometry(),
2568 program,
2569 &EuclideanR3
2570 )
2571 .err(),
2572 Some(FieldRefusal::Kind(FieldKind::ConservativeBound))
2573 );
2574 }
2575 }
2576
2577 #[test]
2578 fn r4_fields_keep_off_slice_culling_and_refuse_r3_use() {
2579 use crate::view::Vec4;
2580 use loam_math::EuclideanR4;
2581 use loam_shape::field::DistanceField;
2582
2583 for op in [
2584 FieldOp::HyperSphere { radius: 1.0 },
2585 FieldOp::HalfSpace4 {
2586 normal: [1.0, 0.0, 0.0, 1.0],
2587 offset: 0.0,
2588 },
2589 ] {
2590 let mut r3 = Fixture::new();
2591 r3.spawn(Vec3::ZERO, FieldKind::ExactDistance, op, &[]);
2592 assert_eq!(
2593 r3.compile(),
2594 Err(DomainError::Unsupported("field primitive dimension"))
2595 );
2596 }
2597
2598 let mut entities = test_entities();
2599 let mut domain = DomainBuilder::new("r4", EuclideanR4)
2600 .tracked(DEFAULT_LOG_CAPACITY)
2601 .fields()
2602 .build(DomainId::new(0), entities.scene());
2603 let mut place = |at: Vec4, op: FieldOp, operands: &[Entity]| {
2604 let entity = entities.spawn();
2605 domain.attach_pose(entity, Pose::at(at)).expect("pose row");
2606 domain
2607 .attach_field(
2608 entity,
2609 Field {
2610 kind: FieldKind::ExactDistance,
2611 op,
2612 operands: operands.to_vec(),
2613 },
2614 )
2615 .expect("field row");
2616 entity
2617 };
2618 let shell = place(Vec4::new(0.0, 0.0, 0.0, 10.0), sphere(1.0), &[]);
2619 let near = place(
2620 Vec4::new(5.0, 0.0, 0.0, 0.0),
2621 FieldOp::HyperSphere { radius: 2.0 },
2622 &[],
2623 );
2624 let root = place(Vec4::ZERO, FieldOp::Union, &[near, shell]);
2625 domain.compile_fields().expect("compile");
2626
2627 let program = domain.field_program();
2628 let probe = [1.0, 0.0, 0.0, 0.0];
2629 let unculled = program.evaluate(probe).expect("eval").0;
2630 let culled = program.evaluate_bounded(probe, 0.0).expect("eval").0;
2631 assert!(
2632 unculled.abs() < 1e-6,
2633 "the unit sphere translated to w = 10 reads {unculled} on its own surface"
2634 );
2635 assert_eq!(
2636 culled, unculled,
2637 "the hierarchy culled a surface the program evaluates"
2638 );
2639
2640 domain.remove_field(root).expect("root field");
2641 domain.remove_field(shell).expect("shell field");
2642 domain
2643 .set_point(near, Vec4::new(0.0, 0.0, 0.0, 1.0))
2644 .expect("hypersphere pose");
2645 domain.compile_fields().expect("compile hypersphere");
2646
2647 let program = domain.field_program();
2648 let point = [1.0, 0.0, 0.0, 0.0];
2649 let r4_distance = program.distance(point);
2650 let r3_slice_distance = 1.0 - 3.0f32.sqrt();
2651 assert_eq!(program.dimension(), 4);
2652 assert_eq!(program.field_kind(), FieldKind::ExactDistance);
2653 assert!((r4_distance - r3_slice_distance).abs() > program.error_at(point));
2654
2655 #[cfg(feature = "physics")]
2656 {
2657 use loam_physics::euclidean_r3::sphere_body_r3;
2658 use loam_physics::field_contact::sphere_against_field;
2659 use loam_physics::{FieldRefusal, World};
2660
2661 let mut world = World::new(EuclideanR3);
2662 let body = world
2663 .push_body(sphere_body_r3(Vec3::X, Vec3::ZERO, 0.1, 1.0).expect("sphere body"));
2664 assert_eq!(
2665 sphere_against_field(
2666 &world.bodies()[body],
2667 world.geometry(),
2668 program,
2669 &EuclideanR3
2670 )
2671 .err(),
2672 Some(FieldRefusal::Dimension(4))
2673 );
2674 }
2675 }
2676
2677 #[test]
2678 fn an_intersection_of_exact_half_spaces_reports_a_bound_not_a_distance() {
2679 let mut fixture = Fixture::new();
2680 let west = fixture.spawn(
2681 Vec3::ZERO,
2682 FieldKind::ExactDistance,
2683 FieldOp::HalfSpace {
2684 normal: [-1.0, 0.0, 0.0],
2685 offset: 0.0,
2686 },
2687 &[],
2688 );
2689 let south = fixture.spawn(
2690 Vec3::ZERO,
2691 FieldKind::ExactDistance,
2692 FieldOp::HalfSpace {
2693 normal: [0.0, -1.0, 0.0],
2694 offset: 0.0,
2695 },
2696 &[],
2697 );
2698 fixture.spawn(
2699 Vec3::ZERO,
2700 FieldKind::ExactDistance,
2701 FieldOp::Intersection,
2702 &[west, south],
2703 );
2704 fixture.compile().expect("compile");
2705
2706 let program = fixture.domain.field_program();
2707 let corner = [-1.0, -1.0, 0.0, 0.0];
2708 let (value, kind) = program.evaluate(corner).expect("eval");
2709 assert_eq!(kind, FieldKind::ConservativeBound);
2710 assert!(
2711 (value - 1.0).abs() < 1e-6,
2712 "the intersection reads {value}, not the max of the two half-spaces"
2713 );
2714 assert!(
2715 value < 2.0f32.sqrt() - 1e-3,
2716 "{value} is not below the true distance {} to the quadrant",
2717 2.0f32.sqrt()
2718 );
2719
2720 #[cfg(feature = "physics")]
2721 {
2722 use loam_physics::euclidean_r3::sphere_body_r3;
2723 use loam_physics::field_contact::sphere_against_field;
2724 use loam_physics::{FieldRefusal, World};
2725
2726 let mut world = World::new(EuclideanR3);
2727 let body = world.push_body(
2728 sphere_body_r3(Vec3::new(-1.0, -1.0, 0.0), Vec3::ZERO, 1.2, 1.0).expect("body"),
2729 );
2730 assert_eq!(
2731 sphere_against_field(
2732 &world.bodies()[body],
2733 world.geometry(),
2734 program,
2735 &EuclideanR3
2736 )
2737 .err(),
2738 Some(FieldRefusal::Kind(FieldKind::ConservativeBound)),
2739 "the contact query read a bound as separation"
2740 );
2741 }
2742 }
2743
2744 #[test]
2745 fn a_curved_chart_refuses_to_compile_a_field() {
2746 use loam_math::HyperbolicH3;
2747 let entities = test_entities();
2748 let mut domain = DomainBuilder::new("h3", HyperbolicH3)
2749 .tracked(DEFAULT_LOG_CAPACITY)
2750 .fields()
2751 .build(DomainId::new(0), entities.scene());
2752 assert_eq!(
2753 domain.compile_fields(),
2754 Err(DomainError::Unsupported("curved field chart"))
2755 );
2756 }
2757}