1use std::collections::{HashMap, VecDeque};
10use std::f64::consts::{FRAC_PI_2, PI};
11
12use rand::SeedableRng;
13use rand_pcg::Pcg64;
14use serde::{Deserialize, Serialize};
15
16use crate::error::ShapeError;
17use crate::expr::{EvalCtx, Expr};
18use crate::model::{FaceProfile, Material, ShapeModel, Terminal, taper_to_profile};
19use crate::ops::{
20 AttachCase, AttachSelector, Axis, CarveSelector, CompTarget, FaceSelector, OffsetCase,
21 OffsetSelector, RoofConfig, RoofFaceSelector, RoofType, ShapeOp, SplitEntry, SplitSize,
22 SplitSlot,
23};
24use crate::query::{
25 register_scope_snap_planes, scope_obb_overlaps_terminal, snap_split_boundaries,
26};
27use crate::scope::{Quat, Scope, Vec3};
28
29const MAX_DEPTH: usize = 64;
31const MAX_QUEUE: usize = 100_000;
32const MAX_TERMINALS: usize = 100_000;
33const MAX_SPLIT_CHILDREN: usize = 4096;
36const MAX_SCATTER_POINTS: usize = 1024;
38
39pub use crate::ops::{RuleVariant, VariantSelector};
42
43struct WorkItem {
46 scope: Scope,
47 rule: String,
48 args: Vec<f64>,
52 depth: usize,
53 taper: f64,
57 face_profile_override: Option<FaceProfile>,
60 material: Option<Material>,
62 split_i: usize,
67 split_n: usize,
69 rng_state: u64,
71 label: Option<String>,
73}
74
75#[allow(clippy::too_many_arguments)]
77fn eval_expr(
78 e: &Expr,
79 scope: &Scope,
80 split_i: usize,
81 split_n: usize,
82 depth: usize,
83 params: &[(String, f64)],
84 globals: &HashMap<String, f64>,
85 rng: &mut Pcg64,
86) -> Result<f64, ShapeError> {
87 let mut ctx = EvalCtx {
88 scope_size: scope.size,
89 split_i: split_i as f64,
90 split_n: split_n as f64,
91 depth: depth as f64,
92 params,
93 globals,
94 rng,
95 };
96 e.eval(&mut ctx)
97}
98
99fn splitmix64(mut z: u64) -> u64 {
110 z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
111 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
112 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
113 z ^ (z >> 31)
114}
115
116fn fork_state(parent: u64, ordinal: u64) -> u64 {
118 splitmix64(parent ^ ordinal.wrapping_add(1).wrapping_mul(0x9E37_79B9_7F4A_7C15))
119}
120
121fn fnv1a(s: &str) -> u64 {
123 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
124 for b in s.as_bytes() {
125 h ^= u64::from(*b);
126 h = h.wrapping_mul(0x0000_0100_0000_01B3);
127 }
128 h
129}
130
131#[derive(Clone, Copy, Debug, PartialEq)]
135pub(crate) enum ResolvedSize {
136 Absolute(f64),
137 Relative(f64),
138 Floating(f64),
139}
140
141impl ResolvedSize {
142 fn value(self) -> f64 {
143 match self {
144 ResolvedSize::Absolute(v) | ResolvedSize::Relative(v) | ResolvedSize::Floating(v) => v,
145 }
146 }
147}
148
149fn resolve_split_sizes(slots: &[ResolvedSize], total_dim: f64) -> Result<Vec<f64>, ShapeError> {
151 if slots.is_empty() {
152 return Err(ShapeError::EmptySplit);
153 }
154 for slot in slots {
155 let v = slot.value();
156 if !v.is_finite() || v <= 0.0 {
157 return match slot {
158 ResolvedSize::Floating(v) => Err(ShapeError::InvalidFloatingSize(*v)),
159 _ => Err(ShapeError::InvalidNumericValue),
160 };
161 }
162 }
163
164 let mut fixed: Vec<Option<f64>> = Vec::with_capacity(slots.len());
165 let mut used = 0.0_f64;
166 let mut float_weight_total = 0.0_f64;
167
168 for slot in slots {
169 match *slot {
170 ResolvedSize::Absolute(v) => {
171 fixed.push(Some(v));
172 used += v;
173 }
174 ResolvedSize::Relative(t) => {
175 let s = total_dim * t;
176 fixed.push(Some(s));
177 used += s;
178 }
179 ResolvedSize::Floating(w) => {
180 fixed.push(None);
181 float_weight_total += w;
182 }
183 }
184 }
185
186 if !used.is_finite() {
188 return Err(ShapeError::InvalidNumericValue);
189 }
190
191 if used > total_dim + 1e-9 {
192 return Err(ShapeError::SplitOverflow(total_dim));
193 }
194
195 let remaining = (total_dim - used).max(0.0);
196
197 if !float_weight_total.is_finite() {
199 return Err(ShapeError::InvalidNumericValue);
200 }
201
202 let mut result = Vec::with_capacity(slots.len());
203 for (i, slot) in slots.iter().enumerate() {
204 match fixed[i] {
205 Some(v) => result.push(v),
206 None => {
207 let w = match *slot {
208 ResolvedSize::Floating(w) => w,
209 _ => unreachable!(),
210 };
211 if float_weight_total <= 0.0 {
212 return Err(ShapeError::NoFloatingSlots);
213 }
214 result.push(remaining * (w / float_weight_total));
217 }
218 }
219 }
220
221 Ok(result)
222}
223
224fn slice_scope(parent: &Scope, axis: Axis, offset: f64, size: f64) -> Scope {
229 let offset_vec = match axis {
230 Axis::X => Vec3::new(offset, 0.0, 0.0),
231 Axis::Y => Vec3::new(0.0, offset, 0.0),
232 Axis::Z => Vec3::new(0.0, 0.0, offset),
233 };
234
235 let child_position = parent.position + parent.rotation * offset_vec;
236
237 let child_size = match axis {
238 Axis::X => Vec3::new(size, parent.size.y, parent.size.z),
239 Axis::Y => Vec3::new(parent.size.x, size, parent.size.z),
240 Axis::Z => Vec3::new(parent.size.x, parent.size.y, size),
241 };
242
243 Scope::new(child_position, parent.rotation, child_size)
244}
245
246fn face_descs(scope_size: Vec3) -> [(FaceSelector, Vec3, Vec3, Quat); 6] {
260 let sx = scope_size.x;
261 let sy = scope_size.y;
262 let sz = scope_size.z;
263
264 [
265 (
268 FaceSelector::Bottom,
269 Vec3::new(0.0, 0.0, 0.0),
270 Vec3::new(sx, sz, 0.0),
271 Quat::from_axis_angle(Vec3::X, FRAC_PI_2),
272 ),
273 (
277 FaceSelector::Top,
278 Vec3::new(0.0, sy, sz),
279 Vec3::new(sx, sz, 0.0),
280 Quat::from_axis_angle(Vec3::X, -FRAC_PI_2),
281 ),
282 (
286 FaceSelector::Front,
287 Vec3::new(sx, 0.0, 0.0),
288 Vec3::new(sx, sy, 0.0),
289 Quat::from_axis_angle(Vec3::Y, PI),
290 ),
291 (
295 FaceSelector::Back,
296 Vec3::new(0.0, 0.0, sz),
297 Vec3::new(sx, sy, 0.0),
298 Quat::IDENTITY,
299 ),
300 (
304 FaceSelector::Left,
305 Vec3::new(0.0, 0.0, 0.0),
306 Vec3::new(sz, sy, 0.0),
307 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
308 ),
309 (
313 FaceSelector::Right,
314 Vec3::new(sx, 0.0, sz),
315 Vec3::new(sz, sy, 0.0),
316 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
317 ),
318 ]
319}
320
321fn find_face_rule(
323 selector: FaceSelector,
324 cases: &[crate::ops::CompFaceCase],
325) -> Option<&crate::ops::RuleCall> {
326 if let Some(case) = cases.iter().find(|c| c.selector == selector) {
327 return Some(&case.rule);
328 }
329 let is_side = matches!(
330 selector,
331 FaceSelector::Front | FaceSelector::Back | FaceSelector::Left | FaceSelector::Right
332 );
333 if is_side && let Some(case) = cases.iter().find(|c| c.selector == FaceSelector::Side) {
334 return Some(&case.rule);
335 }
336 cases
337 .iter()
338 .find(|c| c.selector == FaceSelector::All)
339 .map(|c| &c.rule)
340}
341
342fn axis_vec(axis: Axis) -> Vec3 {
344 match axis {
345 Axis::X => Vec3::X,
346 Axis::Y => Vec3::Y,
347 Axis::Z => Vec3::Z,
348 }
349}
350
351fn find_offset_rule(
353 selector: OffsetSelector,
354 cases: &[OffsetCase],
355) -> Option<&crate::ops::RuleCall> {
356 cases
357 .iter()
358 .find(|c| c.selector == selector)
359 .or_else(|| cases.iter().find(|c| c.selector == OffsetSelector::All))
360 .map(|c| &c.rule)
361}
362
363pub(crate) struct ResolvedRoofCase {
366 selector: RoofFaceSelector,
367 name: String,
368 args: Vec<f64>,
369}
370
371fn find_roof_rule(
373 selector: RoofFaceSelector,
374 cases: &[ResolvedRoofCase],
375) -> Option<&ResolvedRoofCase> {
376 cases
377 .iter()
378 .find(|c| c.selector == selector)
379 .or_else(|| cases.iter().find(|c| c.selector == RoofFaceSelector::All))
380}
381
382fn edge_descs(size: Vec3) -> Vec<(crate::ops::EdgeSelector, Vec3, Vec3, f64)> {
387 use crate::ops::EdgeSelector as E;
388 let (sx, sy, sz) = (size.x, size.y, size.z);
389 if sz.abs() < 1e-9 && sy.abs() > 1e-9 {
390 return vec![
392 (E::Bottom, Vec3::new(0.0, 0.0, 0.0), Vec3::X, sx),
393 (E::Top, Vec3::new(0.0, sy, 0.0), Vec3::X, sx),
394 (E::Vertical, Vec3::new(0.0, 0.0, 0.0), Vec3::Y, sy),
395 (E::Vertical, Vec3::new(sx, 0.0, 0.0), Vec3::Y, sy),
396 ];
397 }
398 let mut v = Vec::with_capacity(12);
399 for (cx, cz) in [(0.0, 0.0), (sx, 0.0), (sx, sz), (0.0, sz)] {
401 v.push((E::Vertical, Vec3::new(cx, 0.0, cz), Vec3::Y, sy));
402 }
403 for (class, y) in [(E::Bottom, 0.0), (E::Top, sy)] {
405 v.push((class, Vec3::new(0.0, y, 0.0), Vec3::X, sx));
406 v.push((class, Vec3::new(0.0, y, sz), Vec3::X, sx));
407 v.push((class, Vec3::new(0.0, y, 0.0), Vec3::Z, sz));
408 v.push((class, Vec3::new(sx, y, 0.0), Vec3::Z, sz));
409 }
410 v
411}
412
413fn find_edge_rule(
416 class: crate::ops::EdgeSelector,
417 cases: &[crate::ops::CompEdgeCase],
418) -> Option<&crate::ops::RuleCall> {
419 use crate::ops::EdgeSelector as E;
420 if let Some(c) = cases.iter().find(|c| c.selector == class) {
421 return Some(&c.rule);
422 }
423 if matches!(class, E::Top | E::Bottom)
424 && let Some(c) = cases.iter().find(|c| c.selector == E::Horizontal)
425 {
426 return Some(&c.rule);
427 }
428 cases.iter().find(|c| c.selector == E::All).map(|c| &c.rule)
429}
430
431fn find_carve_rule(
433 selector: crate::ops::CarveSelector,
434 cases: &[crate::ops::CarveCase],
435) -> Option<&crate::ops::CarveCase> {
436 cases.iter().find(|c| c.selector == selector).or_else(|| {
437 cases
438 .iter()
439 .find(|c| c.selector == crate::ops::CarveSelector::All)
440 })
441}
442
443fn mirror_profile(p: &FaceProfile) -> FaceProfile {
445 match p {
446 FaceProfile::Rectangle => FaceProfile::Rectangle,
447 FaceProfile::Taper(t) => FaceProfile::Taper(*t),
448 FaceProfile::Triangle { peak_offset } => FaceProfile::Triangle {
449 peak_offset: 1.0 - peak_offset,
450 },
451 FaceProfile::Trapezoid {
452 top_width,
453 offset_x,
454 } => FaceProfile::Trapezoid {
455 top_width: *top_width,
456 offset_x: 1.0 - top_width - offset_x,
457 },
458 FaceProfile::Polygon(pts) => FaceProfile::Polygon(
459 pts.iter()
460 .map(|v| glam::DVec2::new(1.0 - v.x, v.y))
461 .collect(),
462 ),
463 }
464}
465
466fn find_attach_rule(
468 selector: AttachSelector,
469 cases: &[AttachCase],
470) -> Option<&crate::ops::RuleCall> {
471 cases
472 .iter()
473 .find(|c| c.selector == selector)
474 .or_else(|| cases.iter().find(|c| c.selector == AttachSelector::All))
475 .map(|c| &c.rule)
476}
477
478fn roof_slope_rotations(alpha: f64) -> (Quat, Quat, Quat, Quat) {
491 let front_rot =
494 Quat::from_axis_angle(Vec3::X, FRAC_PI_2 - alpha) * Quat::from_axis_angle(Vec3::Y, PI);
495 let back_rot = Quat::from_axis_angle(Vec3::X, alpha - FRAC_PI_2);
498 let left_rot = Quat::from_axis_angle(Vec3::Z, alpha - FRAC_PI_2)
501 * Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2);
502 let right_rot = Quat::from_axis_angle(Vec3::Z, FRAC_PI_2 - alpha)
505 * Quat::from_axis_angle(Vec3::Y, FRAC_PI_2);
506 (front_rot, back_rot, left_rot, right_rot)
507}
508
509fn slope_to_wall_rot(slope_rot: Quat) -> Option<Quat> {
518 let eave_x = slope_rot * Vec3::X;
519 let slope_z = slope_rot * Vec3::Z;
520 let mut wall_z = Vec3::new(slope_z.x, 0.0, slope_z.z);
521 if wall_z.length_squared() < 1e-12 {
522 return None;
523 }
524 wall_z = wall_z.normalize();
525 let mut wall_x = Vec3::new(eave_x.x, 0.0, eave_x.z);
526 if wall_x.length_squared() < 1e-12 {
527 return None;
528 }
529 wall_x = wall_x.normalize();
530 let wall_y = wall_z.cross(wall_x);
531 let mat = glam::DMat3::from_cols(wall_x, wall_y, wall_z);
532 Some(Quat::from_mat3(&mat).normalize())
533}
534
535#[allow(clippy::too_many_arguments)]
544fn apply_roof(
545 config: &RoofConfig,
546 cases: &[ResolvedRoofCase],
547 scope: &Scope,
548 depth: usize,
549 material: &Option<Material>,
550 queue: &mut VecDeque<WorkItem>,
551 model: &mut ShapeModel,
552 max_terminals: usize,
553 split_i: usize,
554 split_n: usize,
555 rng_state: u64,
556 ridge_axis: Option<Axis>,
557 label: &Option<String>,
558) -> Result<(), ShapeError> {
559 if !config.pitch.is_finite() || config.pitch <= 0.0 || config.pitch >= 90.0 {
560 return Err(ShapeError::InvalidRoofAngle(config.pitch));
561 }
562 if !config.overhang.is_finite() || config.overhang < 0.0 {
563 return Err(ShapeError::InvalidNumericValue);
564 }
565
566 let sx = scope.size.x;
567 let sz = scope.size.z;
568 let ridge_x = match ridge_axis {
572 Some(Axis::X) => true,
573 Some(_) => false,
574 None => sx >= sz,
575 };
576 let o = config.overhang;
577 let alpha = config.pitch.to_radians();
578 let cos_a = alpha.cos();
579 let tan_a = alpha.tan();
580 let (front_rot, back_rot, left_rot, right_rot) = roof_slope_rotations(alpha);
581 let y_anchor = -o * tan_a;
583 let fb_len = (sz / 2.0 + o) / cos_a;
585 let lr_len = (sx / 2.0 + o) / cos_a;
586 let h = (sz / 2.0) * tan_a;
588
589 if !fb_len.is_finite() || !lr_len.is_finite() || !h.is_finite() {
590 return Err(ShapeError::InvalidNumericValue);
591 }
592
593 type Panel = (Vec3, Vec3, Quat, RoofFaceSelector, FaceProfile);
595
596 let mut panels: Vec<Panel> = match config.roof_type {
597 RoofType::Flat => vec![(
600 Vec3::new(0.0, 0.0, sz),
601 Vec3::new(sx, sz, 0.0),
602 Quat::from_axis_angle(Vec3::X, -FRAC_PI_2),
603 RoofFaceSelector::Slope,
604 FaceProfile::Rectangle,
605 )],
606
607 RoofType::Shed => {
610 let shed_h = sz * tan_a;
611 vec![
612 (
613 Vec3::new(sx + o, y_anchor, -o),
614 Vec3::new(sx + 2.0 * o, (sz + 2.0 * o) / cos_a, 0.0),
615 front_rot,
616 RoofFaceSelector::Slope,
617 FaceProfile::Rectangle,
618 ),
619 (
622 Vec3::new(0.0, 0.0, sz),
623 Vec3::new(sx, shed_h, 0.0),
624 Quat::IDENTITY,
625 RoofFaceSelector::Back,
626 FaceProfile::Rectangle,
627 ),
628 (
629 Vec3::new(0.0, 0.0, 0.0),
630 Vec3::new(sz, shed_h, 0.0),
631 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
632 RoofFaceSelector::GableEnd,
633 FaceProfile::Triangle { peak_offset: 1.0 },
634 ),
635 (
636 Vec3::new(sx, 0.0, sz),
637 Vec3::new(sz, shed_h, 0.0),
638 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
639 RoofFaceSelector::GableEnd,
640 FaceProfile::Triangle { peak_offset: 0.0 },
641 ),
642 ]
643 }
644
645 RoofType::Gable | RoofType::OpenGable | RoofType::BoxGable => {
647 let (slope_len, eave_len, ridge_h) = if ridge_x {
648 ((sz / 2.0 + o) / cos_a, sx + 2.0 * o, (sz / 2.0) * tan_a)
649 } else {
650 ((sx / 2.0 + o) / cos_a, sz + 2.0 * o, (sx / 2.0) * tan_a)
651 };
652
653 let mut panels = if ridge_x {
654 vec![
655 (
656 Vec3::new(sx + o, y_anchor, -o),
657 Vec3::new(eave_len, slope_len, 0.0),
658 front_rot,
659 RoofFaceSelector::Slope,
660 FaceProfile::Rectangle,
661 ),
662 (
663 Vec3::new(-o, y_anchor, sz + o),
664 Vec3::new(eave_len, slope_len, 0.0),
665 back_rot,
666 RoofFaceSelector::Slope,
667 FaceProfile::Rectangle,
668 ),
669 ]
670 } else {
671 vec![
672 (
673 Vec3::new(-o, y_anchor, -o),
674 Vec3::new(eave_len, slope_len, 0.0),
675 left_rot,
676 RoofFaceSelector::Slope,
677 FaceProfile::Rectangle,
678 ),
679 (
680 Vec3::new(sx + o, y_anchor, sz + o),
681 Vec3::new(eave_len, slope_len, 0.0),
682 right_rot,
683 RoofFaceSelector::Slope,
684 FaceProfile::Rectangle,
685 ),
686 ]
687 };
688
689 if config.roof_type != RoofType::OpenGable {
690 let profile = if config.roof_type == RoofType::BoxGable {
691 FaceProfile::Rectangle
692 } else {
693 FaceProfile::Triangle { peak_offset: 0.5 }
694 };
695
696 if ridge_x {
697 panels.extend(vec![
698 (
699 Vec3::new(0.0, 0.0, 0.0),
700 Vec3::new(sz, ridge_h, 0.0),
701 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
702 RoofFaceSelector::GableEnd,
703 profile.clone(),
704 ),
705 (
706 Vec3::new(sx, 0.0, sz),
707 Vec3::new(sz, ridge_h, 0.0),
708 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
709 RoofFaceSelector::GableEnd,
710 profile,
711 ),
712 ]);
713 } else {
714 panels.extend(vec![
715 (
716 Vec3::new(sx, 0.0, 0.0),
717 Vec3::new(sx, ridge_h, 0.0),
718 Quat::from_axis_angle(Vec3::Y, PI),
719 RoofFaceSelector::GableEnd,
720 profile.clone(),
721 ),
722 (
723 Vec3::new(0.0, 0.0, sz),
724 Vec3::new(sx, ridge_h, 0.0),
725 Quat::IDENTITY,
726 RoofFaceSelector::GableEnd,
727 profile,
728 ),
729 ]);
730 }
731 }
732 panels
733 }
734
735 RoofType::Pyramid | RoofType::PyramidHip | RoofType::Hip => {
739 let eave_w = sx + 2.0 * o;
740 let eave_d = sz + 2.0 * o;
741 let max_run = sx.min(sz) / 2.0 + o;
742 let slope_len = max_run / cos_a;
743
744 let (fb_profile, lr_profile) = if sx > sz + 1e-5 {
745 let top_w = (sx - sz) / eave_w;
746 let off_x = (sz / 2.0 + o) / eave_w;
747 (
748 FaceProfile::Trapezoid {
749 top_width: top_w,
750 offset_x: off_x,
751 },
752 FaceProfile::Triangle { peak_offset: 0.5 },
753 )
754 } else if sz > sx + 1e-5 {
755 let top_w = (sz - sx) / eave_d;
756 let off_x = (sx / 2.0 + o) / eave_d;
757 (
758 FaceProfile::Triangle { peak_offset: 0.5 },
759 FaceProfile::Trapezoid {
760 top_width: top_w,
761 offset_x: off_x,
762 },
763 )
764 } else {
765 (
766 FaceProfile::Triangle { peak_offset: 0.5 },
767 FaceProfile::Triangle { peak_offset: 0.5 },
768 )
769 };
770
771 vec![
772 (
773 Vec3::new(sx + o, y_anchor, -o),
774 Vec3::new(eave_w, slope_len, 0.0),
775 front_rot,
776 RoofFaceSelector::Slope,
777 fb_profile.clone(),
778 ),
779 (
780 Vec3::new(-o, y_anchor, sz + o),
781 Vec3::new(eave_w, slope_len, 0.0),
782 back_rot,
783 RoofFaceSelector::Slope,
784 fb_profile,
785 ),
786 (
787 Vec3::new(-o, y_anchor, -o),
788 Vec3::new(eave_d, slope_len, 0.0),
789 left_rot,
790 RoofFaceSelector::Slope,
791 lr_profile.clone(),
792 ),
793 (
794 Vec3::new(sx + o, y_anchor, sz + o),
795 Vec3::new(eave_d, slope_len, 0.0),
796 right_rot,
797 RoofFaceSelector::Slope,
798 lr_profile,
799 ),
800 ]
801 }
802
803 RoofType::Butterfly => {
807 let y_valley = y_anchor - (sz / 2.0 + o) * tan_a;
809 if !y_valley.is_finite() {
810 return Err(ShapeError::InvalidNumericValue);
811 }
812 vec![
813 (
815 Vec3::new(-o, y_valley, sz / 2.0),
816 Vec3::new(sx + 2.0 * o, fb_len, 0.0),
817 back_rot,
818 RoofFaceSelector::ValleySlope,
819 FaceProfile::Rectangle,
820 ),
821 (
823 Vec3::new(sx + o, y_valley, sz / 2.0),
824 Vec3::new(sx + 2.0 * o, fb_len, 0.0),
825 front_rot,
826 RoofFaceSelector::ValleySlope,
827 FaceProfile::Rectangle,
828 ),
829 ]
830 }
831
832 RoofType::MShaped => {
836 let quarter = sz / 4.0;
837 let h_m = quarter * tan_a;
838 if !h_m.is_finite() {
839 return Err(ShapeError::InvalidNumericValue);
840 }
841 let slope_m = quarter / cos_a;
842 let y_valley = y_anchor - h_m; vec![
844 (
846 Vec3::new(sx + o, y_anchor, -o),
847 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
848 front_rot,
849 RoofFaceSelector::OuterSlope,
850 FaceProfile::Rectangle,
851 ),
852 (
854 Vec3::new(-o, y_valley, sz / 2.0),
855 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
856 back_rot,
857 RoofFaceSelector::InnerSlope,
858 FaceProfile::Rectangle,
859 ),
860 (
862 Vec3::new(sx + o, y_valley, sz / 2.0),
863 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
864 front_rot,
865 RoofFaceSelector::InnerSlope,
866 FaceProfile::Rectangle,
867 ),
868 (
870 Vec3::new(-o, y_anchor, sz + o),
871 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
872 back_rot,
873 RoofFaceSelector::OuterSlope,
874 FaceProfile::Rectangle,
875 ),
876 ]
877 }
878
879 RoofType::Gambrel => {
882 let alpha2 = config.secondary_pitch_or_default().to_radians();
883 if !alpha2.is_finite() || alpha2 <= 0.0 || alpha2 >= FRAC_PI_2 {
884 return Err(ShapeError::InvalidNumericValue);
885 }
886 let cos_a2 = alpha2.cos();
887 let tan_a2 = alpha2.tan();
888 let tier = config.tier_height_or(0.5).clamp(0.01, 0.99);
889 let (ufr, ubr, ulr, urr) = roof_slope_rotations(alpha2);
890
891 if ridge_x {
892 let run_z = sz / 2.0 + o;
893 let break_run = (tier * run_z).clamp(o, run_z - 1e-3);
894 let h_break = break_run * tan_a;
895 if !h_break.is_finite() {
896 return Err(ShapeError::InvalidNumericValue);
897 }
898 let lower_slope = break_run / cos_a;
899 let upper_run = run_z - break_run;
900 let upper_slope = upper_run / cos_a2;
901 let upper_h = upper_run * tan_a2;
902 let y_break = y_anchor + h_break;
903 let eave_w = sx + 2.0 * o;
904 let wall_y_break = h_break - o * tan_a;
905 let wall_break_run = break_run - o;
906 let mid_w = (sz - 2.0 * wall_break_run).max(0.0);
907
908 let lower_gable_profile = if mid_w > 1e-9 {
909 FaceProfile::Trapezoid {
910 top_width: mid_w / sz,
911 offset_x: wall_break_run / sz,
912 }
913 } else {
914 FaceProfile::Triangle { peak_offset: 0.5 }
915 };
916
917 vec![
918 (
919 Vec3::new(sx + o, y_anchor, -o),
920 Vec3::new(eave_w, lower_slope, 0.0),
921 front_rot,
922 RoofFaceSelector::LowerSlope,
923 FaceProfile::Rectangle,
924 ),
925 (
926 Vec3::new(-o, y_anchor, sz + o),
927 Vec3::new(eave_w, lower_slope, 0.0),
928 back_rot,
929 RoofFaceSelector::LowerSlope,
930 FaceProfile::Rectangle,
931 ),
932 (
933 Vec3::new(sx + o, y_break, -o + break_run),
934 Vec3::new(eave_w, upper_slope, 0.0),
935 ufr,
936 RoofFaceSelector::UpperSlope,
937 FaceProfile::Rectangle,
938 ),
939 (
940 Vec3::new(-o, y_break, sz + o - break_run),
941 Vec3::new(eave_w, upper_slope, 0.0),
942 ubr,
943 RoofFaceSelector::UpperSlope,
944 FaceProfile::Rectangle,
945 ),
946 (
947 Vec3::new(0.0, 0.0, 0.0),
948 Vec3::new(sz, wall_y_break, 0.0),
949 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
950 RoofFaceSelector::GableEnd,
951 lower_gable_profile.clone(),
952 ),
953 (
954 Vec3::new(sx, 0.0, sz),
955 Vec3::new(sz, wall_y_break, 0.0),
956 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
957 RoofFaceSelector::GableEnd,
958 lower_gable_profile,
959 ),
960 (
961 Vec3::new(0.0, wall_y_break, wall_break_run),
962 Vec3::new(mid_w, upper_h, 0.0),
963 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
964 RoofFaceSelector::GableEnd,
965 FaceProfile::Triangle { peak_offset: 0.5 },
966 ),
967 (
968 Vec3::new(sx, wall_y_break, sz - wall_break_run),
969 Vec3::new(mid_w, upper_h, 0.0),
970 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
971 RoofFaceSelector::GableEnd,
972 FaceProfile::Triangle { peak_offset: 0.5 },
973 ),
974 ]
975 } else {
976 let run_x = sx / 2.0 + o;
977 let break_run = (tier * run_x).clamp(o, run_x - 1e-3);
978 let h_break = break_run * tan_a;
979 if !h_break.is_finite() {
980 return Err(ShapeError::InvalidNumericValue);
981 }
982 let lower_slope = break_run / cos_a;
983 let upper_run = run_x - break_run;
984 let upper_slope = upper_run / cos_a2;
985 let upper_h = upper_run * tan_a2;
986 let y_break = y_anchor + h_break;
987 let eave_d = sz + 2.0 * o;
988 let wall_y_break = h_break - o * tan_a;
989 let wall_break_run = break_run - o;
990 let mid_d = (sx - 2.0 * wall_break_run).max(0.0);
991
992 let lower_gable_profile = if mid_d > 1e-9 {
993 FaceProfile::Trapezoid {
994 top_width: mid_d / sx,
995 offset_x: wall_break_run / sx,
996 }
997 } else {
998 FaceProfile::Triangle { peak_offset: 0.5 }
999 };
1000
1001 vec![
1002 (
1003 Vec3::new(-o, y_anchor, -o),
1004 Vec3::new(eave_d, lower_slope, 0.0),
1005 left_rot,
1006 RoofFaceSelector::LowerSlope,
1007 FaceProfile::Rectangle,
1008 ),
1009 (
1010 Vec3::new(sx + o, y_anchor, sz + o),
1011 Vec3::new(eave_d, lower_slope, 0.0),
1012 right_rot,
1013 RoofFaceSelector::LowerSlope,
1014 FaceProfile::Rectangle,
1015 ),
1016 (
1017 Vec3::new(-o + break_run, y_break, -o),
1018 Vec3::new(eave_d, upper_slope, 0.0),
1019 ulr,
1020 RoofFaceSelector::UpperSlope,
1021 FaceProfile::Rectangle,
1022 ),
1023 (
1024 Vec3::new(sx + o - break_run, y_break, sz + o),
1025 Vec3::new(eave_d, upper_slope, 0.0),
1026 urr,
1027 RoofFaceSelector::UpperSlope,
1028 FaceProfile::Rectangle,
1029 ),
1030 (
1031 Vec3::new(sx, 0.0, 0.0),
1032 Vec3::new(sx, wall_y_break, 0.0),
1033 Quat::from_axis_angle(Vec3::Y, PI),
1034 RoofFaceSelector::GableEnd,
1035 lower_gable_profile.clone(),
1036 ),
1037 (
1038 Vec3::new(0.0, 0.0, sz),
1039 Vec3::new(sx, wall_y_break, 0.0),
1040 Quat::IDENTITY,
1041 RoofFaceSelector::GableEnd,
1042 lower_gable_profile,
1043 ),
1044 (
1045 Vec3::new(sx - wall_break_run, wall_y_break, 0.0),
1046 Vec3::new(mid_d, upper_h, 0.0),
1047 Quat::from_axis_angle(Vec3::Y, PI),
1048 RoofFaceSelector::GableEnd,
1049 FaceProfile::Triangle { peak_offset: 0.5 },
1050 ),
1051 (
1052 Vec3::new(wall_break_run, wall_y_break, sz),
1053 Vec3::new(mid_d, upper_h, 0.0),
1054 Quat::IDENTITY,
1055 RoofFaceSelector::GableEnd,
1056 FaceProfile::Triangle { peak_offset: 0.5 },
1057 ),
1058 ]
1059 }
1060 }
1061
1062 RoofType::Mansard => {
1065 let alpha2 = config.secondary_pitch_or_default().to_radians();
1066 if !alpha2.is_finite() || alpha2 <= 0.0 || alpha2 >= FRAC_PI_2 {
1067 return Err(ShapeError::InvalidNumericValue);
1068 }
1069 let cos_a2 = alpha2.cos();
1070 let tier = config.tier_height_or(0.5).clamp(0.01, 0.99);
1071
1072 let max_run = sx.min(sz) / 2.0 + o;
1073 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
1074 let h_break = break_run * tan_a;
1075
1076 if !h_break.is_finite() {
1077 return Err(ShapeError::InvalidNumericValue);
1078 }
1079
1080 let lower_slope = break_run / cos_a;
1081 let y_break = y_anchor + h_break;
1082
1083 let eave_w = sx + 2.0 * o;
1084 let eave_d = sz + 2.0 * o;
1085
1086 let mid_w = (eave_w - 2.0 * break_run).max(0.0);
1087 let mid_d = (eave_d - 2.0 * break_run).max(0.0);
1088
1089 let lower_fb_profile = if mid_w > 1e-9 {
1090 FaceProfile::Trapezoid {
1091 top_width: mid_w / eave_w,
1092 offset_x: break_run / eave_w,
1093 }
1094 } else {
1095 FaceProfile::Triangle { peak_offset: 0.5 }
1096 };
1097
1098 let lower_lr_profile = if mid_d > 1e-9 {
1099 FaceProfile::Trapezoid {
1100 top_width: mid_d / eave_d,
1101 offset_x: break_run / eave_d,
1102 }
1103 } else {
1104 FaceProfile::Triangle { peak_offset: 0.5 }
1105 };
1106
1107 let (ufr, ubr, ulr, urr) = roof_slope_rotations(alpha2);
1108
1109 let upper_run = mid_w.min(mid_d) / 2.0;
1110 let upper_slope = upper_run / cos_a2;
1111
1112 let top_w = (mid_w - 2.0 * upper_run).max(0.0);
1113 let top_d = (mid_d - 2.0 * upper_run).max(0.0);
1114
1115 let upper_fb_profile = if top_w > 1e-9 {
1116 FaceProfile::Trapezoid {
1117 top_width: top_w / mid_w,
1118 offset_x: upper_run / mid_w,
1119 }
1120 } else {
1121 FaceProfile::Triangle { peak_offset: 0.5 }
1122 };
1123
1124 let upper_lr_profile = if top_d > 1e-9 {
1125 FaceProfile::Trapezoid {
1126 top_width: top_d / mid_d,
1127 offset_x: upper_run / mid_d,
1128 }
1129 } else {
1130 FaceProfile::Triangle { peak_offset: 0.5 }
1131 };
1132
1133 vec![
1134 (
1136 Vec3::new(sx + o, y_anchor, -o),
1137 Vec3::new(eave_w, lower_slope, 0.0),
1138 front_rot,
1139 RoofFaceSelector::LowerSlope,
1140 lower_fb_profile.clone(),
1141 ),
1142 (
1143 Vec3::new(-o, y_anchor, sz + o),
1144 Vec3::new(eave_w, lower_slope, 0.0),
1145 back_rot,
1146 RoofFaceSelector::LowerSlope,
1147 lower_fb_profile,
1148 ),
1149 (
1150 Vec3::new(-o, y_anchor, -o),
1151 Vec3::new(eave_d, lower_slope, 0.0),
1152 left_rot,
1153 RoofFaceSelector::LowerSlope,
1154 lower_lr_profile.clone(),
1155 ),
1156 (
1157 Vec3::new(sx + o, y_anchor, sz + o),
1158 Vec3::new(eave_d, lower_slope, 0.0),
1159 right_rot,
1160 RoofFaceSelector::LowerSlope,
1161 lower_lr_profile,
1162 ),
1163 (
1165 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1166 Vec3::new(mid_w, upper_slope, 0.0),
1167 ufr,
1168 RoofFaceSelector::UpperSlope,
1169 upper_fb_profile.clone(),
1170 ),
1171 (
1172 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1173 Vec3::new(mid_w, upper_slope, 0.0),
1174 ubr,
1175 RoofFaceSelector::UpperSlope,
1176 upper_fb_profile,
1177 ),
1178 (
1179 Vec3::new(-o + break_run, y_break, -o + break_run),
1180 Vec3::new(mid_d, upper_slope, 0.0),
1181 ulr,
1182 RoofFaceSelector::UpperSlope,
1183 upper_lr_profile.clone(),
1184 ),
1185 (
1186 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1187 Vec3::new(mid_d, upper_slope, 0.0),
1188 urr,
1189 RoofFaceSelector::UpperSlope,
1190 upper_lr_profile,
1191 ),
1192 ]
1193 }
1194
1195 RoofType::Saltbox => {
1199 let (orient_z, _width, depth) = if ridge_x {
1200 (true, sx, sz)
1201 } else {
1202 (false, sz, sx)
1203 };
1204 let ridge_d = depth * config.ridge_offset;
1205 if !ridge_d.is_finite() || ridge_d <= 0.0 || ridge_d >= depth {
1206 return Err(ShapeError::InvalidNumericValue);
1207 }
1208 let h_s = ridge_d * tan_a;
1209 let back_depth = depth - ridge_d;
1210 let alpha_back = ((h_s) / (back_depth + o)).atan();
1211 let cos_ab = alpha_back.cos();
1212 if !h_s.is_finite() || !alpha_back.is_finite() || cos_ab < 1e-9 {
1213 return Err(ShapeError::InvalidNumericValue);
1214 }
1215 let front_len = (ridge_d + o) / cos_a;
1216 let back_len = (back_depth + o) / cos_ab;
1217 if !front_len.is_finite() || !back_len.is_finite() {
1218 return Err(ShapeError::InvalidNumericValue);
1219 }
1220 let (_, back_rot_s, _, right_rot_s) = roof_slope_rotations(alpha_back);
1221 let peak_fwd = ridge_d / depth;
1222
1223 if orient_z {
1224 vec![
1225 (
1226 Vec3::new(sx + o, y_anchor, -o),
1227 Vec3::new(sx + 2.0 * o, front_len, 0.0),
1228 front_rot,
1229 RoofFaceSelector::Slope,
1230 FaceProfile::Rectangle,
1231 ),
1232 (
1233 Vec3::new(-o, y_anchor, sz + o),
1234 Vec3::new(sx + 2.0 * o, back_len, 0.0),
1235 back_rot_s,
1236 RoofFaceSelector::Slope,
1237 FaceProfile::Rectangle,
1238 ),
1239 (
1240 Vec3::new(0.0, 0.0, 0.0),
1241 Vec3::new(sz, h_s, 0.0),
1242 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1243 RoofFaceSelector::GableEnd,
1244 FaceProfile::Triangle {
1245 peak_offset: peak_fwd,
1246 },
1247 ),
1248 (
1249 Vec3::new(sx, 0.0, sz),
1250 Vec3::new(sz, h_s, 0.0),
1251 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1252 RoofFaceSelector::GableEnd,
1253 FaceProfile::Triangle {
1254 peak_offset: 1.0 - peak_fwd,
1255 },
1256 ),
1257 ]
1258 } else {
1259 vec![
1260 (
1261 Vec3::new(-o, y_anchor, -o),
1262 Vec3::new(sz + 2.0 * o, front_len, 0.0),
1263 left_rot,
1264 RoofFaceSelector::Slope,
1265 FaceProfile::Rectangle,
1266 ),
1267 (
1268 Vec3::new(sx + o, y_anchor, sz + o),
1269 Vec3::new(sz + 2.0 * o, back_len, 0.0),
1270 right_rot_s,
1271 RoofFaceSelector::Slope,
1272 FaceProfile::Rectangle,
1273 ),
1274 (
1275 Vec3::new(sx, 0.0, 0.0),
1276 Vec3::new(sx, h_s, 0.0),
1277 Quat::from_axis_angle(Vec3::Y, PI),
1278 RoofFaceSelector::GableEnd,
1279 FaceProfile::Triangle {
1280 peak_offset: 1.0 - peak_fwd,
1281 },
1282 ),
1283 (
1284 Vec3::new(0.0, 0.0, sz),
1285 Vec3::new(sx, h_s, 0.0),
1286 Quat::IDENTITY,
1287 RoofFaceSelector::GableEnd,
1288 FaceProfile::Triangle {
1289 peak_offset: peak_fwd,
1290 },
1291 ),
1292 ]
1293 }
1294 }
1295
1296 RoofType::Jerkinhead => {
1300 let tier = config.tier_height_or(0.25).clamp(0.01, 0.99);
1301 let orient_z = ridge_x;
1302 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1303
1304 let max_clip = (width / 2.0 + o).min(depth / 2.0);
1305 let clip_run = (tier * depth / 2.0).clamp(0.0, max_clip - 1e-3);
1306
1307 let eave_w = width + 2.0 * o;
1308
1309 let slope_len = (depth / 2.0 + o) / cos_a;
1310
1311 let ridge_span = width - 2.0 * clip_run;
1324 let slope_profile = if ridge_span > 1e-9 {
1325 let verge_y = (depth / 2.0 - clip_run) / (depth / 2.0 + o);
1326 let top_inset = (clip_run + o) / eave_w;
1327 FaceProfile::Polygon(vec![
1328 glam::DVec2::new(0.0, 0.0),
1329 glam::DVec2::new(1.0, 0.0),
1330 glam::DVec2::new(1.0, verge_y),
1331 glam::DVec2::new(1.0 - top_inset, 1.0),
1332 glam::DVec2::new(top_inset, 1.0),
1333 glam::DVec2::new(0.0, verge_y),
1334 ])
1335 } else {
1336 FaceProfile::Triangle { peak_offset: 0.5 }
1337 };
1338
1339 let true_h = (depth / 2.0) * tan_a;
1340 let wall_h = (true_h - clip_run * tan_a).max(0.0);
1341 let wall_profile = if clip_run > 1e-9 {
1342 FaceProfile::Trapezoid {
1343 top_width: (2.0 * clip_run) / depth,
1344 offset_x: (depth / 2.0 - clip_run) / depth,
1345 }
1346 } else {
1347 FaceProfile::Triangle { peak_offset: 0.5 }
1348 };
1349
1350 let hip_base_w = 2.0 * clip_run + 2.0 * o;
1351 let hip_slope_len = (clip_run + o) / cos_a;
1352 let hip_profile = FaceProfile::Triangle { peak_offset: 0.5 };
1353
1354 if orient_z {
1355 let left_hip_origin = Vec3::new(-o, wall_h - o * tan_a, sz / 2.0 - clip_run - o);
1356 let right_hip_origin =
1357 Vec3::new(sx + o, wall_h - o * tan_a, sz / 2.0 + clip_run + o);
1358 vec![
1359 (
1360 Vec3::new(sx + o, y_anchor, -o),
1361 Vec3::new(eave_w, slope_len, 0.0),
1362 front_rot,
1363 RoofFaceSelector::Slope,
1364 slope_profile.clone(),
1365 ),
1366 (
1367 Vec3::new(-o, y_anchor, sz + o),
1368 Vec3::new(eave_w, slope_len, 0.0),
1369 back_rot,
1370 RoofFaceSelector::Slope,
1371 slope_profile,
1372 ),
1373 (
1374 Vec3::new(0.0, 0.0, 0.0),
1375 Vec3::new(sz, wall_h, 0.0),
1376 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1377 RoofFaceSelector::GableEnd,
1378 wall_profile.clone(),
1379 ),
1380 (
1381 Vec3::new(sx, 0.0, sz),
1382 Vec3::new(sz, wall_h, 0.0),
1383 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1384 RoofFaceSelector::GableEnd,
1385 wall_profile,
1386 ),
1387 (
1388 left_hip_origin,
1389 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1390 left_rot,
1391 RoofFaceSelector::HipEnd,
1392 hip_profile.clone(),
1393 ),
1394 (
1395 right_hip_origin,
1396 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1397 right_rot,
1398 RoofFaceSelector::HipEnd,
1399 hip_profile,
1400 ),
1401 ]
1402 } else {
1403 let front_hip_origin = Vec3::new(sx / 2.0 + clip_run + o, wall_h - o * tan_a, -o);
1404 let back_hip_origin =
1405 Vec3::new(sx / 2.0 - clip_run - o, wall_h - o * tan_a, sz + o);
1406 vec![
1407 (
1408 Vec3::new(-o, y_anchor, -o),
1409 Vec3::new(eave_w, slope_len, 0.0),
1410 left_rot,
1411 RoofFaceSelector::Slope,
1412 slope_profile.clone(),
1413 ),
1414 (
1415 Vec3::new(sx + o, y_anchor, sz + o),
1416 Vec3::new(eave_w, slope_len, 0.0),
1417 right_rot,
1418 RoofFaceSelector::Slope,
1419 slope_profile,
1420 ),
1421 (
1422 Vec3::new(sx, 0.0, 0.0),
1423 Vec3::new(sx, wall_h, 0.0),
1424 Quat::from_axis_angle(Vec3::Y, PI),
1425 RoofFaceSelector::GableEnd,
1426 wall_profile.clone(),
1427 ),
1428 (
1429 Vec3::new(0.0, 0.0, sz),
1430 Vec3::new(sx, wall_h, 0.0),
1431 Quat::IDENTITY,
1432 RoofFaceSelector::GableEnd,
1433 wall_profile,
1434 ),
1435 (
1436 front_hip_origin,
1437 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1438 front_rot,
1439 RoofFaceSelector::HipEnd,
1440 hip_profile.clone(),
1441 ),
1442 (
1443 back_hip_origin,
1444 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1445 back_rot,
1446 RoofFaceSelector::HipEnd,
1447 hip_profile,
1448 ),
1449 ]
1450 }
1451 }
1452
1453 RoofType::DutchGable => {
1457 let tier = config.tier_height_or(0.7).clamp(0.01, 0.99);
1458 let orient_z = ridge_x;
1459 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1460
1461 let max_run = width.min(depth) / 2.0 + o;
1462 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
1463
1464 if !break_run.is_finite() {
1465 return Err(ShapeError::InvalidNumericValue);
1466 }
1467
1468 let y_break = y_anchor + break_run * tan_a;
1469 let eave_w = width + 2.0 * o;
1470 let eave_d = depth + 2.0 * o;
1471
1472 let top_w = (eave_w - 2.0 * break_run).max(0.0);
1473 let top_d = (eave_d - 2.0 * break_run).max(0.0);
1474
1475 let lower_slope_len = break_run / cos_a;
1476
1477 let fb_profile = if top_w > 1e-9 {
1478 FaceProfile::Trapezoid {
1479 top_width: top_w / eave_w,
1480 offset_x: break_run / eave_w,
1481 }
1482 } else {
1483 FaceProfile::Triangle { peak_offset: 0.5 }
1484 };
1485
1486 let lr_profile = if top_d > 1e-9 {
1487 FaceProfile::Trapezoid {
1488 top_width: top_d / eave_d,
1489 offset_x: break_run / eave_d,
1490 }
1491 } else {
1492 FaceProfile::Triangle { peak_offset: 0.5 }
1493 };
1494
1495 let upper_run = (depth / 2.0 + o) - break_run;
1496 let upper_slope_len = upper_run / cos_a;
1497 let upper_h = upper_run * tan_a;
1498
1499 if orient_z {
1500 vec![
1501 (
1503 Vec3::new(sx + o, y_anchor, -o),
1504 Vec3::new(eave_w, lower_slope_len, 0.0),
1505 front_rot,
1506 RoofFaceSelector::Slope,
1507 fb_profile.clone(),
1508 ),
1509 (
1510 Vec3::new(-o, y_anchor, sz + o),
1511 Vec3::new(eave_w, lower_slope_len, 0.0),
1512 back_rot,
1513 RoofFaceSelector::Slope,
1514 fb_profile,
1515 ),
1516 (
1518 Vec3::new(-o, y_anchor, -o),
1519 Vec3::new(eave_d, lower_slope_len, 0.0),
1520 left_rot,
1521 RoofFaceSelector::Slope,
1522 lr_profile.clone(),
1523 ),
1524 (
1525 Vec3::new(sx + o, y_anchor, sz + o),
1526 Vec3::new(eave_d, lower_slope_len, 0.0),
1527 right_rot,
1528 RoofFaceSelector::Slope,
1529 lr_profile,
1530 ),
1531 (
1533 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1534 Vec3::new(top_w, upper_slope_len, 0.0),
1535 front_rot,
1536 RoofFaceSelector::Slope,
1537 FaceProfile::Rectangle,
1538 ),
1539 (
1540 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1541 Vec3::new(top_w, upper_slope_len, 0.0),
1542 back_rot,
1543 RoofFaceSelector::Slope,
1544 FaceProfile::Rectangle,
1545 ),
1546 (
1548 Vec3::new(-o + break_run, y_break, -o + break_run),
1549 Vec3::new(top_d, upper_h, 0.0),
1550 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1551 RoofFaceSelector::GableEnd,
1552 FaceProfile::Triangle { peak_offset: 0.5 },
1553 ),
1554 (
1555 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1556 Vec3::new(top_d, upper_h, 0.0),
1557 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1558 RoofFaceSelector::GableEnd,
1559 FaceProfile::Triangle { peak_offset: 0.5 },
1560 ),
1561 ]
1562 } else {
1563 vec![
1564 (
1566 Vec3::new(-o, y_anchor, -o),
1567 Vec3::new(eave_w, lower_slope_len, 0.0),
1568 left_rot,
1569 RoofFaceSelector::Slope,
1570 fb_profile.clone(),
1571 ),
1572 (
1573 Vec3::new(sx + o, y_anchor, sz + o),
1574 Vec3::new(eave_w, lower_slope_len, 0.0),
1575 right_rot,
1576 RoofFaceSelector::Slope,
1577 fb_profile,
1578 ),
1579 (
1581 Vec3::new(sx + o, y_anchor, -o),
1582 Vec3::new(eave_d, lower_slope_len, 0.0),
1583 front_rot,
1584 RoofFaceSelector::Slope,
1585 lr_profile.clone(),
1586 ),
1587 (
1588 Vec3::new(-o, y_anchor, sz + o),
1589 Vec3::new(eave_d, lower_slope_len, 0.0),
1590 back_rot,
1591 RoofFaceSelector::Slope,
1592 lr_profile,
1593 ),
1594 (
1596 Vec3::new(-o + break_run, y_break, -o + break_run),
1597 Vec3::new(top_w, upper_slope_len, 0.0),
1598 left_rot,
1599 RoofFaceSelector::Slope,
1600 FaceProfile::Rectangle,
1601 ),
1602 (
1603 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1604 Vec3::new(top_w, upper_slope_len, 0.0),
1605 right_rot,
1606 RoofFaceSelector::Slope,
1607 FaceProfile::Rectangle,
1608 ),
1609 (
1611 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1612 Vec3::new(top_d, upper_h, 0.0),
1613 Quat::from_axis_angle(Vec3::Y, PI),
1614 RoofFaceSelector::GableEnd,
1615 FaceProfile::Triangle { peak_offset: 0.5 },
1616 ),
1617 (
1618 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1619 Vec3::new(top_d, upper_h, 0.0),
1620 Quat::IDENTITY,
1621 RoofFaceSelector::GableEnd,
1622 FaceProfile::Triangle { peak_offset: 0.5 },
1623 ),
1624 ]
1625 }
1626 }
1627 };
1628
1629 if config.fascia_depth.is_finite() && config.fascia_depth > 0.0 {
1633 let fascia_depth = config.fascia_depth;
1634 let mut fascia_panels: Vec<Panel> = Vec::new();
1635 for (local_off, face_size, rot_delta, selector, _profile) in &panels {
1636 let eave_bearing = matches!(
1637 selector,
1638 RoofFaceSelector::Slope
1639 | RoofFaceSelector::LowerSlope
1640 | RoofFaceSelector::OuterSlope
1641 );
1642 if !eave_bearing {
1643 continue;
1644 }
1645 if (local_off.y - y_anchor).abs() > 1e-6 {
1646 continue;
1647 }
1648 let Some(wall_rot) = slope_to_wall_rot(*rot_delta) else {
1649 continue;
1650 };
1651 fascia_panels.push((
1652 Vec3::new(local_off.x, local_off.y - fascia_depth, local_off.z),
1653 Vec3::new(face_size.x, fascia_depth, 0.0),
1654 wall_rot,
1655 RoofFaceSelector::Fascia,
1656 FaceProfile::Rectangle,
1657 ));
1658 }
1659 panels.extend(fascia_panels);
1660 }
1661
1662 if queue.len() + panels.len() > MAX_QUEUE {
1663 return Err(ShapeError::CapacityOverflow);
1664 }
1665 let mut child_ordinal: u64 = 0;
1666 for (local_off, face_size, rot_delta, selector, profile) in panels {
1667 let Some(rule) = find_roof_rule(selector, cases) else {
1668 continue;
1669 };
1670 if face_size.x < 1e-9 || face_size.y < 1e-9 {
1672 continue;
1673 }
1674 let face_pos = scope.position + scope.rotation * local_off;
1675 let face_rot = (scope.rotation * rot_delta).normalize();
1676 let face_scope = Scope::new(face_pos, face_rot, face_size);
1677 face_scope.validate()?;
1678 if model.len() + queue.len() >= max_terminals {
1679 return Err(ShapeError::CapacityOverflow);
1680 }
1681 queue.push_back(WorkItem {
1682 scope: face_scope,
1683 rule: rule.name.clone(),
1684 args: rule.args.clone(),
1685 depth: depth + 1,
1686 taper: 0.0,
1687 face_profile_override: Some(profile),
1688 material: material.clone(),
1689 split_i,
1690 split_n,
1691 rng_state: fork_state(rng_state, child_ordinal),
1692 label: label.clone(),
1693 });
1694 child_ordinal += 1;
1695 }
1696
1697 Ok(())
1698}
1699
1700#[allow(clippy::too_many_arguments)]
1708fn select_variant<'a>(
1709 variants: &'a [RuleVariant],
1710 scope: &Scope,
1711 split_i: usize,
1712 split_n: usize,
1713 depth: usize,
1714 params: &[(String, f64)],
1715 globals: &HashMap<String, f64>,
1716 rng: &mut Pcg64,
1717) -> Result<&'a [ShapeOp], ShapeError> {
1718 if variants.is_empty() {
1719 return Ok(&[]);
1720 }
1721 let guarded = variants
1722 .iter()
1723 .any(|v| matches!(v.selector, VariantSelector::When(_) | VariantSelector::Else));
1724 if guarded {
1725 for v in variants {
1726 match &v.selector {
1727 VariantSelector::When(cond) => {
1728 let hit =
1729 eval_expr(cond, scope, split_i, split_n, depth, params, globals, rng)?;
1730 if hit != 0.0 {
1731 return Ok(&v.ops);
1732 }
1733 }
1734 VariantSelector::Else => return Ok(&v.ops),
1735 VariantSelector::Weight(_) => {
1736 return Err(ShapeError::ParseError(
1737 "rule mixes weighted and guarded variants".to_string(),
1738 ));
1739 }
1740 }
1741 }
1742 return Ok(&[]);
1743 }
1744 if variants.len() == 1 {
1745 return Ok(&variants[0].ops);
1746 }
1747 let total: f64 = variants
1748 .iter()
1749 .map(|v| match v.selector {
1750 VariantSelector::Weight(w) => w,
1751 _ => 0.0,
1752 })
1753 .sum();
1754 use rand::Rng;
1755 let r: f64 = rng.random::<f64>() * total;
1756 let mut acc = 0.0;
1757 for v in variants {
1758 if let VariantSelector::Weight(w) = v.selector {
1759 acc += w;
1760 if r < acc {
1761 return Ok(&v.ops);
1762 }
1763 }
1764 }
1765 Ok(&variants.last().unwrap().ops)
1766}
1767
1768#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1780pub struct RuleDef {
1781 pub params: Vec<String>,
1784 pub variants: Vec<RuleVariant>,
1785}
1786
1787pub struct Interpreter {
1788 rules: HashMap<String, RuleDef>,
1789 attrs: HashMap<String, f64>,
1792 consts: HashMap<String, f64>,
1794 attr_defaults: HashMap<String, f64>,
1797 styles: HashMap<String, Vec<(String, f64)>>,
1800 active_style: Option<String>,
1802 pub max_depth: usize,
1805 pub max_terminals: usize,
1809 pub seed: u64,
1813}
1814
1815impl Default for Interpreter {
1816 fn default() -> Self {
1817 Self::new()
1818 }
1819}
1820
1821impl Interpreter {
1822 pub fn new() -> Self {
1823 Self {
1824 rules: HashMap::new(),
1825 attrs: HashMap::new(),
1826 consts: HashMap::new(),
1827 attr_defaults: HashMap::new(),
1828 styles: HashMap::new(),
1829 active_style: None,
1830 max_depth: MAX_DEPTH,
1831 max_terminals: MAX_TERMINALS,
1832 seed: 0,
1833 }
1834 }
1835
1836 pub fn add_statement(&mut self, stmt: crate::grammar::Statement) -> Result<(), ShapeError> {
1848 use crate::grammar::Statement;
1849 match stmt {
1850 Statement::Rule(rule) => self.add_grammar_rule(rule),
1851 Statement::Attr { name, value } => {
1852 self.attr_defaults.insert(name, value);
1853 Ok(())
1854 }
1855 Statement::Const { name, value } => {
1856 self.consts.insert(name, value);
1857 Ok(())
1858 }
1859 Statement::Style {
1860 name,
1861 extends,
1862 overrides,
1863 } => {
1864 let mut flat = match extends {
1865 Some(base) => self.styles.get(&base).cloned().ok_or_else(|| {
1866 ShapeError::ParseError(format!(
1867 "style `{name}` extends unknown style `{base}`"
1868 ))
1869 })?,
1870 None => Vec::new(),
1871 };
1872 flat.extend(overrides);
1873 self.styles.insert(name, flat);
1874 Ok(())
1875 }
1876 }
1877 }
1878
1879 pub fn set_style(&mut self, name: impl Into<String>) -> Result<(), ShapeError> {
1882 let name = name.into();
1883 if !self.styles.contains_key(&name) {
1884 return Err(ShapeError::ParseError(format!("unknown style `{name}`")));
1885 }
1886 self.active_style = Some(name);
1887 Ok(())
1888 }
1889
1890 pub fn clear_style(&mut self) {
1892 self.active_style = None;
1893 }
1894
1895 fn effective_globals(&self) -> HashMap<String, f64> {
1899 let mut g = self.consts.clone();
1900 for (k, v) in &self.attr_defaults {
1901 g.insert(k.clone(), *v);
1902 }
1903 if let Some(style) = &self.active_style
1904 && let Some(overrides) = self.styles.get(style)
1905 {
1906 for (k, v) in overrides {
1907 g.insert(k.clone(), *v);
1908 }
1909 }
1910 for (k, v) in &self.attrs {
1911 g.insert(k.clone(), *v);
1912 }
1913 g
1914 }
1915
1916 pub fn rules(&self) -> &HashMap<String, RuleDef> {
1918 &self.rules
1919 }
1920
1921 pub fn set_attr(&mut self, name: impl Into<String>, value: f64) {
1925 self.attrs.insert(name.into(), value);
1926 }
1927
1928 pub fn attrs(&self) -> &HashMap<String, f64> {
1930 &self.attrs
1931 }
1932
1933 pub fn set_variants(&mut self, name: impl Into<String>, variants: Vec<RuleVariant>) {
1939 let name = name.into();
1940 let params = self
1941 .rules
1942 .get(&name)
1943 .map(|def| def.params.clone())
1944 .unwrap_or_default();
1945 self.rules.insert(name, RuleDef { params, variants });
1946 }
1947
1948 pub fn add_rule(&mut self, name: impl Into<String>, ops: Vec<ShapeOp>) {
1950 self.rules.insert(
1951 name.into(),
1952 RuleDef {
1953 params: Vec::new(),
1954 variants: vec![RuleVariant::weighted(1.0, ops)],
1955 },
1956 );
1957 }
1958
1959 pub fn add_grammar_rule(
1963 &mut self,
1964 rule: crate::grammar::GrammarRule,
1965 ) -> Result<(), ShapeError> {
1966 if rule.name == "NIL" {
1967 return Err(ShapeError::ParseError(
1968 "`NIL` is reserved and cannot be defined as a rule".to_string(),
1969 ));
1970 }
1971 self.add_rule_variants(rule.name, rule.params, rule.variants)
1972 }
1973
1974 pub fn add_rule_variants(
1977 &mut self,
1978 name: impl Into<String>,
1979 params: Vec<String>,
1980 variants: Vec<RuleVariant>,
1981 ) -> Result<(), ShapeError> {
1982 if params.len() > crate::grammar::MAX_RULE_ARGS {
1983 return Err(ShapeError::ParseError(format!(
1984 "rule declares {} parameters (max {})",
1985 params.len(),
1986 crate::grammar::MAX_RULE_ARGS
1987 )));
1988 }
1989 for (i, p) in params.iter().enumerate() {
1990 if params[..i].contains(p) {
1991 return Err(ShapeError::ParseError(format!(
1992 "duplicate rule parameter name: {p}"
1993 )));
1994 }
1995 }
1996 let n_weight = variants
1997 .iter()
1998 .filter(|v| matches!(v.selector, VariantSelector::Weight(_)))
1999 .count();
2000 let n_guard = variants.len() - n_weight;
2001 if n_weight > 0 && n_guard > 0 {
2002 return Err(ShapeError::ParseError(
2003 "rule mixes weighted and guarded variants".to_string(),
2004 ));
2005 }
2006 for (i, v) in variants.iter().enumerate() {
2007 match &v.selector {
2008 VariantSelector::Weight(w) => {
2009 if !w.is_finite() || *w < 0.0 {
2010 return Err(ShapeError::InvalidNumericValue);
2011 }
2012 }
2013 VariantSelector::Else => {
2014 if i + 1 != variants.len() {
2015 return Err(ShapeError::ParseError(
2016 "`else:` must be the last variant".to_string(),
2017 ));
2018 }
2019 }
2020 VariantSelector::When(_) => {}
2021 }
2022 }
2023 self.rules.insert(name.into(), RuleDef { params, variants });
2024 Ok(())
2025 }
2026
2027 pub fn add_rule_def(
2033 &mut self,
2034 name: impl Into<String>,
2035 params: Vec<String>,
2036 variants: Vec<(f64, Vec<ShapeOp>)>,
2037 ) -> Result<(), ShapeError> {
2038 if params.len() > crate::grammar::MAX_RULE_ARGS {
2039 return Err(ShapeError::ParseError(format!(
2040 "rule declares {} parameters (max {})",
2041 params.len(),
2042 crate::grammar::MAX_RULE_ARGS
2043 )));
2044 }
2045 for (i, p) in params.iter().enumerate() {
2046 if params[..i].contains(p) {
2047 return Err(ShapeError::ParseError(format!(
2048 "duplicate rule parameter name: {p}"
2049 )));
2050 }
2051 }
2052 for (weight, _) in &variants {
2053 if !weight.is_finite() || *weight < 0.0 {
2054 return Err(ShapeError::InvalidNumericValue);
2055 }
2056 }
2057 let variants = variants
2058 .into_iter()
2059 .map(|(weight, ops)| RuleVariant::weighted(weight, ops))
2060 .collect();
2061 self.rules.insert(name.into(), RuleDef { params, variants });
2062 Ok(())
2063 }
2064
2065 pub fn add_weighted_rules(
2072 &mut self,
2073 name: impl Into<String>,
2074 variants: Vec<(f64, Vec<ShapeOp>)>,
2075 ) -> Result<(), ShapeError> {
2076 for (weight, _) in &variants {
2077 if !weight.is_finite() || *weight < 0.0 {
2078 return Err(ShapeError::InvalidNumericValue);
2079 }
2080 }
2081 let wvs = variants
2082 .into_iter()
2083 .map(|(weight, ops)| RuleVariant::weighted(weight, ops))
2084 .collect();
2085 self.rules.insert(
2086 name.into(),
2087 RuleDef {
2088 params: Vec::new(),
2089 variants: wvs,
2090 },
2091 );
2092 Ok(())
2093 }
2094
2095 pub fn has_rule(&self, name: &str) -> bool {
2097 self.rules.contains_key(name)
2098 }
2099
2100 pub fn derive(
2107 &self,
2108 root_scope: Scope,
2109 root_rule: impl Into<String>,
2110 ) -> Result<ShapeModel, ShapeError> {
2111 root_scope.validate()?;
2112
2113 let mut model = ShapeModel::new();
2114 let mut queue: VecDeque<WorkItem> = VecDeque::new();
2115 let globals = self.effective_globals();
2116
2117 queue.push_back(WorkItem {
2118 scope: root_scope,
2119 rule: root_rule.into(),
2120 args: Vec::new(),
2121 depth: 0,
2122 taper: 0.0,
2123 face_profile_override: None,
2124 material: None,
2125 split_i: 0,
2126 split_n: 1,
2127 rng_state: splitmix64(self.seed),
2128 label: None,
2129 });
2130
2131 while let Some(item) = queue.pop_front() {
2132 if queue.len() > MAX_QUEUE {
2133 return Err(ShapeError::CapacityOverflow);
2134 }
2135 if item.depth > self.max_depth {
2136 return Err(ShapeError::DepthLimitExceeded(self.max_depth));
2137 }
2138
2139 if item.rule == "NIL" {
2143 continue;
2144 }
2145
2146 let mut item_rng = Pcg64::seed_from_u64(item.rng_state);
2149 let def = match self.rules.get(&item.rule) {
2150 Some(def) => def,
2151 None => {
2152 if model.len() >= self.max_terminals {
2154 return Err(ShapeError::CapacityOverflow);
2155 }
2156 let profile = item
2157 .face_profile_override
2158 .unwrap_or_else(|| taper_to_profile(item.taper));
2159 let mut terminal =
2160 Terminal::new_profiled(item.scope, &item.rule, profile, item.material);
2161 terminal.label = item.label;
2162 model.push(terminal);
2163 continue;
2164 }
2165 };
2166
2167 if def.params.len() != item.args.len() {
2169 return Err(ShapeError::ArityMismatch(format!(
2170 "rule `{}` expects {} argument(s), got {}",
2171 item.rule,
2172 def.params.len(),
2173 item.args.len()
2174 )));
2175 }
2176 let params: Vec<(String, f64)> = def
2177 .params
2178 .iter()
2179 .cloned()
2180 .zip(item.args.iter().copied())
2181 .collect();
2182
2183 let ops = select_variant(
2184 &def.variants,
2185 &item.scope,
2186 item.split_i,
2187 item.split_n,
2188 item.depth,
2189 ¶ms,
2190 &globals,
2191 &mut item_rng,
2192 )?;
2193
2194 self.apply_ops(
2195 item.scope,
2196 item.taper,
2197 item.face_profile_override,
2198 item.material,
2199 item.label.clone(),
2200 ops,
2201 item.depth,
2202 ¶ms,
2203 item.split_i,
2204 item.split_n,
2205 item.rng_state,
2206 &globals,
2207 &mut item_rng,
2208 &mut queue,
2209 &mut model,
2210 )?;
2211 }
2212
2213 Ok(model)
2214 }
2215
2216 #[allow(clippy::too_many_arguments)]
2222 #[allow(unused_assignments)]
2225 fn apply_ops(
2226 &self,
2227 initial_scope: Scope,
2228 initial_taper: f64,
2229 initial_face_profile: Option<FaceProfile>,
2230 initial_material: Option<Material>,
2231 initial_label: Option<String>,
2232 ops: &[ShapeOp],
2233 depth: usize,
2234 params: &[(String, f64)],
2235 split_i: usize,
2236 split_n: usize,
2237 rng_state: u64,
2238 globals: &HashMap<String, f64>,
2239 rng: &mut Pcg64,
2240 queue: &mut VecDeque<WorkItem>,
2241 model: &mut ShapeModel,
2242 ) -> Result<(), ShapeError> {
2243 let mut scope = initial_scope;
2244 let entry_scope = initial_scope;
2246 let mut taper = initial_taper;
2247 let mut face_profile = initial_face_profile;
2248 let mut material = initial_material;
2249 let mut label = initial_label;
2250
2251 macro_rules! ev {
2255 ($e:expr) => {
2256 eval_expr($e, &scope, split_i, split_n, depth, params, globals, rng)?
2257 };
2258 }
2259 let mut child_ordinal: u64 = 0;
2262 macro_rules! fork {
2263 () => {{
2264 let s = fork_state(rng_state, child_ordinal);
2265 child_ordinal += 1;
2266 s
2267 }};
2268 }
2269 macro_rules! ev_args {
2271 ($call:expr) => {{
2272 let mut argv = Vec::with_capacity($call.args.len());
2273 for a in &$call.args {
2274 argv.push(ev!(a));
2275 }
2276 argv
2277 }};
2278 }
2279
2280 for op in ops {
2281 match op {
2282 ShapeOp::Extrude(h) => {
2284 let h = ev!(h);
2285 if h <= 0.0 {
2286 return Err(ShapeError::InvalidNumericValue);
2287 }
2288 if scope.size.z.abs() < 1e-9 && scope.size.y.abs() > 1e-9 {
2294 scope.size.z = h;
2295 } else {
2296 scope.size.y = h;
2297 }
2298 }
2299
2300 ShapeOp::Taper(amount) => {
2301 taper = ev!(amount).clamp(0.0, 1.0);
2302 }
2303
2304 ShapeOp::Rotate([w, x, y, z]) => {
2305 let (w, x, y, z) = (ev!(w), ev!(x), ev!(y), ev!(z));
2306 let len_sq = w * w + x * x + y * y + z * z;
2310 if !len_sq.is_finite() || len_sq < 1e-12 {
2311 return Err(ShapeError::InvalidNumericValue);
2312 }
2313 let q = Quat::from_xyzw(x, y, z, w);
2314 scope.rotation = (scope.rotation * q.normalize()).normalize();
2315 }
2316
2317 ShapeOp::Translate([x, y, z]) => {
2318 let v = Vec3::new(ev!(x), ev!(y), ev!(z));
2319 scope.position += scope.rotation * v;
2320 if !scope.position.is_finite() {
2323 return Err(ShapeError::InvalidNumericValue);
2324 }
2325 }
2326
2327 ShapeOp::Scale([x, y, z]) => {
2328 let v = Vec3::new(ev!(x), ev!(y), ev!(z));
2329 if v.x <= 0.0 || v.y <= 0.0 || v.z <= 0.0 {
2330 return Err(ShapeError::InvalidNumericValue);
2331 }
2332 scope.size *= v;
2333 if !scope.size.is_finite() {
2337 return Err(ShapeError::InvalidNumericValue);
2338 }
2339 }
2340
2341 ShapeOp::Mat(mat) => {
2342 material = Some(mat.clone());
2343 }
2344
2345 ShapeOp::Label(name) => {
2346 label = Some(name.clone());
2347 }
2348
2349 ShapeOp::Size([x, y, z]) => {
2350 let v = Vec3::new(ev!(x), ev!(y), ev!(z));
2351 if v.x < 0.0 || v.y < 0.0 || v.z < 0.0 {
2352 return Err(ShapeError::InvalidNumericValue);
2353 }
2354 scope.size = v;
2355 }
2356
2357 ShapeOp::Center { x, y, z } => {
2358 let mut local = entry_scope
2360 .rotation
2361 .inverse()
2362 .mul_vec3(scope.position - entry_scope.position);
2363 if *x {
2364 local.x = (entry_scope.size.x - scope.size.x) / 2.0;
2365 }
2366 if *y {
2367 local.y = (entry_scope.size.y - scope.size.y) / 2.0;
2368 }
2369 if *z {
2370 local.z = (entry_scope.size.z - scope.size.z) / 2.0;
2371 }
2372 scope.position = entry_scope.position + entry_scope.rotation * local;
2373 if !scope.position.is_finite() {
2374 return Err(ShapeError::InvalidNumericValue);
2375 }
2376 }
2377
2378 ShapeOp::Mirror => {
2379 if let Some(profile) = &mut face_profile {
2380 *profile = mirror_profile(profile);
2381 }
2382 }
2383
2384 ShapeOp::ShapeL { front, side, cases } => {
2386 let d = ev!(front);
2387 let w = ev!(side);
2388 let (sx, sz) = (scope.size.x, scope.size.z);
2389 if d <= 0.0 || w <= 0.0 || d >= sz - 1e-9 || w >= sx - 1e-9 {
2390 return Err(ShapeError::InvalidNumericValue);
2391 }
2392 let parts: [(CarveSelector, Vec3, Vec3); 3] = [
2395 (
2396 CarveSelector::Shape,
2397 Vec3::ZERO,
2398 Vec3::new(sx, scope.size.y, d),
2399 ),
2400 (
2401 CarveSelector::Shape,
2402 Vec3::new(0.0, 0.0, d),
2403 Vec3::new(w, scope.size.y, sz - d),
2404 ),
2405 (
2406 CarveSelector::Remainder,
2407 Vec3::new(w, 0.0, d),
2408 Vec3::new(sx - w, scope.size.y, sz - d),
2409 ),
2410 ];
2411 if queue.len() + parts.len() > MAX_QUEUE {
2412 return Err(ShapeError::CapacityOverflow);
2413 }
2414 for (selector, local_off, size) in parts {
2415 let Some(case) = find_carve_rule(selector, cases) else {
2416 continue;
2417 };
2418 let pos = scope.position + scope.rotation * local_off;
2419 let child = Scope::new(pos, scope.rotation, size);
2420 child.validate()?;
2421 let args = ev_args!(&case.rule);
2422 queue.push_back(WorkItem {
2423 scope: child,
2424 rule: case.rule.name.clone(),
2425 args,
2426 depth: depth + 1,
2427 taper: 0.0,
2428 face_profile_override: None,
2429 material: material.clone(),
2430 split_i,
2431 split_n,
2432 rng_state: fork!(),
2433 label: label.clone(),
2434 });
2435 }
2436 return Ok(());
2437 }
2438
2439 ShapeOp::ShapeU {
2440 front,
2441 left,
2442 right,
2443 cases,
2444 } => {
2445 let d = ev!(front);
2446 let wl = ev!(left);
2447 let wr = ev!(right);
2448 let (sx, sz) = (scope.size.x, scope.size.z);
2449 if d <= 0.0 || wl <= 0.0 || wr <= 0.0 || d >= sz - 1e-9 || wl + wr >= sx - 1e-9
2450 {
2451 return Err(ShapeError::InvalidNumericValue);
2452 }
2453 let parts: [(CarveSelector, Vec3, Vec3); 4] = [
2454 (
2455 CarveSelector::Shape,
2456 Vec3::ZERO,
2457 Vec3::new(sx, scope.size.y, d),
2458 ),
2459 (
2460 CarveSelector::Shape,
2461 Vec3::new(0.0, 0.0, d),
2462 Vec3::new(wl, scope.size.y, sz - d),
2463 ),
2464 (
2465 CarveSelector::Shape,
2466 Vec3::new(sx - wr, 0.0, d),
2467 Vec3::new(wr, scope.size.y, sz - d),
2468 ),
2469 (
2470 CarveSelector::Remainder,
2471 Vec3::new(wl, 0.0, d),
2472 Vec3::new(sx - wl - wr, scope.size.y, sz - d),
2473 ),
2474 ];
2475 if queue.len() + parts.len() > MAX_QUEUE {
2476 return Err(ShapeError::CapacityOverflow);
2477 }
2478 for (selector, local_off, size) in parts {
2479 let Some(case) = find_carve_rule(selector, cases) else {
2480 continue;
2481 };
2482 let pos = scope.position + scope.rotation * local_off;
2483 let child = Scope::new(pos, scope.rotation, size);
2484 child.validate()?;
2485 let args = ev_args!(&case.rule);
2486 queue.push_back(WorkItem {
2487 scope: child,
2488 rule: case.rule.name.clone(),
2489 args,
2490 depth: depth + 1,
2491 taper: 0.0,
2492 face_profile_override: None,
2493 material: material.clone(),
2494 split_i,
2495 split_n,
2496 rng_state: fork!(),
2497 label: label.clone(),
2498 });
2499 }
2500 return Ok(());
2501 }
2502
2503 ShapeOp::Polygon(verts) => {
2504 if verts.len() < 3 {
2505 return Err(ShapeError::InvalidNumericValue);
2506 }
2507 for v in verts {
2508 if !v.is_finite() {
2509 return Err(ShapeError::InvalidNumericValue);
2510 }
2511 }
2512 face_profile = Some(FaceProfile::Polygon(verts.clone()));
2513 }
2514
2515 ShapeOp::RegSnap(label) => {
2517 register_scope_snap_planes(&scope, label, &mut model.snap_planes);
2518 }
2519
2520 ShapeOp::IfClear { rule, label: filt } => {
2526 let occluded = model
2527 .terminals
2528 .iter()
2529 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2530 .any(|t| scope_obb_overlaps_terminal(&scope, t));
2531 if !occluded {
2532 if queue.len() >= MAX_QUEUE {
2533 return Err(ShapeError::CapacityOverflow);
2534 }
2535 let args = ev_args!(rule);
2536 queue.push_back(WorkItem {
2537 scope,
2538 rule: rule.name.clone(),
2539 args,
2540 depth: depth + 1,
2541 taper,
2542 face_profile_override: face_profile.take(),
2543 material: material.clone(),
2544 split_i,
2545 split_n,
2546 rng_state: fork!(),
2547 label: label.clone(),
2548 });
2549 }
2550 return Ok(());
2551 }
2552 ShapeOp::IfOccluded { rule, label: filt } => {
2553 let occluded = model
2554 .terminals
2555 .iter()
2556 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2557 .any(|t| scope_obb_overlaps_terminal(&scope, t));
2558 if occluded {
2559 if queue.len() >= MAX_QUEUE {
2560 return Err(ShapeError::CapacityOverflow);
2561 }
2562 let args = ev_args!(rule);
2563 queue.push_back(WorkItem {
2564 scope,
2565 rule: rule.name.clone(),
2566 args,
2567 depth: depth + 1,
2568 taper,
2569 face_profile_override: face_profile.take(),
2570 material: material.clone(),
2571 split_i,
2572 split_n,
2573 rng_state: fork!(),
2574 label: label.clone(),
2575 });
2576 }
2577 return Ok(());
2578 }
2579
2580 ShapeOp::IfInside { rule, label: filt } => {
2581 let inside = model
2582 .terminals
2583 .iter()
2584 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2585 .any(|t| crate::query::scope_inside_terminal(&scope, t));
2586 if inside {
2587 if queue.len() >= MAX_QUEUE {
2588 return Err(ShapeError::CapacityOverflow);
2589 }
2590 let args = ev_args!(rule);
2591 queue.push_back(WorkItem {
2592 scope,
2593 rule: rule.name.clone(),
2594 args,
2595 depth: depth + 1,
2596 taper,
2597 face_profile_override: face_profile.take(),
2598 material: material.clone(),
2599 split_i,
2600 split_n,
2601 rng_state: fork!(),
2602 label: label.clone(),
2603 });
2604 }
2605 return Ok(());
2606 }
2607
2608 ShapeOp::IfTouches { rule, label: filt } => {
2609 let touches = model
2610 .terminals
2611 .iter()
2612 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2613 .any(|t| crate::query::scope_touches_terminal(&scope, t));
2614 if touches {
2615 if queue.len() >= MAX_QUEUE {
2616 return Err(ShapeError::CapacityOverflow);
2617 }
2618 let args = ev_args!(rule);
2619 queue.push_back(WorkItem {
2620 scope,
2621 rule: rule.name.clone(),
2622 args,
2623 depth: depth + 1,
2624 taper,
2625 face_profile_override: face_profile.take(),
2626 material: material.clone(),
2627 split_i,
2628 split_n,
2629 rng_state: fork!(),
2630 label: label.clone(),
2631 });
2632 }
2633 return Ok(());
2634 }
2635
2636 ShapeOp::Pick { key, choices } => {
2641 if choices.is_empty() {
2642 return Ok(());
2643 }
2644 let mut pick_rng = Pcg64::seed_from_u64(splitmix64(self.seed ^ fnv1a(key)));
2645 use rand::Rng as _;
2646 let total: f64 = choices.iter().map(|(w, _)| w).sum();
2647 let chosen = if total <= 0.0 {
2648 &choices[0].1
2649 } else {
2650 let r = pick_rng.random::<f64>() * total;
2651 let mut acc = 0.0;
2652 let mut sel = &choices[choices.len() - 1].1;
2653 for (w, call) in choices {
2654 acc += w;
2655 if r < acc {
2656 sel = call;
2657 break;
2658 }
2659 }
2660 sel
2661 };
2662 if queue.len() >= MAX_QUEUE {
2663 return Err(ShapeError::CapacityOverflow);
2664 }
2665 let args = ev_args!(chosen);
2666 queue.push_back(WorkItem {
2667 scope,
2668 rule: chosen.name.clone(),
2669 args,
2670 depth: depth + 1,
2671 taper,
2672 face_profile_override: face_profile.take(),
2673 material: material.clone(),
2674 split_i,
2675 split_n,
2676 rng_state: fork!(),
2677 label: label.clone(),
2678 });
2679 return Ok(());
2680 }
2681
2682 ShapeOp::Scatter {
2684 volume,
2685 count,
2686 rule,
2687 } => {
2688 let n_f = ev!(count);
2689 if n_f < 0.0 {
2690 return Err(ShapeError::InvalidNumericValue);
2691 }
2692 let n = (n_f.floor() as usize).min(MAX_SCATTER_POINTS);
2693 if queue.len() + n > MAX_QUEUE {
2694 return Err(ShapeError::CapacityOverflow);
2695 }
2696 use rand::Rng as _;
2697 for i in 0..n {
2698 let px = rng.random::<f64>() * scope.size.x;
2699 let py = if *volume {
2700 rng.random::<f64>() * scope.size.y
2701 } else {
2702 scope.size.y
2703 };
2704 let pz = rng.random::<f64>() * scope.size.z;
2705 let pos = scope.position + scope.rotation * Vec3::new(px, py, pz);
2706 let child = Scope::new(pos, scope.rotation, Vec3::ZERO);
2707 child.validate()?;
2708 let args = ev_args!(rule);
2709 queue.push_back(WorkItem {
2710 scope: child,
2711 rule: rule.name.clone(),
2712 args,
2713 depth: depth + 1,
2714 taper: 0.0,
2715 face_profile_override: None,
2716 material: material.clone(),
2717 split_i: i,
2718 split_n: n,
2719 rng_state: fork!(),
2720 label: label.clone(),
2721 });
2722 }
2723 return Ok(());
2724 }
2725
2726 ShapeOp::Align { local_axis, target } => {
2728 let len_sq = target.length_squared();
2733 if !target.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
2734 return Err(ShapeError::InvalidAlignTarget);
2735 }
2736 let target_norm = target.normalize();
2737 let current = scope.rotation * axis_vec(*local_axis);
2738 let dot = current.dot(target_norm);
2741 let q = if (dot + 1.0).abs() < 1e-9 {
2742 let perp = if current.x.abs() <= current.y.abs()
2748 && current.x.abs() <= current.z.abs()
2749 {
2750 current.cross(Vec3::X).normalize()
2751 } else if current.y.abs() <= current.z.abs() {
2752 current.cross(Vec3::Y).normalize()
2753 } else {
2754 current.cross(Vec3::Z).normalize()
2755 };
2756 Quat::from_axis_angle(perp, PI)
2757 } else {
2758 Quat::from_rotation_arc(current, target_norm)
2759 };
2760 scope.rotation = (q * scope.rotation).normalize();
2761 }
2762
2763 ShapeOp::Offset { distance, cases } => {
2765 let distance = ev!(distance);
2766 if distance == 0.0 {
2770 return Err(ShapeError::InvalidNumericValue);
2771 }
2772 let inset = -distance;
2773 let sx = scope.size.x;
2774 let sy = scope.size.y;
2775 let inside_w = sx - 2.0 * inset;
2776 let inside_h = sy - 2.0 * inset;
2777 if !inside_w.is_finite()
2781 || !inside_h.is_finite()
2782 || inside_w < 0.0
2783 || inside_h < 0.0
2784 {
2785 return Err(ShapeError::OffsetTooLarge);
2786 }
2787 if let Some(rule) = find_offset_rule(OffsetSelector::Inside, cases) {
2788 if queue.len() >= MAX_QUEUE {
2789 return Err(ShapeError::CapacityOverflow);
2790 }
2791 let pos = scope.position + scope.rotation * Vec3::new(inset, inset, 0.0);
2792 let child_scope =
2793 Scope::new(pos, scope.rotation, Vec3::new(inside_w, inside_h, 0.0));
2794 child_scope.validate()?;
2795 let args = ev_args!(rule);
2796 queue.push_back(WorkItem {
2797 scope: child_scope,
2798 rule: rule.name.clone(),
2799 args,
2800 depth: depth + 1,
2801 taper: 0.0,
2802 face_profile_override: None,
2803 material: material.clone(),
2804 split_i,
2805 split_n,
2806 rng_state: fork!(),
2807 label: label.clone(),
2808 });
2809 }
2810 if let Some(rule) = find_offset_rule(OffsetSelector::Border, cases) {
2811 let strips = [
2813 (Vec3::new(0.0, 0.0, 0.0), Vec3::new(sx, inset, 0.0)),
2814 (Vec3::new(0.0, sy - inset, 0.0), Vec3::new(sx, inset, 0.0)),
2815 (
2816 Vec3::new(0.0, inset, 0.0),
2817 Vec3::new(inset, sy - 2.0 * inset, 0.0),
2818 ),
2819 (
2820 Vec3::new(sx - inset, inset, 0.0),
2821 Vec3::new(inset, sy - 2.0 * inset, 0.0),
2822 ),
2823 ];
2824 if queue.len() + strips.len() > MAX_QUEUE {
2825 return Err(ShapeError::CapacityOverflow);
2826 }
2827 for (local_off, strip_size) in strips {
2828 let pos = scope.position + scope.rotation * local_off;
2829 let child_scope = Scope::new(pos, scope.rotation, strip_size);
2830 child_scope.validate()?;
2831 let args = ev_args!(rule);
2832 queue.push_back(WorkItem {
2833 scope: child_scope,
2834 rule: rule.name.clone(),
2835 args,
2836 depth: depth + 1,
2837 taper: 0.0,
2838 face_profile_override: None,
2839 material: material.clone(),
2840 split_i,
2841 split_n,
2842 rng_state: fork!(),
2843 label: label.clone(),
2844 });
2845 }
2846 }
2847 return Ok(());
2848 }
2849
2850 ShapeOp::Roof { spec, cases } => {
2852 let pitch = if let Some(h_expr) = &spec.height {
2857 let h = ev!(h_expr);
2858 if h <= 0.0 {
2859 return Err(ShapeError::InvalidNumericValue);
2860 }
2861 let run = if spec.roof_type == RoofType::Shed {
2864 scope.size.z.max(1e-9)
2865 } else {
2866 (scope.size.x.min(scope.size.z) / 2.0).max(1e-9)
2867 };
2868 (h / run).atan().to_degrees()
2869 } else {
2870 ev!(&spec.pitch)
2871 };
2872 let secondary_pitch = match &spec.secondary_pitch {
2873 Some(e) => Some(ev!(e)),
2874 None => None,
2875 };
2876 let tier_height = match &spec.tier_height {
2877 Some(e) => Some(ev!(e)),
2878 None => None,
2879 };
2880 let config = RoofConfig {
2881 roof_type: spec.roof_type,
2882 pitch,
2883 secondary_pitch,
2884 overhang: ev!(&spec.overhang),
2885 ridge_offset: ev!(&spec.ridge_offset),
2886 fascia_depth: ev!(&spec.fascia_depth),
2887 tier_height,
2888 };
2889 let resolved_cases = {
2890 let mut rcs = Vec::with_capacity(cases.len());
2891 for c in cases {
2892 let args = ev_args!(&c.rule);
2893 rcs.push(ResolvedRoofCase {
2894 selector: c.selector,
2895 name: c.rule.name.clone(),
2896 args,
2897 });
2898 }
2899 rcs
2900 };
2901 apply_roof(
2902 &config,
2903 &resolved_cases,
2904 &scope,
2905 depth,
2906 &material,
2907 queue,
2908 model,
2909 self.max_terminals,
2910 split_i,
2911 split_n,
2912 rng_state,
2913 spec.ridge_axis,
2914 &label,
2915 )?;
2916 return Ok(());
2917 }
2918
2919 ShapeOp::Attach { world_axis, cases } => {
2921 let len_sq = world_axis.length_squared();
2922 if !world_axis.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
2923 return Err(ShapeError::InvalidAlignTarget);
2924 }
2925 let axis_norm = world_axis.normalize();
2926 let rot = Quat::from_rotation_arc(Vec3::Y, axis_norm);
2930 let attach_scope = Scope::new(
2931 scope.position,
2932 rot.normalize(),
2933 Vec3::new(scope.size.x, scope.size.y, 0.0),
2934 );
2935 attach_scope.validate()?;
2936 if let Some(rule) = find_attach_rule(crate::ops::AttachSelector::Surface, cases)
2937 {
2938 if queue.len() >= MAX_QUEUE {
2939 return Err(ShapeError::CapacityOverflow);
2940 }
2941 let args = ev_args!(rule);
2942 queue.push_back(WorkItem {
2943 scope: attach_scope,
2944 rule: rule.name.clone(),
2945 args,
2946 depth: depth + 1,
2947 taper: 0.0,
2948 face_profile_override: None,
2949 material: material.clone(),
2950 split_i,
2951 split_n,
2952 rng_state: fork!(),
2953 label: label.clone(),
2954 });
2955 }
2956 return Ok(());
2957 }
2958
2959 ShapeOp::Split {
2961 axis,
2962 entries,
2963 snap,
2964 } => {
2965 let total = match axis {
2966 Axis::X => scope.size.x,
2967 Axis::Y => scope.size.y,
2968 Axis::Z => scope.size.z,
2969 };
2970 let mut flat: Vec<(&SplitSlot, ResolvedSize)> = Vec::new();
2973 {
2974 macro_rules! resolve_size {
2975 ($slot:expr) => {
2976 match &$slot.size {
2977 SplitSize::Absolute(e) => ResolvedSize::Absolute(ev!(e)),
2978 SplitSize::Relative(e) => ResolvedSize::Relative(ev!(e)),
2979 SplitSize::Floating(e) => ResolvedSize::Floating(ev!(e)),
2980 }
2981 };
2982 }
2983 let mut resolved_entries: Vec<(usize, Vec<ResolvedSize>)> =
2987 Vec::with_capacity(entries.len());
2988 let mut outside_fixed = 0.0_f64;
2989 let mut outside_float_w = 0.0_f64;
2990 let mut group_nominal = 0.0_f64;
2991 let mut group_len = 0usize;
2992 for (ei, entry) in entries.iter().enumerate() {
2993 match entry {
2994 SplitEntry::Slot(slot) => {
2995 let r = resolve_size!(slot);
2996 match r {
2997 ResolvedSize::Absolute(v) => outside_fixed += v,
2998 ResolvedSize::Relative(t) => outside_fixed += t * total,
2999 ResolvedSize::Floating(w) => outside_float_w += w,
3000 }
3001 resolved_entries.push((ei, vec![r]));
3002 }
3003 SplitEntry::Group(slots) => {
3004 let mut rs = Vec::with_capacity(slots.len());
3005 for slot in slots {
3006 let r = resolve_size!(slot);
3007 group_nominal += match r {
3008 ResolvedSize::Absolute(v) => v,
3009 ResolvedSize::Relative(t) => t * total,
3010 ResolvedSize::Floating(w) => w,
3011 };
3012 rs.push(r);
3013 }
3014 group_len = slots.len();
3015 resolved_entries.push((ei, rs));
3016 }
3017 }
3018 }
3019 if !outside_fixed.is_finite()
3020 || !outside_float_w.is_finite()
3021 || !group_nominal.is_finite()
3022 {
3023 return Err(ShapeError::InvalidNumericValue);
3024 }
3025 if outside_fixed > total + 1e-9 {
3026 return Err(ShapeError::SplitOverflow(total));
3027 }
3028 let remaining = (total - outside_fixed).max(0.0);
3029 let k = if group_len > 0 && group_nominal > 1e-12 {
3031 ((remaining + 1e-9) / group_nominal).floor() as usize
3032 } else {
3033 0
3034 };
3035 let singles = entries.len() - usize::from(group_len > 0);
3036 if singles + k * group_len > MAX_SPLIT_CHILDREN {
3037 return Err(ShapeError::CapacityOverflow);
3038 }
3039 let leftover = remaining - k as f64 * group_nominal;
3040 let copy_scale = if group_len > 0 && outside_float_w <= 0.0 {
3044 if k == 0 {
3045 if remaining > 1e-9 {
3046 return Err(ShapeError::SplitOverflow(total));
3047 }
3048 1.0
3049 } else {
3050 remaining / (k as f64 * group_nominal)
3051 }
3052 } else {
3053 1.0
3054 };
3055 for (ei, rs) in &resolved_entries {
3056 match &entries[*ei] {
3057 SplitEntry::Slot(slot) => {
3058 let r = match rs[0] {
3059 ResolvedSize::Floating(w) => {
3060 if outside_float_w <= 0.0 {
3064 return Err(ShapeError::NoFloatingSlots);
3065 }
3066 ResolvedSize::Absolute(leftover * (w / outside_float_w))
3067 }
3068 other => other,
3069 };
3070 flat.push((slot, r));
3071 }
3072 SplitEntry::Group(slots) => {
3073 for _copy in 0..k {
3074 for (slot, r) in slots.iter().zip(rs.iter()) {
3075 let v = match *r {
3076 ResolvedSize::Absolute(v) => v,
3077 ResolvedSize::Relative(t) => t * total,
3078 ResolvedSize::Floating(w) => w,
3079 };
3080 flat.push((
3081 slot,
3082 ResolvedSize::Absolute(v * copy_scale),
3083 ));
3084 }
3085 }
3086 }
3087 }
3088 }
3089 }
3090 let resolved: Vec<ResolvedSize> = flat.iter().map(|(_, r)| *r).collect();
3091 let mut sizes = resolve_split_sizes(&resolved, total)?;
3092 if let Some(binding) = snap {
3097 let tol = binding.tolerance.unwrap_or(0.05 * total);
3098 snap_split_boundaries(
3099 &scope,
3100 *axis,
3101 &mut sizes,
3102 &binding.label,
3103 tol,
3104 &model.snap_planes,
3105 );
3106 }
3107 if queue.len() + flat.len() > MAX_QUEUE {
3108 return Err(ShapeError::CapacityOverflow);
3109 }
3110 let child_n = flat.len();
3111 let mut offset = 0.0;
3112 for (i, ((slot, _), size)) in flat.iter().zip(sizes.iter()).enumerate() {
3113 let child = slice_scope(&scope, *axis, offset, *size);
3114 child.validate()?;
3115 let args = ev_args!(&slot.rule);
3116 queue.push_back(WorkItem {
3117 scope: child,
3118 rule: slot.rule.name.clone(),
3119 args,
3120 depth: depth + 1,
3121 taper: 0.0,
3122 face_profile_override: None,
3123 material: material.clone(),
3124 split_i: i,
3125 split_n: child_n,
3126 rng_state: fork!(),
3127 label: label.clone(),
3128 });
3129 offset += size;
3130 }
3131 return Ok(());
3132 }
3133
3134 ShapeOp::SplitArea { axis, slots } => {
3139 let (total, cross) = match axis {
3140 Axis::X => (scope.size.x, scope.size.z),
3141 Axis::Z => (scope.size.z, scope.size.x),
3142 Axis::Y => return Err(ShapeError::InvalidNumericValue),
3143 };
3144 if !cross.is_finite() || cross <= 1e-12 {
3145 return Err(ShapeError::InvalidNumericValue);
3146 }
3147 let resolved: Vec<ResolvedSize> = {
3148 let mut v = Vec::with_capacity(slots.len());
3149 for slot in slots {
3150 v.push(match &slot.size {
3151 SplitSize::Absolute(e) => ResolvedSize::Absolute(ev!(e) / cross),
3154 SplitSize::Relative(e) => ResolvedSize::Relative(ev!(e)),
3155 SplitSize::Floating(e) => ResolvedSize::Floating(ev!(e)),
3156 });
3157 }
3158 v
3159 };
3160 let sizes = resolve_split_sizes(&resolved, total)?;
3161 if queue.len() + slots.len() > MAX_QUEUE {
3162 return Err(ShapeError::CapacityOverflow);
3163 }
3164 let child_n = slots.len();
3165 let mut offset = 0.0;
3166 for (i, (slot, size)) in slots.iter().zip(sizes.iter()).enumerate() {
3167 let child = slice_scope(&scope, *axis, offset, *size);
3168 child.validate()?;
3169 let args = ev_args!(&slot.rule);
3170 queue.push_back(WorkItem {
3171 scope: child,
3172 rule: slot.rule.name.clone(),
3173 args,
3174 depth: depth + 1,
3175 taper: 0.0,
3176 face_profile_override: None,
3177 material: material.clone(),
3178 split_i: i,
3179 split_n: child_n,
3180 rng_state: fork!(),
3181 label: label.clone(),
3182 });
3183 offset += size;
3184 }
3185 return Ok(());
3186 }
3187
3188 ShapeOp::Fit { axis, candidates } => {
3193 let extent = match axis {
3194 Axis::X => scope.size.x,
3195 Axis::Y => scope.size.y,
3196 Axis::Z => scope.size.z,
3197 };
3198 for cand in candidates {
3199 let min = ev!(&cand.min_size);
3200 if min <= extent + 1e-9 {
3201 if queue.len() >= MAX_QUEUE {
3202 return Err(ShapeError::CapacityOverflow);
3203 }
3204 let args = ev_args!(&cand.rule);
3205 queue.push_back(WorkItem {
3206 scope,
3207 rule: cand.rule.name.clone(),
3208 args,
3209 depth: depth + 1,
3210 taper,
3211 face_profile_override: face_profile.take(),
3212 material: material.clone(),
3213 split_i,
3214 split_n,
3215 rng_state: fork!(),
3216 label: label.clone(),
3217 });
3218 return Ok(());
3219 }
3220 }
3221 return Ok(());
3222 }
3223
3224 ShapeOp::Repeat {
3230 axis,
3231 tile_sizes,
3232 rule,
3233 } => {
3234 if tile_sizes.is_empty() {
3235 return Err(ShapeError::InvalidNumericValue);
3236 }
3237 let tile_sizes: Vec<f64> = {
3238 let mut v = Vec::with_capacity(tile_sizes.len());
3239 for ts in tile_sizes {
3240 let t = ev!(ts);
3241 if t <= 0.0 {
3242 return Err(ShapeError::InvalidNumericValue);
3243 }
3244 v.push(t);
3245 }
3246 v
3247 };
3248 let total = match axis {
3249 Axis::X => scope.size.x,
3250 Axis::Y => scope.size.y,
3251 Axis::Z => scope.size.z,
3252 };
3253 if !total.is_finite() || total <= 0.0 {
3257 return Err(ShapeError::InvalidNumericValue);
3258 }
3259 let pattern_min = tile_sizes.iter().cloned().fold(f64::INFINITY, f64::min);
3260 if pattern_min <= 0.0 {
3261 return Err(ShapeError::InvalidNumericValue);
3262 }
3263 let n_max_f = (total / pattern_min).floor();
3268 if !n_max_f.is_finite() {
3269 return Err(ShapeError::CapacityOverflow);
3270 }
3271 let n_max = n_max_f as usize;
3272 if queue.len().saturating_add(n_max) > MAX_QUEUE {
3273 return Err(ShapeError::CapacityOverflow);
3274 }
3275 let mut placed: Vec<f64> = Vec::new();
3279 let mut acc = 0.0_f64;
3280 loop {
3281 let next = tile_sizes[placed.len() % tile_sizes.len()];
3282 if acc + next > total + 1e-12 {
3283 break;
3284 }
3285 placed.push(next);
3286 acc += next;
3287 }
3288 if !placed.is_empty() {
3289 let scale = total / acc;
3290 let child_n = placed.len();
3291 let mut offset = 0.0_f64;
3292 for (i, tile) in placed.iter().enumerate() {
3293 let actual = tile * scale;
3294 let child = slice_scope(&scope, *axis, offset, actual);
3295 child.validate()?;
3296 let args = ev_args!(rule);
3299 queue.push_back(WorkItem {
3300 scope: child,
3301 rule: rule.name.clone(),
3302 args,
3303 depth: depth + 1,
3304 taper: 0.0,
3305 face_profile_override: None,
3306 material: material.clone(),
3307 split_i: i,
3308 split_n: child_n,
3309 rng_state: fork!(),
3310 label: label.clone(),
3311 });
3312 offset += actual;
3313 }
3314 }
3315 return Ok(());
3316 }
3317
3318 ShapeOp::Comp(CompTarget::Edges(cases)) => {
3324 let descs = edge_descs(scope.size);
3325 if queue.len() + descs.len() > MAX_QUEUE {
3326 return Err(ShapeError::CapacityOverflow);
3327 }
3328 for (class, origin, dir, len) in descs {
3329 let Some(rule) = find_edge_rule(class, cases) else {
3330 continue;
3331 };
3332 if len <= 1e-9 {
3333 continue;
3334 }
3335 let pos = scope.position + scope.rotation * origin;
3336 let rot =
3338 (scope.rotation * Quat::from_rotation_arc(Vec3::X, dir)).normalize();
3339 let child = Scope::new(pos, rot, Vec3::new(len, 0.0, 0.0));
3340 child.validate()?;
3341 let args = ev_args!(rule);
3342 queue.push_back(WorkItem {
3343 scope: child,
3344 rule: rule.name.clone(),
3345 args,
3346 depth: depth + 1,
3347 taper: 0.0,
3348 face_profile_override: None,
3349 material: material.clone(),
3350 split_i,
3351 split_n,
3352 rng_state: fork!(),
3353 label: label.clone(),
3354 });
3355 }
3356 return Ok(());
3357 }
3358
3359 ShapeOp::Comp(CompTarget::Faces(cases)) => {
3360 if queue.len() + 6 > MAX_QUEUE {
3362 return Err(ShapeError::CapacityOverflow);
3363 }
3364 for (selector, offset_local, face_size, rot_delta) in face_descs(scope.size) {
3365 let rule = match find_face_rule(selector, cases) {
3366 Some(r) => r,
3367 None => continue,
3368 };
3369 let face_pos = scope.position + scope.rotation * offset_local;
3370 let face_rotation = scope.rotation * rot_delta;
3371 let face_scope = Scope::new(face_pos, face_rotation, face_size);
3372 face_scope.validate()?;
3373 let args = ev_args!(&rule);
3374 queue.push_back(WorkItem {
3375 scope: face_scope,
3376 rule: rule.name.clone(),
3377 args,
3378 depth: depth + 1,
3379 taper: 0.0,
3380 face_profile_override: None,
3381 material: material.clone(),
3382 split_i,
3383 split_n,
3384 rng_state: fork!(),
3385 label: label.clone(),
3386 });
3387 }
3388 return Ok(());
3389 }
3390
3391 ShapeOp::I(mesh_id) => {
3393 if model.len() >= self.max_terminals {
3394 return Err(ShapeError::CapacityOverflow);
3395 }
3396 let profile = face_profile
3397 .take()
3398 .unwrap_or_else(|| taper_to_profile(taper));
3399 let mut terminal = Terminal::new_profiled(scope, mesh_id, profile, material);
3400 terminal.label = label;
3401 model.push(terminal);
3402 return Ok(());
3403 }
3404
3405 ShapeOp::Rule(call) => {
3407 let args = ev_args!(call);
3408 queue.push_back(WorkItem {
3409 scope,
3410 rule: call.name.clone(),
3411 args,
3412 depth: depth + 1,
3413 taper,
3414 face_profile_override: face_profile,
3415 material,
3416 split_i,
3417 split_n,
3418 rng_state: fork!(),
3419 label: label.clone(),
3420 });
3421 return Ok(());
3422 }
3423 }
3424 }
3425
3426 Ok(())
3429 }
3430}
3431
3432#[cfg(test)]
3433mod tests {
3434 use super::*;
3435 use crate::ops::{Axis, SplitSize, SplitSlot};
3436 use crate::scope::{Quat, Vec3};
3437
3438 fn slot(size: SplitSize, rule: &str) -> SplitSlot {
3439 SplitSlot {
3440 size,
3441 rule: rule.into(),
3442 }
3443 }
3444
3445 fn qexpr(q: Quat) -> [Expr; 4] {
3447 [
3448 Expr::lit(q.w),
3449 Expr::lit(q.x),
3450 Expr::lit(q.y),
3451 Expr::lit(q.z),
3452 ]
3453 }
3454
3455 fn resolved(slots: &[SplitSlot]) -> Vec<ResolvedSize> {
3457 slots
3458 .iter()
3459 .map(|s| match &s.size {
3460 SplitSize::Absolute(e) => ResolvedSize::Absolute(e.as_lit().unwrap()),
3461 SplitSize::Relative(e) => ResolvedSize::Relative(e.as_lit().unwrap()),
3462 SplitSize::Floating(e) => ResolvedSize::Floating(e.as_lit().unwrap()),
3463 })
3464 .collect()
3465 }
3466
3467 #[test]
3468 fn test_resolve_split_absolute() {
3469 let slots = vec![
3470 slot(SplitSize::abs(3.0), "A"),
3471 slot(SplitSize::abs(7.0), "B"),
3472 ];
3473 let sizes = resolve_split_sizes(&resolved(&slots), 10.0).unwrap();
3474 assert!((sizes[0] - 3.0).abs() < 1e-9);
3475 assert!((sizes[1] - 7.0).abs() < 1e-9);
3476 }
3477
3478 #[test]
3479 fn test_resolve_split_floating_equal() {
3480 let slots = vec![
3481 slot(SplitSize::float(1.0), "A"),
3482 slot(SplitSize::float(1.0), "B"),
3483 ];
3484 let sizes = resolve_split_sizes(&resolved(&slots), 10.0).unwrap();
3485 assert!((sizes[0] - 5.0).abs() < 1e-9);
3486 assert!((sizes[1] - 5.0).abs() < 1e-9);
3487 }
3488
3489 #[test]
3490 fn test_resolve_split_mixed() {
3491 let slots = vec![
3492 slot(SplitSize::abs(2.0), "Base"),
3493 slot(SplitSize::float(1.0), "A"),
3494 slot(SplitSize::float(1.0), "B"),
3495 ];
3496 let sizes = resolve_split_sizes(&resolved(&slots), 10.0).unwrap();
3497 assert!((sizes[0] - 2.0).abs() < 1e-9);
3498 assert!((sizes[1] - 4.0).abs() < 1e-9);
3499 assert!((sizes[2] - 4.0).abs() < 1e-9);
3500 }
3501
3502 #[test]
3503 fn test_resolve_split_overflow_rejected() {
3504 let slots = vec![
3505 slot(SplitSize::abs(6.0), "A"),
3506 slot(SplitSize::abs(6.0), "B"),
3507 ];
3508 assert!(matches!(
3509 resolve_split_sizes(&resolved(&slots), 10.0),
3510 Err(ShapeError::SplitOverflow(_))
3511 ));
3512 }
3513
3514 #[test]
3515 fn test_derive_extrude_then_terminal() {
3516 let mut interp = Interpreter::new();
3517 interp.add_rule(
3518 "Lot",
3519 vec![
3520 ShapeOp::Extrude(Expr::lit(10.0)),
3521 ShapeOp::I("Building".to_string()),
3522 ],
3523 );
3524 let scope = Scope::unit();
3525 let model = interp.derive(scope, "Lot").unwrap();
3526 assert_eq!(model.len(), 1);
3527 assert_eq!(model.terminals[0].mesh_id, "Building");
3528 assert!((model.terminals[0].scope.size.y - 10.0).abs() < 1e-9);
3529 }
3530
3531 #[test]
3532 fn test_derive_split_y_three_floors() {
3533 let mut interp = Interpreter::new();
3534 interp.add_rule(
3535 "Building",
3536 vec![ShapeOp::Split {
3537 axis: Axis::Y,
3538 entries: vec![
3539 slot(SplitSize::abs(2.0), "Ground").into(),
3540 slot(SplitSize::float(1.0), "Upper").into(),
3541 slot(SplitSize::abs(1.5), "Roof").into(),
3542 ],
3543 snap: None,
3544 }],
3545 );
3546 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 10.0, 10.0));
3547 let model = interp.derive(scope, "Building").unwrap();
3548 assert_eq!(model.len(), 3);
3549 assert!((model.terminals[0].scope.size.y - 2.0).abs() < 1e-9);
3550 assert!((model.terminals[1].scope.size.y - 6.5).abs() < 1e-9);
3551 assert!((model.terminals[2].scope.size.y - 1.5).abs() < 1e-9);
3552 }
3553
3554 #[test]
3555 fn test_derive_depth_limit() {
3556 let mut interp = Interpreter::new();
3557 interp.add_rule("A", vec![ShapeOp::Rule("A".into())]);
3558 interp.max_depth = 5;
3559 let model = interp.derive(Scope::unit(), "A");
3560 assert!(matches!(model, Err(ShapeError::DepthLimitExceeded(_))));
3561 }
3562
3563 #[test]
3564 fn test_derive_comp_faces() {
3565 let mut interp = Interpreter::new();
3566 interp.add_rule(
3567 "Box",
3568 vec![ShapeOp::Comp(CompTarget::Faces(vec![
3569 crate::ops::CompFaceCase {
3570 selector: FaceSelector::Top,
3571 rule: "Roof".into(),
3572 },
3573 crate::ops::CompFaceCase {
3574 selector: FaceSelector::Side,
3575 rule: "Wall".into(),
3576 },
3577 crate::ops::CompFaceCase {
3578 selector: FaceSelector::Bottom,
3579 rule: "Base".into(),
3580 },
3581 ]))],
3582 );
3583 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(5.0, 3.0, 5.0));
3584 let model = interp.derive(scope, "Box").unwrap();
3585 assert_eq!(model.len(), 6);
3586 }
3587
3588 #[test]
3589 fn test_derive_repeat() {
3590 let mut interp = Interpreter::new();
3591 interp.add_rule(
3592 "Facade",
3593 vec![ShapeOp::Repeat {
3594 axis: Axis::X,
3595 tile_sizes: vec![Expr::lit(2.0)],
3596 rule: "Window".into(),
3597 }],
3598 );
3599 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 0.0));
3600 let model = interp.derive(scope, "Facade").unwrap();
3601 assert_eq!(model.len(), 5);
3603 }
3604
3605 #[test]
3606 fn test_derive_mat_propagates() {
3607 let mut interp = Interpreter::new();
3608 interp.add_rule(
3609 "R",
3610 vec![
3611 ShapeOp::Mat(Material::new("Brick")),
3612 ShapeOp::I("Wall".to_string()),
3613 ],
3614 );
3615 let model = interp.derive(Scope::unit(), "R").unwrap();
3616 assert_eq!(model.terminals[0].material, Some(Material::new("Brick")));
3617 }
3618
3619 #[test]
3622 fn test_empty_variants_discards_shape() {
3623 let mut interp = Interpreter::new();
3624 interp.add_weighted_rules("Empty", vec![]).unwrap();
3627 let model = interp.derive(Scope::unit(), "Empty").unwrap();
3628 assert_eq!(model.len(), 0);
3629 }
3630
3631 #[test]
3634 fn test_repeat_tiny_tile_size_rejected() {
3635 let mut interp = Interpreter::new();
3640 interp.add_rule(
3641 "R",
3642 vec![ShapeOp::Repeat {
3643 axis: Axis::X,
3644 tile_sizes: vec![Expr::lit(f64::MIN_POSITIVE)],
3645 rule: "Tile".into(),
3646 }],
3647 );
3648 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
3649 assert!(matches!(
3650 interp.derive(scope, "R"),
3651 Err(ShapeError::CapacityOverflow)
3652 ));
3653 }
3654
3655 #[test]
3658 fn test_scale_multiply_overflow_to_infinity_rejected() {
3659 let mut interp = Interpreter::new();
3662 interp.add_rule(
3663 "R",
3664 vec![
3665 ShapeOp::Scale([Expr::lit(1e200), Expr::lit(1.0), Expr::lit(1.0)]),
3666 ShapeOp::Scale([Expr::lit(1e200), Expr::lit(1.0), Expr::lit(1.0)]), ShapeOp::I("Mesh".to_string()),
3668 ],
3669 );
3670 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
3671 assert!(matches!(
3672 interp.derive(scope, "R"),
3673 Err(ShapeError::InvalidNumericValue)
3674 ));
3675 }
3676
3677 #[test]
3678 fn test_split_absolute_sum_overflow_rejected() {
3679 let slots = vec![
3681 slot(SplitSize::abs(f64::MAX), "A"),
3682 slot(SplitSize::abs(f64::MAX), "B"),
3683 ];
3684 assert!(matches!(
3685 resolve_split_sizes(&resolved(&slots), f64::MAX),
3686 Err(ShapeError::InvalidNumericValue)
3687 ));
3688 }
3689
3690 #[test]
3693 fn test_repeat_respects_combined_queue_limit() {
3694 let mut interp = Interpreter::new();
3700 interp.add_rule(
3702 "Big",
3703 vec![ShapeOp::Repeat {
3704 axis: Axis::X,
3705 tile_sizes: vec![Expr::lit(1e-1)],
3706 rule: "Tile".into(),
3707 }],
3708 );
3709 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1e10, 1.0, 1.0));
3710 assert!(matches!(
3711 interp.derive(scope, "Big"),
3712 Err(ShapeError::CapacityOverflow)
3713 ));
3714 }
3715
3716 #[test]
3719 fn test_api_negative_scale_rejected() {
3720 let mut interp = Interpreter::new();
3721 interp.add_rule(
3722 "R",
3723 vec![
3724 ShapeOp::Scale([Expr::lit(-1.0), Expr::lit(1.0), Expr::lit(1.0)]),
3725 ShapeOp::I("Mesh".to_string()),
3726 ],
3727 );
3728 assert!(matches!(
3729 interp.derive(Scope::unit(), "R"),
3730 Err(ShapeError::InvalidNumericValue)
3731 ));
3732 }
3733
3734 #[test]
3735 fn test_api_zero_scale_rejected() {
3736 let mut interp = Interpreter::new();
3737 interp.add_rule(
3738 "R",
3739 vec![
3740 ShapeOp::Scale([Expr::lit(0.0), Expr::lit(1.0), Expr::lit(1.0)]),
3741 ShapeOp::I("Mesh".to_string()),
3742 ],
3743 );
3744 assert!(matches!(
3745 interp.derive(Scope::unit(), "R"),
3746 Err(ShapeError::InvalidNumericValue)
3747 ));
3748 }
3749
3750 #[test]
3753 fn test_split_floating_large_remaining_no_overflow() {
3754 let slots = vec![
3758 slot(SplitSize::float(2.0), "A"),
3759 slot(SplitSize::float(1.0), "B"),
3760 ];
3761 let sizes = resolve_split_sizes(&resolved(&slots), 1e308).unwrap();
3762 assert!(sizes[0].is_finite(), "size[0] overflowed to {}", sizes[0]);
3763 assert!(sizes[1].is_finite(), "size[1] overflowed to {}", sizes[1]);
3764 assert!((sizes[0] / sizes[1] - 2.0).abs() < 1e-6);
3766 }
3767
3768 #[test]
3771 fn test_split_floating_weight_overflow_rejected() {
3772 let slots = vec![
3775 slot(SplitSize::float(f64::MAX), "A"),
3776 slot(SplitSize::float(f64::MAX), "B"),
3777 ];
3778 assert!(matches!(
3779 resolve_split_sizes(&resolved(&slots), 10.0),
3780 Err(ShapeError::InvalidNumericValue)
3781 ));
3782 }
3783
3784 #[test]
3785 fn test_stochastic_rule_deterministic_with_seed() {
3786 let mut interp = Interpreter::new();
3787 interp
3788 .add_weighted_rules(
3789 "Facade",
3790 vec![
3791 (70.0, vec![ShapeOp::I("Brick".to_string())]),
3792 (30.0, vec![ShapeOp::I("Glass".to_string())]),
3793 ],
3794 )
3795 .unwrap();
3796 interp.seed = 42;
3797 let m1 = interp.derive(Scope::unit(), "Facade").unwrap();
3799 let m2 = interp.derive(Scope::unit(), "Facade").unwrap();
3800 assert_eq!(m1.terminals[0].mesh_id, m2.terminals[0].mesh_id);
3801 }
3802
3803 #[test]
3804 fn test_face_comp_orientations() {
3805 let mut interp = Interpreter::new();
3809 interp.add_rule(
3810 "Box",
3811 vec![ShapeOp::Comp(CompTarget::Faces(vec![
3812 crate::ops::CompFaceCase {
3813 selector: FaceSelector::All,
3814 rule: "Face".into(),
3815 },
3816 ]))],
3817 );
3818 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 2.0));
3819 let model = interp.derive(scope, "Box").unwrap();
3820 assert_eq!(model.len(), 6);
3821
3822 let normals: Vec<Vec3> = model
3824 .terminals
3825 .iter()
3826 .map(|t| t.scope.rotation * Vec3::Z)
3827 .collect();
3828
3829 let expected = [
3831 Vec3::NEG_Y, Vec3::Y, Vec3::NEG_Z, Vec3::Z, Vec3::NEG_X, Vec3::X, ];
3838 for exp in &expected {
3839 assert!(
3840 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
3841 "missing normal {:?}, got {:?}",
3842 exp,
3843 normals
3844 );
3845 }
3846
3847 let pos = |i: usize| model.terminals[i].scope.position;
3851 assert!(
3852 (pos(0) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
3853 "Bottom pos"
3854 ); assert!(
3856 (pos(1) - Vec3::new(0.0, 3.0, 2.0)).length() < 1e-6,
3857 "Top pos"
3858 ); assert!(
3860 (pos(2) - Vec3::new(4.0, 0.0, 0.0)).length() < 1e-6,
3861 "Front pos"
3862 ); assert!(
3864 (pos(3) - Vec3::new(0.0, 0.0, 2.0)).length() < 1e-6,
3865 "Back pos"
3866 ); assert!(
3868 (pos(4) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
3869 "Left pos"
3870 ); assert!(
3872 (pos(5) - Vec3::new(4.0, 0.0, 2.0)).length() < 1e-6,
3873 "Right pos"
3874 ); }
3876
3877 #[test]
3880 fn test_negative_scope_size_rejected() {
3881 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(-1.0, 1.0, 1.0));
3882 let interp = Interpreter::new();
3883 assert!(matches!(
3884 interp.derive(scope, "Anything"),
3885 Err(ShapeError::InvalidNumericValue)
3886 ));
3887 }
3888
3889 #[test]
3890 fn test_zero_scope_size_accepted() {
3891 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 0.0, 10.0));
3893 let interp = Interpreter::new();
3894 let model = interp.derive(scope, "Footprint").unwrap();
3895 assert_eq!(model.len(), 1);
3896 }
3897
3898 #[test]
3901 fn test_unnormalized_root_quat_rejected() {
3902 let bad_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
3904 let scope = Scope::new(Vec3::ZERO, bad_q, Vec3::ONE);
3905 let interp = Interpreter::new();
3906 assert!(matches!(
3907 interp.derive(scope, "Anything"),
3908 Err(ShapeError::InvalidNumericValue)
3909 ));
3910 }
3911
3912 #[test]
3913 fn test_degenerate_rotate_op_rejected() {
3914 let zero = [
3916 Expr::lit(0.0),
3917 Expr::lit(0.0),
3918 Expr::lit(0.0),
3919 Expr::lit(0.0),
3920 ];
3921 let mut interp = Interpreter::new();
3922 interp.add_rule(
3923 "R",
3924 vec![ShapeOp::Rotate(zero), ShapeOp::I("M".to_string())],
3925 );
3926 assert!(matches!(
3927 interp.derive(Scope::unit(), "R"),
3928 Err(ShapeError::InvalidNumericValue)
3929 ));
3930 }
3931
3932 #[test]
3933 fn test_scaled_rotate_op_normalized() {
3934 let scaled_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0); let mut interp = Interpreter::new();
3938 interp.add_rule(
3939 "R",
3940 vec![
3941 ShapeOp::Rotate(qexpr(scaled_q)),
3942 ShapeOp::I("M".to_string()),
3943 ],
3944 );
3945 let model = interp.derive(Scope::unit(), "R").unwrap();
3947 assert_eq!(model.len(), 1);
3948 let r = model.terminals[0].scope.rotation;
3949 assert!(
3950 (r.length_squared() - 1.0).abs() < 1e-9,
3951 "rotation should be unit"
3952 );
3953 }
3954
3955 #[test]
3958 fn test_nan_weight_rejected() {
3959 let mut interp = Interpreter::new();
3960 assert!(matches!(
3961 interp.add_weighted_rules("R", vec![(f64::NAN, vec![ShapeOp::I("M".to_string())])]),
3962 Err(ShapeError::InvalidNumericValue)
3963 ));
3964 }
3965
3966 #[test]
3967 fn test_infinite_weight_rejected() {
3968 let mut interp = Interpreter::new();
3969 assert!(matches!(
3970 interp.add_weighted_rules(
3971 "R",
3972 vec![(f64::INFINITY, vec![ShapeOp::I("M".to_string())])]
3973 ),
3974 Err(ShapeError::InvalidNumericValue)
3975 ));
3976 }
3977
3978 #[test]
3979 fn test_negative_weight_rejected() {
3980 let mut interp = Interpreter::new();
3981 assert!(matches!(
3982 interp.add_weighted_rules("R", vec![(-1.0, vec![ShapeOp::I("M".to_string())])]),
3983 Err(ShapeError::InvalidNumericValue)
3984 ));
3985 }
3986
3987 #[test]
3990 fn test_align_y_to_world_up_when_rotated() {
3991 let mut interp = Interpreter::new();
3993 let ninety_z = Quat::from_axis_angle(Vec3::Z, std::f64::consts::FRAC_PI_2);
3994 interp.add_rule(
3995 "R",
3996 vec![
3997 ShapeOp::Rotate(qexpr(ninety_z)),
3998 ShapeOp::Align {
3999 local_axis: Axis::Y,
4000 target: Vec3::Y,
4001 },
4002 ShapeOp::I("M".to_string()),
4003 ],
4004 );
4005 let model = interp.derive(Scope::unit(), "R").unwrap();
4006 assert_eq!(model.len(), 1);
4007 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
4008 assert!(
4009 (world_y - Vec3::Y).length() < 1e-6,
4010 "expected Y=(0,1,0), got {:?}",
4011 world_y
4012 );
4013 }
4014
4015 #[test]
4016 fn test_align_already_aligned_is_noop() {
4017 let mut interp = Interpreter::new();
4018 interp.add_rule(
4019 "R",
4020 vec![
4021 ShapeOp::Align {
4022 local_axis: Axis::Y,
4023 target: Vec3::Y,
4024 },
4025 ShapeOp::I("M".to_string()),
4026 ],
4027 );
4028 let model = interp.derive(Scope::unit(), "R").unwrap();
4029 let rot = model.terminals[0].scope.rotation;
4030 assert!((rot.length_squared() - 1.0).abs() < 1e-9);
4031 let world_y = rot * Vec3::Y;
4033 assert!((world_y - Vec3::Y).length() < 1e-6);
4034 }
4035
4036 #[test]
4037 fn test_align_zero_target_rejected() {
4038 let mut interp = Interpreter::new();
4039 interp.add_rule(
4040 "R",
4041 vec![
4042 ShapeOp::Align {
4043 local_axis: Axis::Y,
4044 target: Vec3::ZERO,
4045 },
4046 ShapeOp::I("M".to_string()),
4047 ],
4048 );
4049 assert!(matches!(
4050 interp.derive(Scope::unit(), "R"),
4051 Err(ShapeError::InvalidAlignTarget)
4052 ));
4053 }
4054
4055 #[test]
4058 fn test_offset_inset_produces_inside_and_border() {
4059 let mut interp = Interpreter::new();
4060 interp.add_rule(
4061 "R",
4062 vec![ShapeOp::Offset {
4063 distance: Expr::lit(-0.5),
4064 cases: vec![
4065 crate::ops::OffsetCase {
4066 selector: crate::ops::OffsetSelector::Inside,
4067 rule: "Glass".into(),
4068 },
4069 crate::ops::OffsetCase {
4070 selector: crate::ops::OffsetSelector::Border,
4071 rule: "Frame".into(),
4072 },
4073 ],
4074 }],
4075 );
4076 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
4078 let model = interp.derive(scope, "R").unwrap();
4079 assert_eq!(model.len(), 5);
4081 let inside = model
4083 .terminals
4084 .iter()
4085 .find(|t| t.mesh_id == "Glass")
4086 .unwrap();
4087 assert!((inside.scope.size.x - 3.0).abs() < 1e-9);
4088 assert!((inside.scope.size.y - 2.0).abs() < 1e-9);
4089 assert!((inside.scope.position - Vec3::new(0.5, 0.5, 0.0)).length() < 1e-9);
4090 }
4091
4092 #[test]
4093 fn test_offset_too_large_rejected() {
4094 let mut interp = Interpreter::new();
4095 interp.add_rule(
4096 "R",
4097 vec![ShapeOp::Offset {
4098 distance: Expr::lit(-2.0), cases: vec![crate::ops::OffsetCase {
4100 selector: crate::ops::OffsetSelector::Inside,
4101 rule: "A".into(),
4102 }],
4103 }],
4104 );
4105 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
4106 assert!(matches!(
4107 interp.derive(scope, "R"),
4108 Err(ShapeError::OffsetTooLarge)
4109 ));
4110 }
4111
4112 #[test]
4113 fn test_offset_positive_distance_is_an_outset() {
4114 let mut interp = Interpreter::new();
4117 interp.add_rule(
4118 "R",
4119 vec![ShapeOp::Offset {
4120 distance: Expr::lit(0.2),
4121 cases: vec![crate::ops::OffsetCase {
4122 selector: crate::ops::OffsetSelector::Inside,
4123 rule: "A".into(),
4124 }],
4125 }],
4126 );
4127 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
4128 let model = interp.derive(scope, "R").unwrap();
4129 assert_eq!(model.len(), 1);
4130 let t = &model.terminals[0];
4131 assert!((t.scope.size.x - 4.4).abs() < 1e-9);
4132 assert!((t.scope.size.y - 3.4).abs() < 1e-9);
4133 assert!((t.scope.position.x - -0.2).abs() < 1e-9);
4134 assert!((t.scope.position.y - -0.2).abs() < 1e-9);
4135 let mut i2 = Interpreter::new();
4137 i2.add_rule(
4138 "R",
4139 vec![ShapeOp::Offset {
4140 distance: Expr::lit(0.0),
4141 cases: vec![crate::ops::OffsetCase {
4142 selector: crate::ops::OffsetSelector::Inside,
4143 rule: "A".into(),
4144 }],
4145 }],
4146 );
4147 assert!(matches!(
4148 i2.derive(Scope::unit(), "R"),
4149 Err(ShapeError::InvalidNumericValue)
4150 ));
4151 }
4152
4153 #[test]
4156 fn test_roof_shed_produces_one_slope() {
4157 let mut interp = Interpreter::new();
4158 interp.add_rule(
4159 "R",
4160 vec![ShapeOp::Roof {
4161 spec: RoofConfig::new(RoofType::Shed, 30.0).into(),
4162 cases: vec![crate::ops::RoofCase {
4163 selector: RoofFaceSelector::Slope,
4164 rule: "Tiles".into(),
4165 }],
4166 }],
4167 );
4168 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
4169 let model = interp.derive(scope, "R").unwrap();
4170 assert_eq!(model.len(), 1);
4171 assert_eq!(model.terminals[0].mesh_id, "Tiles");
4172 assert!((model.terminals[0].scope.position.y - 0.0).abs() < 1e-6);
4174 }
4175
4176 #[test]
4177 fn test_roof_gable_produces_four_panels() {
4178 let mut interp = Interpreter::new();
4179 interp.add_rule(
4180 "R",
4181 vec![ShapeOp::Roof {
4182 spec: RoofConfig::new(RoofType::Gable, 30.0).into(),
4183 cases: vec![
4184 crate::ops::RoofCase {
4185 selector: RoofFaceSelector::Slope,
4186 rule: "Tiles".into(),
4187 },
4188 crate::ops::RoofCase {
4189 selector: RoofFaceSelector::GableEnd,
4190 rule: "Bricks".into(),
4191 },
4192 ],
4193 }],
4194 );
4195 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
4196 let model = interp.derive(scope, "R").unwrap();
4197 assert_eq!(model.len(), 4);
4199 let tiles: Vec<_> = model
4200 .terminals
4201 .iter()
4202 .filter(|t| t.mesh_id == "Tiles")
4203 .collect();
4204 let bricks: Vec<_> = model
4205 .terminals
4206 .iter()
4207 .filter(|t| t.mesh_id == "Bricks")
4208 .collect();
4209 assert_eq!(tiles.len(), 2);
4210 assert_eq!(bricks.len(), 2);
4211 }
4212
4213 #[test]
4214 fn test_roof_hip_produces_four_slopes() {
4215 let mut interp = Interpreter::new();
4216 interp.add_rule(
4217 "R",
4218 vec![ShapeOp::Roof {
4219 spec: crate::ops::RoofSpec::from({
4220 let mut c = RoofConfig::new(RoofType::Hip, 45.0);
4221 c.overhang = 0.3;
4222 c
4223 }),
4224 cases: vec![crate::ops::RoofCase {
4225 selector: RoofFaceSelector::Slope,
4226 rule: "Tiles".into(),
4227 }],
4228 }],
4229 );
4230 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
4231 let model = interp.derive(scope, "R").unwrap();
4232 assert_eq!(model.len(), 4);
4233 }
4234
4235 #[test]
4236 fn test_roof_pyramid_produces_four_tapered_slopes() {
4237 let mut interp = Interpreter::new();
4238 interp.add_rule(
4239 "R",
4240 vec![ShapeOp::Roof {
4241 spec: RoofConfig::new(RoofType::Pyramid, 40.0).into(),
4242 cases: vec![crate::ops::RoofCase {
4243 selector: RoofFaceSelector::Slope,
4244 rule: "Tiles".into(),
4245 }],
4246 }],
4247 );
4248 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(6.0, 3.0, 6.0));
4249 let model = interp.derive(scope, "R").unwrap();
4250 assert_eq!(model.len(), 4);
4251 for t in &model.terminals {
4253 assert!(
4254 matches!(t.face_profile, FaceProfile::Triangle { peak_offset } if (peak_offset - 0.5).abs() < 1e-9),
4255 "expected Triangle{{peak_offset=0.5}}, got {:?}",
4256 t.face_profile
4257 );
4258 }
4259 }
4260
4261 #[test]
4262 fn test_roof_slope_normals_outward() {
4263 let alpha: f64 = 30_f64.to_radians();
4268 let cos_a = alpha.cos();
4269 let sin_a = alpha.sin();
4270 let mut interp = Interpreter::new();
4271 interp.add_rule(
4272 "R",
4273 vec![ShapeOp::Roof {
4274 spec: RoofConfig::new(RoofType::Hip, 30.0).into(),
4275 cases: vec![crate::ops::RoofCase {
4276 selector: RoofFaceSelector::Slope,
4277 rule: "S".into(),
4278 }],
4279 }],
4280 );
4281 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
4282 let model = interp.derive(scope, "R").unwrap();
4283 assert_eq!(model.len(), 4);
4284 let normals: Vec<Vec3> = model
4285 .terminals
4286 .iter()
4287 .map(|t| t.scope.rotation * Vec3::Z)
4288 .collect();
4289 let expected = [
4290 Vec3::new(0.0, cos_a, -sin_a), Vec3::new(0.0, cos_a, sin_a), Vec3::new(-sin_a, cos_a, 0.0), Vec3::new(sin_a, cos_a, 0.0), ];
4295 for exp in &expected {
4296 assert!(
4297 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
4298 "missing outward normal {:?}; got {:?}",
4299 exp,
4300 normals
4301 );
4302 }
4303 for n in &normals {
4305 assert!(n.y > 0.0, "normal pointing downward: {:?}", n);
4306 }
4307 }
4308
4309 #[test]
4310 fn test_align_antiparallel_fallback_no_nan() {
4311 let flip_y = Quat::from_axis_angle(Vec3::Z, PI);
4315 let mut interp = Interpreter::new();
4316 interp.add_rule(
4317 "R",
4318 vec![
4319 ShapeOp::Rotate(qexpr(flip_y)),
4320 ShapeOp::Align {
4321 local_axis: Axis::Y,
4322 target: Vec3::Y,
4323 },
4324 ShapeOp::I("M".to_string()),
4325 ],
4326 );
4327 let model = interp.derive(Scope::unit(), "R").unwrap();
4328 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
4329 assert!(
4330 (world_y - Vec3::Y).length() < 1e-6,
4331 "anti-parallel Align should point Y to world up, got {:?}",
4332 world_y
4333 );
4334 let r = model.terminals[0].scope.rotation;
4336 assert!(
4337 (r.length_squared() - 1.0).abs() < 1e-9,
4338 "rotation not unit: length_sq={}",
4339 r.length_squared()
4340 );
4341 }
4342
4343 #[test]
4344 fn test_roof_invalid_angle_rejected() {
4345 let mut interp = Interpreter::new();
4346 interp.add_rule(
4347 "R",
4348 vec![ShapeOp::Roof {
4349 spec: RoofConfig::new(RoofType::Shed, 0.0).into(),
4350 cases: vec![],
4351 }],
4352 );
4353 assert!(matches!(
4354 interp.derive(Scope::unit(), "R"),
4355 Err(ShapeError::InvalidRoofAngle(_))
4356 ));
4357 }
4358}