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 let top_w = (eave_w - 2.0 * clip_run).max(0.0);
1309
1310 let slope_len = (depth / 2.0 + o) / cos_a;
1311
1312 let slope_profile = if top_w > 1e-9 {
1313 FaceProfile::Trapezoid {
1314 top_width: top_w / eave_w,
1315 offset_x: clip_run / eave_w,
1316 }
1317 } else {
1318 FaceProfile::Triangle { peak_offset: 0.5 }
1319 };
1320
1321 let true_h = (depth / 2.0) * tan_a;
1322 let wall_h = (true_h - clip_run * tan_a).max(0.0);
1323 let wall_profile = if clip_run > 1e-9 {
1324 FaceProfile::Trapezoid {
1325 top_width: (2.0 * clip_run) / depth,
1326 offset_x: (depth / 2.0 - clip_run) / depth,
1327 }
1328 } else {
1329 FaceProfile::Triangle { peak_offset: 0.5 }
1330 };
1331
1332 let hip_base_w = 2.0 * clip_run + 2.0 * o;
1333 let hip_slope_len = (clip_run + o) / cos_a;
1334 let hip_profile = FaceProfile::Triangle { peak_offset: 0.5 };
1335
1336 if orient_z {
1337 let left_hip_origin = Vec3::new(-o, wall_h - o * tan_a, sz / 2.0 - clip_run - o);
1338 let right_hip_origin =
1339 Vec3::new(sx + o, wall_h - o * tan_a, sz / 2.0 + clip_run + o);
1340 vec![
1341 (
1342 Vec3::new(sx + o, y_anchor, -o),
1343 Vec3::new(eave_w, slope_len, 0.0),
1344 front_rot,
1345 RoofFaceSelector::Slope,
1346 slope_profile.clone(),
1347 ),
1348 (
1349 Vec3::new(-o, y_anchor, sz + o),
1350 Vec3::new(eave_w, slope_len, 0.0),
1351 back_rot,
1352 RoofFaceSelector::Slope,
1353 slope_profile,
1354 ),
1355 (
1356 Vec3::new(0.0, 0.0, 0.0),
1357 Vec3::new(sz, wall_h, 0.0),
1358 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1359 RoofFaceSelector::GableEnd,
1360 wall_profile.clone(),
1361 ),
1362 (
1363 Vec3::new(sx, 0.0, sz),
1364 Vec3::new(sz, wall_h, 0.0),
1365 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1366 RoofFaceSelector::GableEnd,
1367 wall_profile,
1368 ),
1369 (
1370 left_hip_origin,
1371 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1372 left_rot,
1373 RoofFaceSelector::HipEnd,
1374 hip_profile.clone(),
1375 ),
1376 (
1377 right_hip_origin,
1378 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1379 right_rot,
1380 RoofFaceSelector::HipEnd,
1381 hip_profile,
1382 ),
1383 ]
1384 } else {
1385 let front_hip_origin = Vec3::new(sx / 2.0 + clip_run + o, wall_h - o * tan_a, -o);
1386 let back_hip_origin =
1387 Vec3::new(sx / 2.0 - clip_run - o, wall_h - o * tan_a, sz + o);
1388 vec![
1389 (
1390 Vec3::new(-o, y_anchor, -o),
1391 Vec3::new(eave_w, slope_len, 0.0),
1392 left_rot,
1393 RoofFaceSelector::Slope,
1394 slope_profile.clone(),
1395 ),
1396 (
1397 Vec3::new(sx + o, y_anchor, sz + o),
1398 Vec3::new(eave_w, slope_len, 0.0),
1399 right_rot,
1400 RoofFaceSelector::Slope,
1401 slope_profile,
1402 ),
1403 (
1404 Vec3::new(sx, 0.0, 0.0),
1405 Vec3::new(sx, wall_h, 0.0),
1406 Quat::from_axis_angle(Vec3::Y, PI),
1407 RoofFaceSelector::GableEnd,
1408 wall_profile.clone(),
1409 ),
1410 (
1411 Vec3::new(0.0, 0.0, sz),
1412 Vec3::new(sx, wall_h, 0.0),
1413 Quat::IDENTITY,
1414 RoofFaceSelector::GableEnd,
1415 wall_profile,
1416 ),
1417 (
1418 front_hip_origin,
1419 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1420 front_rot,
1421 RoofFaceSelector::HipEnd,
1422 hip_profile.clone(),
1423 ),
1424 (
1425 back_hip_origin,
1426 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1427 back_rot,
1428 RoofFaceSelector::HipEnd,
1429 hip_profile,
1430 ),
1431 ]
1432 }
1433 }
1434
1435 RoofType::DutchGable => {
1439 let tier = config.tier_height_or(0.7).clamp(0.01, 0.99);
1440 let orient_z = ridge_x;
1441 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1442
1443 let max_run = width.min(depth) / 2.0 + o;
1444 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
1445
1446 if !break_run.is_finite() {
1447 return Err(ShapeError::InvalidNumericValue);
1448 }
1449
1450 let y_break = y_anchor + break_run * tan_a;
1451 let eave_w = width + 2.0 * o;
1452 let eave_d = depth + 2.0 * o;
1453
1454 let top_w = (eave_w - 2.0 * break_run).max(0.0);
1455 let top_d = (eave_d - 2.0 * break_run).max(0.0);
1456
1457 let lower_slope_len = break_run / cos_a;
1458
1459 let fb_profile = if top_w > 1e-9 {
1460 FaceProfile::Trapezoid {
1461 top_width: top_w / eave_w,
1462 offset_x: break_run / eave_w,
1463 }
1464 } else {
1465 FaceProfile::Triangle { peak_offset: 0.5 }
1466 };
1467
1468 let lr_profile = if top_d > 1e-9 {
1469 FaceProfile::Trapezoid {
1470 top_width: top_d / eave_d,
1471 offset_x: break_run / eave_d,
1472 }
1473 } else {
1474 FaceProfile::Triangle { peak_offset: 0.5 }
1475 };
1476
1477 let upper_run = (depth / 2.0 + o) - break_run;
1478 let upper_slope_len = upper_run / cos_a;
1479 let upper_h = upper_run * tan_a;
1480
1481 if orient_z {
1482 vec![
1483 (
1485 Vec3::new(sx + o, y_anchor, -o),
1486 Vec3::new(eave_w, lower_slope_len, 0.0),
1487 front_rot,
1488 RoofFaceSelector::Slope,
1489 fb_profile.clone(),
1490 ),
1491 (
1492 Vec3::new(-o, y_anchor, sz + o),
1493 Vec3::new(eave_w, lower_slope_len, 0.0),
1494 back_rot,
1495 RoofFaceSelector::Slope,
1496 fb_profile,
1497 ),
1498 (
1500 Vec3::new(-o, y_anchor, -o),
1501 Vec3::new(eave_d, lower_slope_len, 0.0),
1502 left_rot,
1503 RoofFaceSelector::Slope,
1504 lr_profile.clone(),
1505 ),
1506 (
1507 Vec3::new(sx + o, y_anchor, sz + o),
1508 Vec3::new(eave_d, lower_slope_len, 0.0),
1509 right_rot,
1510 RoofFaceSelector::Slope,
1511 lr_profile,
1512 ),
1513 (
1515 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1516 Vec3::new(top_w, upper_slope_len, 0.0),
1517 front_rot,
1518 RoofFaceSelector::Slope,
1519 FaceProfile::Rectangle,
1520 ),
1521 (
1522 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1523 Vec3::new(top_w, upper_slope_len, 0.0),
1524 back_rot,
1525 RoofFaceSelector::Slope,
1526 FaceProfile::Rectangle,
1527 ),
1528 (
1530 Vec3::new(-o + break_run, y_break, -o + break_run),
1531 Vec3::new(top_d, upper_h, 0.0),
1532 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1533 RoofFaceSelector::GableEnd,
1534 FaceProfile::Triangle { peak_offset: 0.5 },
1535 ),
1536 (
1537 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1538 Vec3::new(top_d, upper_h, 0.0),
1539 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1540 RoofFaceSelector::GableEnd,
1541 FaceProfile::Triangle { peak_offset: 0.5 },
1542 ),
1543 ]
1544 } else {
1545 vec![
1546 (
1548 Vec3::new(-o, y_anchor, -o),
1549 Vec3::new(eave_w, lower_slope_len, 0.0),
1550 left_rot,
1551 RoofFaceSelector::Slope,
1552 fb_profile.clone(),
1553 ),
1554 (
1555 Vec3::new(sx + o, y_anchor, sz + o),
1556 Vec3::new(eave_w, lower_slope_len, 0.0),
1557 right_rot,
1558 RoofFaceSelector::Slope,
1559 fb_profile,
1560 ),
1561 (
1563 Vec3::new(sx + o, y_anchor, -o),
1564 Vec3::new(eave_d, lower_slope_len, 0.0),
1565 front_rot,
1566 RoofFaceSelector::Slope,
1567 lr_profile.clone(),
1568 ),
1569 (
1570 Vec3::new(-o, y_anchor, sz + o),
1571 Vec3::new(eave_d, lower_slope_len, 0.0),
1572 back_rot,
1573 RoofFaceSelector::Slope,
1574 lr_profile,
1575 ),
1576 (
1578 Vec3::new(-o + break_run, y_break, -o + break_run),
1579 Vec3::new(top_w, upper_slope_len, 0.0),
1580 left_rot,
1581 RoofFaceSelector::Slope,
1582 FaceProfile::Rectangle,
1583 ),
1584 (
1585 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1586 Vec3::new(top_w, upper_slope_len, 0.0),
1587 right_rot,
1588 RoofFaceSelector::Slope,
1589 FaceProfile::Rectangle,
1590 ),
1591 (
1593 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1594 Vec3::new(top_d, upper_h, 0.0),
1595 Quat::from_axis_angle(Vec3::Y, PI),
1596 RoofFaceSelector::GableEnd,
1597 FaceProfile::Triangle { peak_offset: 0.5 },
1598 ),
1599 (
1600 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1601 Vec3::new(top_d, upper_h, 0.0),
1602 Quat::IDENTITY,
1603 RoofFaceSelector::GableEnd,
1604 FaceProfile::Triangle { peak_offset: 0.5 },
1605 ),
1606 ]
1607 }
1608 }
1609 };
1610
1611 if config.fascia_depth.is_finite() && config.fascia_depth > 0.0 {
1615 let fascia_depth = config.fascia_depth;
1616 let mut fascia_panels: Vec<Panel> = Vec::new();
1617 for (local_off, face_size, rot_delta, selector, _profile) in &panels {
1618 let eave_bearing = matches!(
1619 selector,
1620 RoofFaceSelector::Slope
1621 | RoofFaceSelector::LowerSlope
1622 | RoofFaceSelector::OuterSlope
1623 );
1624 if !eave_bearing {
1625 continue;
1626 }
1627 if (local_off.y - y_anchor).abs() > 1e-6 {
1628 continue;
1629 }
1630 let Some(wall_rot) = slope_to_wall_rot(*rot_delta) else {
1631 continue;
1632 };
1633 fascia_panels.push((
1634 Vec3::new(local_off.x, local_off.y - fascia_depth, local_off.z),
1635 Vec3::new(face_size.x, fascia_depth, 0.0),
1636 wall_rot,
1637 RoofFaceSelector::Fascia,
1638 FaceProfile::Rectangle,
1639 ));
1640 }
1641 panels.extend(fascia_panels);
1642 }
1643
1644 if queue.len() + panels.len() > MAX_QUEUE {
1645 return Err(ShapeError::CapacityOverflow);
1646 }
1647 let mut child_ordinal: u64 = 0;
1648 for (local_off, face_size, rot_delta, selector, profile) in panels {
1649 let Some(rule) = find_roof_rule(selector, cases) else {
1650 continue;
1651 };
1652 if face_size.x < 1e-9 || face_size.y < 1e-9 {
1654 continue;
1655 }
1656 let face_pos = scope.position + scope.rotation * local_off;
1657 let face_rot = (scope.rotation * rot_delta).normalize();
1658 let face_scope = Scope::new(face_pos, face_rot, face_size);
1659 face_scope.validate()?;
1660 if model.len() + queue.len() >= max_terminals {
1661 return Err(ShapeError::CapacityOverflow);
1662 }
1663 queue.push_back(WorkItem {
1664 scope: face_scope,
1665 rule: rule.name.clone(),
1666 args: rule.args.clone(),
1667 depth: depth + 1,
1668 taper: 0.0,
1669 face_profile_override: Some(profile),
1670 material: material.clone(),
1671 split_i,
1672 split_n,
1673 rng_state: fork_state(rng_state, child_ordinal),
1674 label: label.clone(),
1675 });
1676 child_ordinal += 1;
1677 }
1678
1679 Ok(())
1680}
1681
1682#[allow(clippy::too_many_arguments)]
1690fn select_variant<'a>(
1691 variants: &'a [RuleVariant],
1692 scope: &Scope,
1693 split_i: usize,
1694 split_n: usize,
1695 depth: usize,
1696 params: &[(String, f64)],
1697 globals: &HashMap<String, f64>,
1698 rng: &mut Pcg64,
1699) -> Result<&'a [ShapeOp], ShapeError> {
1700 if variants.is_empty() {
1701 return Ok(&[]);
1702 }
1703 let guarded = variants
1704 .iter()
1705 .any(|v| matches!(v.selector, VariantSelector::When(_) | VariantSelector::Else));
1706 if guarded {
1707 for v in variants {
1708 match &v.selector {
1709 VariantSelector::When(cond) => {
1710 let hit =
1711 eval_expr(cond, scope, split_i, split_n, depth, params, globals, rng)?;
1712 if hit != 0.0 {
1713 return Ok(&v.ops);
1714 }
1715 }
1716 VariantSelector::Else => return Ok(&v.ops),
1717 VariantSelector::Weight(_) => {
1718 return Err(ShapeError::ParseError(
1719 "rule mixes weighted and guarded variants".to_string(),
1720 ));
1721 }
1722 }
1723 }
1724 return Ok(&[]);
1725 }
1726 if variants.len() == 1 {
1727 return Ok(&variants[0].ops);
1728 }
1729 let total: f64 = variants
1730 .iter()
1731 .map(|v| match v.selector {
1732 VariantSelector::Weight(w) => w,
1733 _ => 0.0,
1734 })
1735 .sum();
1736 use rand::Rng;
1737 let r: f64 = rng.random::<f64>() * total;
1738 let mut acc = 0.0;
1739 for v in variants {
1740 if let VariantSelector::Weight(w) = v.selector {
1741 acc += w;
1742 if r < acc {
1743 return Ok(&v.ops);
1744 }
1745 }
1746 }
1747 Ok(&variants.last().unwrap().ops)
1748}
1749
1750#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1762pub struct RuleDef {
1763 pub params: Vec<String>,
1766 pub variants: Vec<RuleVariant>,
1767}
1768
1769pub struct Interpreter {
1770 rules: HashMap<String, RuleDef>,
1771 attrs: HashMap<String, f64>,
1774 consts: HashMap<String, f64>,
1776 attr_defaults: HashMap<String, f64>,
1779 styles: HashMap<String, Vec<(String, f64)>>,
1782 active_style: Option<String>,
1784 pub max_depth: usize,
1787 pub max_terminals: usize,
1791 pub seed: u64,
1795}
1796
1797impl Default for Interpreter {
1798 fn default() -> Self {
1799 Self::new()
1800 }
1801}
1802
1803impl Interpreter {
1804 pub fn new() -> Self {
1805 Self {
1806 rules: HashMap::new(),
1807 attrs: HashMap::new(),
1808 consts: HashMap::new(),
1809 attr_defaults: HashMap::new(),
1810 styles: HashMap::new(),
1811 active_style: None,
1812 max_depth: MAX_DEPTH,
1813 max_terminals: MAX_TERMINALS,
1814 seed: 0,
1815 }
1816 }
1817
1818 pub fn add_statement(&mut self, stmt: crate::grammar::Statement) -> Result<(), ShapeError> {
1830 use crate::grammar::Statement;
1831 match stmt {
1832 Statement::Rule(rule) => self.add_grammar_rule(rule),
1833 Statement::Attr { name, value } => {
1834 self.attr_defaults.insert(name, value);
1835 Ok(())
1836 }
1837 Statement::Const { name, value } => {
1838 self.consts.insert(name, value);
1839 Ok(())
1840 }
1841 Statement::Style {
1842 name,
1843 extends,
1844 overrides,
1845 } => {
1846 let mut flat = match extends {
1847 Some(base) => self.styles.get(&base).cloned().ok_or_else(|| {
1848 ShapeError::ParseError(format!(
1849 "style `{name}` extends unknown style `{base}`"
1850 ))
1851 })?,
1852 None => Vec::new(),
1853 };
1854 flat.extend(overrides);
1855 self.styles.insert(name, flat);
1856 Ok(())
1857 }
1858 }
1859 }
1860
1861 pub fn set_style(&mut self, name: impl Into<String>) -> Result<(), ShapeError> {
1864 let name = name.into();
1865 if !self.styles.contains_key(&name) {
1866 return Err(ShapeError::ParseError(format!("unknown style `{name}`")));
1867 }
1868 self.active_style = Some(name);
1869 Ok(())
1870 }
1871
1872 pub fn clear_style(&mut self) {
1874 self.active_style = None;
1875 }
1876
1877 fn effective_globals(&self) -> HashMap<String, f64> {
1881 let mut g = self.consts.clone();
1882 for (k, v) in &self.attr_defaults {
1883 g.insert(k.clone(), *v);
1884 }
1885 if let Some(style) = &self.active_style
1886 && let Some(overrides) = self.styles.get(style)
1887 {
1888 for (k, v) in overrides {
1889 g.insert(k.clone(), *v);
1890 }
1891 }
1892 for (k, v) in &self.attrs {
1893 g.insert(k.clone(), *v);
1894 }
1895 g
1896 }
1897
1898 pub fn rules(&self) -> &HashMap<String, RuleDef> {
1900 &self.rules
1901 }
1902
1903 pub fn set_attr(&mut self, name: impl Into<String>, value: f64) {
1907 self.attrs.insert(name.into(), value);
1908 }
1909
1910 pub fn attrs(&self) -> &HashMap<String, f64> {
1912 &self.attrs
1913 }
1914
1915 pub fn set_variants(&mut self, name: impl Into<String>, variants: Vec<RuleVariant>) {
1921 let name = name.into();
1922 let params = self
1923 .rules
1924 .get(&name)
1925 .map(|def| def.params.clone())
1926 .unwrap_or_default();
1927 self.rules.insert(name, RuleDef { params, variants });
1928 }
1929
1930 pub fn add_rule(&mut self, name: impl Into<String>, ops: Vec<ShapeOp>) {
1932 self.rules.insert(
1933 name.into(),
1934 RuleDef {
1935 params: Vec::new(),
1936 variants: vec![RuleVariant::weighted(1.0, ops)],
1937 },
1938 );
1939 }
1940
1941 pub fn add_grammar_rule(
1945 &mut self,
1946 rule: crate::grammar::GrammarRule,
1947 ) -> Result<(), ShapeError> {
1948 if rule.name == "NIL" {
1949 return Err(ShapeError::ParseError(
1950 "`NIL` is reserved and cannot be defined as a rule".to_string(),
1951 ));
1952 }
1953 self.add_rule_variants(rule.name, rule.params, rule.variants)
1954 }
1955
1956 pub fn add_rule_variants(
1959 &mut self,
1960 name: impl Into<String>,
1961 params: Vec<String>,
1962 variants: Vec<RuleVariant>,
1963 ) -> Result<(), ShapeError> {
1964 if params.len() > crate::grammar::MAX_RULE_ARGS {
1965 return Err(ShapeError::ParseError(format!(
1966 "rule declares {} parameters (max {})",
1967 params.len(),
1968 crate::grammar::MAX_RULE_ARGS
1969 )));
1970 }
1971 for (i, p) in params.iter().enumerate() {
1972 if params[..i].contains(p) {
1973 return Err(ShapeError::ParseError(format!(
1974 "duplicate rule parameter name: {p}"
1975 )));
1976 }
1977 }
1978 let n_weight = variants
1979 .iter()
1980 .filter(|v| matches!(v.selector, VariantSelector::Weight(_)))
1981 .count();
1982 let n_guard = variants.len() - n_weight;
1983 if n_weight > 0 && n_guard > 0 {
1984 return Err(ShapeError::ParseError(
1985 "rule mixes weighted and guarded variants".to_string(),
1986 ));
1987 }
1988 for (i, v) in variants.iter().enumerate() {
1989 match &v.selector {
1990 VariantSelector::Weight(w) => {
1991 if !w.is_finite() || *w < 0.0 {
1992 return Err(ShapeError::InvalidNumericValue);
1993 }
1994 }
1995 VariantSelector::Else => {
1996 if i + 1 != variants.len() {
1997 return Err(ShapeError::ParseError(
1998 "`else:` must be the last variant".to_string(),
1999 ));
2000 }
2001 }
2002 VariantSelector::When(_) => {}
2003 }
2004 }
2005 self.rules.insert(name.into(), RuleDef { params, variants });
2006 Ok(())
2007 }
2008
2009 pub fn add_rule_def(
2015 &mut self,
2016 name: impl Into<String>,
2017 params: Vec<String>,
2018 variants: Vec<(f64, Vec<ShapeOp>)>,
2019 ) -> Result<(), ShapeError> {
2020 if params.len() > crate::grammar::MAX_RULE_ARGS {
2021 return Err(ShapeError::ParseError(format!(
2022 "rule declares {} parameters (max {})",
2023 params.len(),
2024 crate::grammar::MAX_RULE_ARGS
2025 )));
2026 }
2027 for (i, p) in params.iter().enumerate() {
2028 if params[..i].contains(p) {
2029 return Err(ShapeError::ParseError(format!(
2030 "duplicate rule parameter name: {p}"
2031 )));
2032 }
2033 }
2034 for (weight, _) in &variants {
2035 if !weight.is_finite() || *weight < 0.0 {
2036 return Err(ShapeError::InvalidNumericValue);
2037 }
2038 }
2039 let variants = variants
2040 .into_iter()
2041 .map(|(weight, ops)| RuleVariant::weighted(weight, ops))
2042 .collect();
2043 self.rules.insert(name.into(), RuleDef { params, variants });
2044 Ok(())
2045 }
2046
2047 pub fn add_weighted_rules(
2054 &mut self,
2055 name: impl Into<String>,
2056 variants: Vec<(f64, Vec<ShapeOp>)>,
2057 ) -> Result<(), ShapeError> {
2058 for (weight, _) in &variants {
2059 if !weight.is_finite() || *weight < 0.0 {
2060 return Err(ShapeError::InvalidNumericValue);
2061 }
2062 }
2063 let wvs = variants
2064 .into_iter()
2065 .map(|(weight, ops)| RuleVariant::weighted(weight, ops))
2066 .collect();
2067 self.rules.insert(
2068 name.into(),
2069 RuleDef {
2070 params: Vec::new(),
2071 variants: wvs,
2072 },
2073 );
2074 Ok(())
2075 }
2076
2077 pub fn has_rule(&self, name: &str) -> bool {
2079 self.rules.contains_key(name)
2080 }
2081
2082 pub fn derive(
2089 &self,
2090 root_scope: Scope,
2091 root_rule: impl Into<String>,
2092 ) -> Result<ShapeModel, ShapeError> {
2093 root_scope.validate()?;
2094
2095 let mut model = ShapeModel::new();
2096 let mut queue: VecDeque<WorkItem> = VecDeque::new();
2097 let globals = self.effective_globals();
2098
2099 queue.push_back(WorkItem {
2100 scope: root_scope,
2101 rule: root_rule.into(),
2102 args: Vec::new(),
2103 depth: 0,
2104 taper: 0.0,
2105 face_profile_override: None,
2106 material: None,
2107 split_i: 0,
2108 split_n: 1,
2109 rng_state: splitmix64(self.seed),
2110 label: None,
2111 });
2112
2113 while let Some(item) = queue.pop_front() {
2114 if queue.len() > MAX_QUEUE {
2115 return Err(ShapeError::CapacityOverflow);
2116 }
2117 if item.depth > self.max_depth {
2118 return Err(ShapeError::DepthLimitExceeded(self.max_depth));
2119 }
2120
2121 if item.rule == "NIL" {
2125 continue;
2126 }
2127
2128 let mut item_rng = Pcg64::seed_from_u64(item.rng_state);
2131 let def = match self.rules.get(&item.rule) {
2132 Some(def) => def,
2133 None => {
2134 if model.len() >= self.max_terminals {
2136 return Err(ShapeError::CapacityOverflow);
2137 }
2138 let profile = item
2139 .face_profile_override
2140 .unwrap_or_else(|| taper_to_profile(item.taper));
2141 let mut terminal =
2142 Terminal::new_profiled(item.scope, &item.rule, profile, item.material);
2143 terminal.label = item.label;
2144 model.push(terminal);
2145 continue;
2146 }
2147 };
2148
2149 if def.params.len() != item.args.len() {
2151 return Err(ShapeError::ArityMismatch(format!(
2152 "rule `{}` expects {} argument(s), got {}",
2153 item.rule,
2154 def.params.len(),
2155 item.args.len()
2156 )));
2157 }
2158 let params: Vec<(String, f64)> = def
2159 .params
2160 .iter()
2161 .cloned()
2162 .zip(item.args.iter().copied())
2163 .collect();
2164
2165 let ops = select_variant(
2166 &def.variants,
2167 &item.scope,
2168 item.split_i,
2169 item.split_n,
2170 item.depth,
2171 ¶ms,
2172 &globals,
2173 &mut item_rng,
2174 )?;
2175
2176 self.apply_ops(
2177 item.scope,
2178 item.taper,
2179 item.face_profile_override,
2180 item.material,
2181 item.label.clone(),
2182 ops,
2183 item.depth,
2184 ¶ms,
2185 item.split_i,
2186 item.split_n,
2187 item.rng_state,
2188 &globals,
2189 &mut item_rng,
2190 &mut queue,
2191 &mut model,
2192 )?;
2193 }
2194
2195 Ok(model)
2196 }
2197
2198 #[allow(clippy::too_many_arguments)]
2204 #[allow(unused_assignments)]
2207 fn apply_ops(
2208 &self,
2209 initial_scope: Scope,
2210 initial_taper: f64,
2211 initial_face_profile: Option<FaceProfile>,
2212 initial_material: Option<Material>,
2213 initial_label: Option<String>,
2214 ops: &[ShapeOp],
2215 depth: usize,
2216 params: &[(String, f64)],
2217 split_i: usize,
2218 split_n: usize,
2219 rng_state: u64,
2220 globals: &HashMap<String, f64>,
2221 rng: &mut Pcg64,
2222 queue: &mut VecDeque<WorkItem>,
2223 model: &mut ShapeModel,
2224 ) -> Result<(), ShapeError> {
2225 let mut scope = initial_scope;
2226 let entry_scope = initial_scope;
2228 let mut taper = initial_taper;
2229 let mut face_profile = initial_face_profile;
2230 let mut material = initial_material;
2231 let mut label = initial_label;
2232
2233 macro_rules! ev {
2237 ($e:expr) => {
2238 eval_expr($e, &scope, split_i, split_n, depth, params, globals, rng)?
2239 };
2240 }
2241 let mut child_ordinal: u64 = 0;
2244 macro_rules! fork {
2245 () => {{
2246 let s = fork_state(rng_state, child_ordinal);
2247 child_ordinal += 1;
2248 s
2249 }};
2250 }
2251 macro_rules! ev_args {
2253 ($call:expr) => {{
2254 let mut argv = Vec::with_capacity($call.args.len());
2255 for a in &$call.args {
2256 argv.push(ev!(a));
2257 }
2258 argv
2259 }};
2260 }
2261
2262 for op in ops {
2263 match op {
2264 ShapeOp::Extrude(h) => {
2266 let h = ev!(h);
2267 if h <= 0.0 {
2268 return Err(ShapeError::InvalidNumericValue);
2269 }
2270 if scope.size.z.abs() < 1e-9 && scope.size.y.abs() > 1e-9 {
2276 scope.size.z = h;
2277 } else {
2278 scope.size.y = h;
2279 }
2280 }
2281
2282 ShapeOp::Taper(amount) => {
2283 taper = ev!(amount).clamp(0.0, 1.0);
2284 }
2285
2286 ShapeOp::Rotate([w, x, y, z]) => {
2287 let (w, x, y, z) = (ev!(w), ev!(x), ev!(y), ev!(z));
2288 let len_sq = w * w + x * x + y * y + z * z;
2292 if !len_sq.is_finite() || len_sq < 1e-12 {
2293 return Err(ShapeError::InvalidNumericValue);
2294 }
2295 let q = Quat::from_xyzw(x, y, z, w);
2296 scope.rotation = (scope.rotation * q.normalize()).normalize();
2297 }
2298
2299 ShapeOp::Translate([x, y, z]) => {
2300 let v = Vec3::new(ev!(x), ev!(y), ev!(z));
2301 scope.position += scope.rotation * v;
2302 if !scope.position.is_finite() {
2305 return Err(ShapeError::InvalidNumericValue);
2306 }
2307 }
2308
2309 ShapeOp::Scale([x, y, z]) => {
2310 let v = Vec3::new(ev!(x), ev!(y), ev!(z));
2311 if v.x <= 0.0 || v.y <= 0.0 || v.z <= 0.0 {
2312 return Err(ShapeError::InvalidNumericValue);
2313 }
2314 scope.size *= v;
2315 if !scope.size.is_finite() {
2319 return Err(ShapeError::InvalidNumericValue);
2320 }
2321 }
2322
2323 ShapeOp::Mat(mat) => {
2324 material = Some(mat.clone());
2325 }
2326
2327 ShapeOp::Label(name) => {
2328 label = Some(name.clone());
2329 }
2330
2331 ShapeOp::Size([x, y, z]) => {
2332 let v = Vec3::new(ev!(x), ev!(y), ev!(z));
2333 if v.x < 0.0 || v.y < 0.0 || v.z < 0.0 {
2334 return Err(ShapeError::InvalidNumericValue);
2335 }
2336 scope.size = v;
2337 }
2338
2339 ShapeOp::Center { x, y, z } => {
2340 let mut local = entry_scope
2342 .rotation
2343 .inverse()
2344 .mul_vec3(scope.position - entry_scope.position);
2345 if *x {
2346 local.x = (entry_scope.size.x - scope.size.x) / 2.0;
2347 }
2348 if *y {
2349 local.y = (entry_scope.size.y - scope.size.y) / 2.0;
2350 }
2351 if *z {
2352 local.z = (entry_scope.size.z - scope.size.z) / 2.0;
2353 }
2354 scope.position = entry_scope.position + entry_scope.rotation * local;
2355 if !scope.position.is_finite() {
2356 return Err(ShapeError::InvalidNumericValue);
2357 }
2358 }
2359
2360 ShapeOp::Mirror => {
2361 if let Some(profile) = &mut face_profile {
2362 *profile = mirror_profile(profile);
2363 }
2364 }
2365
2366 ShapeOp::ShapeL { front, side, cases } => {
2368 let d = ev!(front);
2369 let w = ev!(side);
2370 let (sx, sz) = (scope.size.x, scope.size.z);
2371 if d <= 0.0 || w <= 0.0 || d >= sz - 1e-9 || w >= sx - 1e-9 {
2372 return Err(ShapeError::InvalidNumericValue);
2373 }
2374 let parts: [(CarveSelector, Vec3, Vec3); 3] = [
2377 (
2378 CarveSelector::Shape,
2379 Vec3::ZERO,
2380 Vec3::new(sx, scope.size.y, d),
2381 ),
2382 (
2383 CarveSelector::Shape,
2384 Vec3::new(0.0, 0.0, d),
2385 Vec3::new(w, scope.size.y, sz - d),
2386 ),
2387 (
2388 CarveSelector::Remainder,
2389 Vec3::new(w, 0.0, d),
2390 Vec3::new(sx - w, scope.size.y, sz - d),
2391 ),
2392 ];
2393 if queue.len() + parts.len() > MAX_QUEUE {
2394 return Err(ShapeError::CapacityOverflow);
2395 }
2396 for (selector, local_off, size) in parts {
2397 let Some(case) = find_carve_rule(selector, cases) else {
2398 continue;
2399 };
2400 let pos = scope.position + scope.rotation * local_off;
2401 let child = Scope::new(pos, scope.rotation, size);
2402 child.validate()?;
2403 let args = ev_args!(&case.rule);
2404 queue.push_back(WorkItem {
2405 scope: child,
2406 rule: case.rule.name.clone(),
2407 args,
2408 depth: depth + 1,
2409 taper: 0.0,
2410 face_profile_override: None,
2411 material: material.clone(),
2412 split_i,
2413 split_n,
2414 rng_state: fork!(),
2415 label: label.clone(),
2416 });
2417 }
2418 return Ok(());
2419 }
2420
2421 ShapeOp::ShapeU {
2422 front,
2423 left,
2424 right,
2425 cases,
2426 } => {
2427 let d = ev!(front);
2428 let wl = ev!(left);
2429 let wr = ev!(right);
2430 let (sx, sz) = (scope.size.x, scope.size.z);
2431 if d <= 0.0 || wl <= 0.0 || wr <= 0.0 || d >= sz - 1e-9 || wl + wr >= sx - 1e-9
2432 {
2433 return Err(ShapeError::InvalidNumericValue);
2434 }
2435 let parts: [(CarveSelector, Vec3, Vec3); 4] = [
2436 (
2437 CarveSelector::Shape,
2438 Vec3::ZERO,
2439 Vec3::new(sx, scope.size.y, d),
2440 ),
2441 (
2442 CarveSelector::Shape,
2443 Vec3::new(0.0, 0.0, d),
2444 Vec3::new(wl, scope.size.y, sz - d),
2445 ),
2446 (
2447 CarveSelector::Shape,
2448 Vec3::new(sx - wr, 0.0, d),
2449 Vec3::new(wr, scope.size.y, sz - d),
2450 ),
2451 (
2452 CarveSelector::Remainder,
2453 Vec3::new(wl, 0.0, d),
2454 Vec3::new(sx - wl - wr, scope.size.y, sz - d),
2455 ),
2456 ];
2457 if queue.len() + parts.len() > MAX_QUEUE {
2458 return Err(ShapeError::CapacityOverflow);
2459 }
2460 for (selector, local_off, size) in parts {
2461 let Some(case) = find_carve_rule(selector, cases) else {
2462 continue;
2463 };
2464 let pos = scope.position + scope.rotation * local_off;
2465 let child = Scope::new(pos, scope.rotation, size);
2466 child.validate()?;
2467 let args = ev_args!(&case.rule);
2468 queue.push_back(WorkItem {
2469 scope: child,
2470 rule: case.rule.name.clone(),
2471 args,
2472 depth: depth + 1,
2473 taper: 0.0,
2474 face_profile_override: None,
2475 material: material.clone(),
2476 split_i,
2477 split_n,
2478 rng_state: fork!(),
2479 label: label.clone(),
2480 });
2481 }
2482 return Ok(());
2483 }
2484
2485 ShapeOp::Polygon(verts) => {
2486 if verts.len() < 3 {
2487 return Err(ShapeError::InvalidNumericValue);
2488 }
2489 for v in verts {
2490 if !v.is_finite() {
2491 return Err(ShapeError::InvalidNumericValue);
2492 }
2493 }
2494 face_profile = Some(FaceProfile::Polygon(verts.clone()));
2495 }
2496
2497 ShapeOp::RegSnap(label) => {
2499 register_scope_snap_planes(&scope, label, &mut model.snap_planes);
2500 }
2501
2502 ShapeOp::IfClear { rule, label: filt } => {
2508 let occluded = model
2509 .terminals
2510 .iter()
2511 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2512 .any(|t| scope_obb_overlaps_terminal(&scope, t));
2513 if !occluded {
2514 if queue.len() >= MAX_QUEUE {
2515 return Err(ShapeError::CapacityOverflow);
2516 }
2517 let args = ev_args!(rule);
2518 queue.push_back(WorkItem {
2519 scope,
2520 rule: rule.name.clone(),
2521 args,
2522 depth: depth + 1,
2523 taper,
2524 face_profile_override: face_profile.take(),
2525 material: material.clone(),
2526 split_i,
2527 split_n,
2528 rng_state: fork!(),
2529 label: label.clone(),
2530 });
2531 }
2532 return Ok(());
2533 }
2534 ShapeOp::IfOccluded { rule, label: filt } => {
2535 let occluded = model
2536 .terminals
2537 .iter()
2538 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2539 .any(|t| scope_obb_overlaps_terminal(&scope, t));
2540 if occluded {
2541 if queue.len() >= MAX_QUEUE {
2542 return Err(ShapeError::CapacityOverflow);
2543 }
2544 let args = ev_args!(rule);
2545 queue.push_back(WorkItem {
2546 scope,
2547 rule: rule.name.clone(),
2548 args,
2549 depth: depth + 1,
2550 taper,
2551 face_profile_override: face_profile.take(),
2552 material: material.clone(),
2553 split_i,
2554 split_n,
2555 rng_state: fork!(),
2556 label: label.clone(),
2557 });
2558 }
2559 return Ok(());
2560 }
2561
2562 ShapeOp::IfInside { rule, label: filt } => {
2563 let inside = model
2564 .terminals
2565 .iter()
2566 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2567 .any(|t| crate::query::scope_inside_terminal(&scope, t));
2568 if inside {
2569 if queue.len() >= MAX_QUEUE {
2570 return Err(ShapeError::CapacityOverflow);
2571 }
2572 let args = ev_args!(rule);
2573 queue.push_back(WorkItem {
2574 scope,
2575 rule: rule.name.clone(),
2576 args,
2577 depth: depth + 1,
2578 taper,
2579 face_profile_override: face_profile.take(),
2580 material: material.clone(),
2581 split_i,
2582 split_n,
2583 rng_state: fork!(),
2584 label: label.clone(),
2585 });
2586 }
2587 return Ok(());
2588 }
2589
2590 ShapeOp::IfTouches { rule, label: filt } => {
2591 let touches = model
2592 .terminals
2593 .iter()
2594 .filter(|t| filt.is_none() || t.label.as_deref() == filt.as_deref())
2595 .any(|t| crate::query::scope_touches_terminal(&scope, t));
2596 if touches {
2597 if queue.len() >= MAX_QUEUE {
2598 return Err(ShapeError::CapacityOverflow);
2599 }
2600 let args = ev_args!(rule);
2601 queue.push_back(WorkItem {
2602 scope,
2603 rule: rule.name.clone(),
2604 args,
2605 depth: depth + 1,
2606 taper,
2607 face_profile_override: face_profile.take(),
2608 material: material.clone(),
2609 split_i,
2610 split_n,
2611 rng_state: fork!(),
2612 label: label.clone(),
2613 });
2614 }
2615 return Ok(());
2616 }
2617
2618 ShapeOp::Pick { key, choices } => {
2623 if choices.is_empty() {
2624 return Ok(());
2625 }
2626 let mut pick_rng = Pcg64::seed_from_u64(splitmix64(self.seed ^ fnv1a(key)));
2627 use rand::Rng as _;
2628 let total: f64 = choices.iter().map(|(w, _)| w).sum();
2629 let chosen = if total <= 0.0 {
2630 &choices[0].1
2631 } else {
2632 let r = pick_rng.random::<f64>() * total;
2633 let mut acc = 0.0;
2634 let mut sel = &choices[choices.len() - 1].1;
2635 for (w, call) in choices {
2636 acc += w;
2637 if r < acc {
2638 sel = call;
2639 break;
2640 }
2641 }
2642 sel
2643 };
2644 if queue.len() >= MAX_QUEUE {
2645 return Err(ShapeError::CapacityOverflow);
2646 }
2647 let args = ev_args!(chosen);
2648 queue.push_back(WorkItem {
2649 scope,
2650 rule: chosen.name.clone(),
2651 args,
2652 depth: depth + 1,
2653 taper,
2654 face_profile_override: face_profile.take(),
2655 material: material.clone(),
2656 split_i,
2657 split_n,
2658 rng_state: fork!(),
2659 label: label.clone(),
2660 });
2661 return Ok(());
2662 }
2663
2664 ShapeOp::Scatter {
2666 volume,
2667 count,
2668 rule,
2669 } => {
2670 let n_f = ev!(count);
2671 if n_f < 0.0 {
2672 return Err(ShapeError::InvalidNumericValue);
2673 }
2674 let n = (n_f.floor() as usize).min(MAX_SCATTER_POINTS);
2675 if queue.len() + n > MAX_QUEUE {
2676 return Err(ShapeError::CapacityOverflow);
2677 }
2678 use rand::Rng as _;
2679 for i in 0..n {
2680 let px = rng.random::<f64>() * scope.size.x;
2681 let py = if *volume {
2682 rng.random::<f64>() * scope.size.y
2683 } else {
2684 scope.size.y
2685 };
2686 let pz = rng.random::<f64>() * scope.size.z;
2687 let pos = scope.position + scope.rotation * Vec3::new(px, py, pz);
2688 let child = Scope::new(pos, scope.rotation, Vec3::ZERO);
2689 child.validate()?;
2690 let args = ev_args!(rule);
2691 queue.push_back(WorkItem {
2692 scope: child,
2693 rule: rule.name.clone(),
2694 args,
2695 depth: depth + 1,
2696 taper: 0.0,
2697 face_profile_override: None,
2698 material: material.clone(),
2699 split_i: i,
2700 split_n: n,
2701 rng_state: fork!(),
2702 label: label.clone(),
2703 });
2704 }
2705 return Ok(());
2706 }
2707
2708 ShapeOp::Align { local_axis, target } => {
2710 let len_sq = target.length_squared();
2715 if !target.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
2716 return Err(ShapeError::InvalidAlignTarget);
2717 }
2718 let target_norm = target.normalize();
2719 let current = scope.rotation * axis_vec(*local_axis);
2720 let dot = current.dot(target_norm);
2723 let q = if (dot + 1.0).abs() < 1e-9 {
2724 let perp = if current.x.abs() <= current.y.abs()
2730 && current.x.abs() <= current.z.abs()
2731 {
2732 current.cross(Vec3::X).normalize()
2733 } else if current.y.abs() <= current.z.abs() {
2734 current.cross(Vec3::Y).normalize()
2735 } else {
2736 current.cross(Vec3::Z).normalize()
2737 };
2738 Quat::from_axis_angle(perp, PI)
2739 } else {
2740 Quat::from_rotation_arc(current, target_norm)
2741 };
2742 scope.rotation = (q * scope.rotation).normalize();
2743 }
2744
2745 ShapeOp::Offset { distance, cases } => {
2747 let distance = ev!(distance);
2748 if distance == 0.0 {
2752 return Err(ShapeError::InvalidNumericValue);
2753 }
2754 let inset = -distance;
2755 let sx = scope.size.x;
2756 let sy = scope.size.y;
2757 let inside_w = sx - 2.0 * inset;
2758 let inside_h = sy - 2.0 * inset;
2759 if !inside_w.is_finite()
2763 || !inside_h.is_finite()
2764 || inside_w < 0.0
2765 || inside_h < 0.0
2766 {
2767 return Err(ShapeError::OffsetTooLarge);
2768 }
2769 if let Some(rule) = find_offset_rule(OffsetSelector::Inside, cases) {
2770 if queue.len() >= MAX_QUEUE {
2771 return Err(ShapeError::CapacityOverflow);
2772 }
2773 let pos = scope.position + scope.rotation * Vec3::new(inset, inset, 0.0);
2774 let child_scope =
2775 Scope::new(pos, scope.rotation, Vec3::new(inside_w, inside_h, 0.0));
2776 child_scope.validate()?;
2777 let args = ev_args!(rule);
2778 queue.push_back(WorkItem {
2779 scope: child_scope,
2780 rule: rule.name.clone(),
2781 args,
2782 depth: depth + 1,
2783 taper: 0.0,
2784 face_profile_override: None,
2785 material: material.clone(),
2786 split_i,
2787 split_n,
2788 rng_state: fork!(),
2789 label: label.clone(),
2790 });
2791 }
2792 if let Some(rule) = find_offset_rule(OffsetSelector::Border, cases) {
2793 let strips = [
2795 (Vec3::new(0.0, 0.0, 0.0), Vec3::new(sx, inset, 0.0)),
2796 (Vec3::new(0.0, sy - inset, 0.0), Vec3::new(sx, inset, 0.0)),
2797 (
2798 Vec3::new(0.0, inset, 0.0),
2799 Vec3::new(inset, sy - 2.0 * inset, 0.0),
2800 ),
2801 (
2802 Vec3::new(sx - inset, inset, 0.0),
2803 Vec3::new(inset, sy - 2.0 * inset, 0.0),
2804 ),
2805 ];
2806 if queue.len() + strips.len() > MAX_QUEUE {
2807 return Err(ShapeError::CapacityOverflow);
2808 }
2809 for (local_off, strip_size) in strips {
2810 let pos = scope.position + scope.rotation * local_off;
2811 let child_scope = Scope::new(pos, scope.rotation, strip_size);
2812 child_scope.validate()?;
2813 let args = ev_args!(rule);
2814 queue.push_back(WorkItem {
2815 scope: child_scope,
2816 rule: rule.name.clone(),
2817 args,
2818 depth: depth + 1,
2819 taper: 0.0,
2820 face_profile_override: None,
2821 material: material.clone(),
2822 split_i,
2823 split_n,
2824 rng_state: fork!(),
2825 label: label.clone(),
2826 });
2827 }
2828 }
2829 return Ok(());
2830 }
2831
2832 ShapeOp::Roof { spec, cases } => {
2834 let pitch = if let Some(h_expr) = &spec.height {
2839 let h = ev!(h_expr);
2840 if h <= 0.0 {
2841 return Err(ShapeError::InvalidNumericValue);
2842 }
2843 let run = if spec.roof_type == RoofType::Shed {
2846 scope.size.z.max(1e-9)
2847 } else {
2848 (scope.size.x.min(scope.size.z) / 2.0).max(1e-9)
2849 };
2850 (h / run).atan().to_degrees()
2851 } else {
2852 ev!(&spec.pitch)
2853 };
2854 let secondary_pitch = match &spec.secondary_pitch {
2855 Some(e) => Some(ev!(e)),
2856 None => None,
2857 };
2858 let tier_height = match &spec.tier_height {
2859 Some(e) => Some(ev!(e)),
2860 None => None,
2861 };
2862 let config = RoofConfig {
2863 roof_type: spec.roof_type,
2864 pitch,
2865 secondary_pitch,
2866 overhang: ev!(&spec.overhang),
2867 ridge_offset: ev!(&spec.ridge_offset),
2868 fascia_depth: ev!(&spec.fascia_depth),
2869 tier_height,
2870 };
2871 let resolved_cases = {
2872 let mut rcs = Vec::with_capacity(cases.len());
2873 for c in cases {
2874 let args = ev_args!(&c.rule);
2875 rcs.push(ResolvedRoofCase {
2876 selector: c.selector,
2877 name: c.rule.name.clone(),
2878 args,
2879 });
2880 }
2881 rcs
2882 };
2883 apply_roof(
2884 &config,
2885 &resolved_cases,
2886 &scope,
2887 depth,
2888 &material,
2889 queue,
2890 model,
2891 self.max_terminals,
2892 split_i,
2893 split_n,
2894 rng_state,
2895 spec.ridge_axis,
2896 &label,
2897 )?;
2898 return Ok(());
2899 }
2900
2901 ShapeOp::Attach { world_axis, cases } => {
2903 let len_sq = world_axis.length_squared();
2904 if !world_axis.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
2905 return Err(ShapeError::InvalidAlignTarget);
2906 }
2907 let axis_norm = world_axis.normalize();
2908 let rot = Quat::from_rotation_arc(Vec3::Y, axis_norm);
2912 let attach_scope = Scope::new(
2913 scope.position,
2914 rot.normalize(),
2915 Vec3::new(scope.size.x, scope.size.y, 0.0),
2916 );
2917 attach_scope.validate()?;
2918 if let Some(rule) = find_attach_rule(crate::ops::AttachSelector::Surface, cases)
2919 {
2920 if queue.len() >= MAX_QUEUE {
2921 return Err(ShapeError::CapacityOverflow);
2922 }
2923 let args = ev_args!(rule);
2924 queue.push_back(WorkItem {
2925 scope: attach_scope,
2926 rule: rule.name.clone(),
2927 args,
2928 depth: depth + 1,
2929 taper: 0.0,
2930 face_profile_override: None,
2931 material: material.clone(),
2932 split_i,
2933 split_n,
2934 rng_state: fork!(),
2935 label: label.clone(),
2936 });
2937 }
2938 return Ok(());
2939 }
2940
2941 ShapeOp::Split {
2943 axis,
2944 entries,
2945 snap,
2946 } => {
2947 let total = match axis {
2948 Axis::X => scope.size.x,
2949 Axis::Y => scope.size.y,
2950 Axis::Z => scope.size.z,
2951 };
2952 let mut flat: Vec<(&SplitSlot, ResolvedSize)> = Vec::new();
2955 {
2956 macro_rules! resolve_size {
2957 ($slot:expr) => {
2958 match &$slot.size {
2959 SplitSize::Absolute(e) => ResolvedSize::Absolute(ev!(e)),
2960 SplitSize::Relative(e) => ResolvedSize::Relative(ev!(e)),
2961 SplitSize::Floating(e) => ResolvedSize::Floating(ev!(e)),
2962 }
2963 };
2964 }
2965 let mut resolved_entries: Vec<(usize, Vec<ResolvedSize>)> =
2969 Vec::with_capacity(entries.len());
2970 let mut outside_fixed = 0.0_f64;
2971 let mut outside_float_w = 0.0_f64;
2972 let mut group_nominal = 0.0_f64;
2973 let mut group_len = 0usize;
2974 for (ei, entry) in entries.iter().enumerate() {
2975 match entry {
2976 SplitEntry::Slot(slot) => {
2977 let r = resolve_size!(slot);
2978 match r {
2979 ResolvedSize::Absolute(v) => outside_fixed += v,
2980 ResolvedSize::Relative(t) => outside_fixed += t * total,
2981 ResolvedSize::Floating(w) => outside_float_w += w,
2982 }
2983 resolved_entries.push((ei, vec![r]));
2984 }
2985 SplitEntry::Group(slots) => {
2986 let mut rs = Vec::with_capacity(slots.len());
2987 for slot in slots {
2988 let r = resolve_size!(slot);
2989 group_nominal += match r {
2990 ResolvedSize::Absolute(v) => v,
2991 ResolvedSize::Relative(t) => t * total,
2992 ResolvedSize::Floating(w) => w,
2993 };
2994 rs.push(r);
2995 }
2996 group_len = slots.len();
2997 resolved_entries.push((ei, rs));
2998 }
2999 }
3000 }
3001 if !outside_fixed.is_finite()
3002 || !outside_float_w.is_finite()
3003 || !group_nominal.is_finite()
3004 {
3005 return Err(ShapeError::InvalidNumericValue);
3006 }
3007 if outside_fixed > total + 1e-9 {
3008 return Err(ShapeError::SplitOverflow(total));
3009 }
3010 let remaining = (total - outside_fixed).max(0.0);
3011 let k = if group_len > 0 && group_nominal > 1e-12 {
3013 ((remaining + 1e-9) / group_nominal).floor() as usize
3014 } else {
3015 0
3016 };
3017 let singles = entries.len() - usize::from(group_len > 0);
3018 if singles + k * group_len > MAX_SPLIT_CHILDREN {
3019 return Err(ShapeError::CapacityOverflow);
3020 }
3021 let leftover = remaining - k as f64 * group_nominal;
3022 let copy_scale = if group_len > 0 && outside_float_w <= 0.0 {
3026 if k == 0 {
3027 if remaining > 1e-9 {
3028 return Err(ShapeError::SplitOverflow(total));
3029 }
3030 1.0
3031 } else {
3032 remaining / (k as f64 * group_nominal)
3033 }
3034 } else {
3035 1.0
3036 };
3037 for (ei, rs) in &resolved_entries {
3038 match &entries[*ei] {
3039 SplitEntry::Slot(slot) => {
3040 let r = match rs[0] {
3041 ResolvedSize::Floating(w) => {
3042 if outside_float_w <= 0.0 {
3046 return Err(ShapeError::NoFloatingSlots);
3047 }
3048 ResolvedSize::Absolute(leftover * (w / outside_float_w))
3049 }
3050 other => other,
3051 };
3052 flat.push((slot, r));
3053 }
3054 SplitEntry::Group(slots) => {
3055 for _copy in 0..k {
3056 for (slot, r) in slots.iter().zip(rs.iter()) {
3057 let v = match *r {
3058 ResolvedSize::Absolute(v) => v,
3059 ResolvedSize::Relative(t) => t * total,
3060 ResolvedSize::Floating(w) => w,
3061 };
3062 flat.push((
3063 slot,
3064 ResolvedSize::Absolute(v * copy_scale),
3065 ));
3066 }
3067 }
3068 }
3069 }
3070 }
3071 }
3072 let resolved: Vec<ResolvedSize> = flat.iter().map(|(_, r)| *r).collect();
3073 let mut sizes = resolve_split_sizes(&resolved, total)?;
3074 if let Some(binding) = snap {
3079 let tol = binding.tolerance.unwrap_or(0.05 * total);
3080 snap_split_boundaries(
3081 &scope,
3082 *axis,
3083 &mut sizes,
3084 &binding.label,
3085 tol,
3086 &model.snap_planes,
3087 );
3088 }
3089 if queue.len() + flat.len() > MAX_QUEUE {
3090 return Err(ShapeError::CapacityOverflow);
3091 }
3092 let child_n = flat.len();
3093 let mut offset = 0.0;
3094 for (i, ((slot, _), size)) in flat.iter().zip(sizes.iter()).enumerate() {
3095 let child = slice_scope(&scope, *axis, offset, *size);
3096 child.validate()?;
3097 let args = ev_args!(&slot.rule);
3098 queue.push_back(WorkItem {
3099 scope: child,
3100 rule: slot.rule.name.clone(),
3101 args,
3102 depth: depth + 1,
3103 taper: 0.0,
3104 face_profile_override: None,
3105 material: material.clone(),
3106 split_i: i,
3107 split_n: child_n,
3108 rng_state: fork!(),
3109 label: label.clone(),
3110 });
3111 offset += size;
3112 }
3113 return Ok(());
3114 }
3115
3116 ShapeOp::SplitArea { axis, slots } => {
3121 let (total, cross) = match axis {
3122 Axis::X => (scope.size.x, scope.size.z),
3123 Axis::Z => (scope.size.z, scope.size.x),
3124 Axis::Y => return Err(ShapeError::InvalidNumericValue),
3125 };
3126 if !cross.is_finite() || cross <= 1e-12 {
3127 return Err(ShapeError::InvalidNumericValue);
3128 }
3129 let resolved: Vec<ResolvedSize> = {
3130 let mut v = Vec::with_capacity(slots.len());
3131 for slot in slots {
3132 v.push(match &slot.size {
3133 SplitSize::Absolute(e) => ResolvedSize::Absolute(ev!(e) / cross),
3136 SplitSize::Relative(e) => ResolvedSize::Relative(ev!(e)),
3137 SplitSize::Floating(e) => ResolvedSize::Floating(ev!(e)),
3138 });
3139 }
3140 v
3141 };
3142 let sizes = resolve_split_sizes(&resolved, total)?;
3143 if queue.len() + slots.len() > MAX_QUEUE {
3144 return Err(ShapeError::CapacityOverflow);
3145 }
3146 let child_n = slots.len();
3147 let mut offset = 0.0;
3148 for (i, (slot, size)) in slots.iter().zip(sizes.iter()).enumerate() {
3149 let child = slice_scope(&scope, *axis, offset, *size);
3150 child.validate()?;
3151 let args = ev_args!(&slot.rule);
3152 queue.push_back(WorkItem {
3153 scope: child,
3154 rule: slot.rule.name.clone(),
3155 args,
3156 depth: depth + 1,
3157 taper: 0.0,
3158 face_profile_override: None,
3159 material: material.clone(),
3160 split_i: i,
3161 split_n: child_n,
3162 rng_state: fork!(),
3163 label: label.clone(),
3164 });
3165 offset += size;
3166 }
3167 return Ok(());
3168 }
3169
3170 ShapeOp::Fit { axis, candidates } => {
3175 let extent = match axis {
3176 Axis::X => scope.size.x,
3177 Axis::Y => scope.size.y,
3178 Axis::Z => scope.size.z,
3179 };
3180 for cand in candidates {
3181 let min = ev!(&cand.min_size);
3182 if min <= extent + 1e-9 {
3183 if queue.len() >= MAX_QUEUE {
3184 return Err(ShapeError::CapacityOverflow);
3185 }
3186 let args = ev_args!(&cand.rule);
3187 queue.push_back(WorkItem {
3188 scope,
3189 rule: cand.rule.name.clone(),
3190 args,
3191 depth: depth + 1,
3192 taper,
3193 face_profile_override: face_profile.take(),
3194 material: material.clone(),
3195 split_i,
3196 split_n,
3197 rng_state: fork!(),
3198 label: label.clone(),
3199 });
3200 return Ok(());
3201 }
3202 }
3203 return Ok(());
3204 }
3205
3206 ShapeOp::Repeat {
3212 axis,
3213 tile_sizes,
3214 rule,
3215 } => {
3216 if tile_sizes.is_empty() {
3217 return Err(ShapeError::InvalidNumericValue);
3218 }
3219 let tile_sizes: Vec<f64> = {
3220 let mut v = Vec::with_capacity(tile_sizes.len());
3221 for ts in tile_sizes {
3222 let t = ev!(ts);
3223 if t <= 0.0 {
3224 return Err(ShapeError::InvalidNumericValue);
3225 }
3226 v.push(t);
3227 }
3228 v
3229 };
3230 let total = match axis {
3231 Axis::X => scope.size.x,
3232 Axis::Y => scope.size.y,
3233 Axis::Z => scope.size.z,
3234 };
3235 if !total.is_finite() || total <= 0.0 {
3239 return Err(ShapeError::InvalidNumericValue);
3240 }
3241 let pattern_min = tile_sizes.iter().cloned().fold(f64::INFINITY, f64::min);
3242 if pattern_min <= 0.0 {
3243 return Err(ShapeError::InvalidNumericValue);
3244 }
3245 let n_max_f = (total / pattern_min).floor();
3250 if !n_max_f.is_finite() {
3251 return Err(ShapeError::CapacityOverflow);
3252 }
3253 let n_max = n_max_f as usize;
3254 if queue.len().saturating_add(n_max) > MAX_QUEUE {
3255 return Err(ShapeError::CapacityOverflow);
3256 }
3257 let mut placed: Vec<f64> = Vec::new();
3261 let mut acc = 0.0_f64;
3262 loop {
3263 let next = tile_sizes[placed.len() % tile_sizes.len()];
3264 if acc + next > total + 1e-12 {
3265 break;
3266 }
3267 placed.push(next);
3268 acc += next;
3269 }
3270 if !placed.is_empty() {
3271 let scale = total / acc;
3272 let child_n = placed.len();
3273 let mut offset = 0.0_f64;
3274 for (i, tile) in placed.iter().enumerate() {
3275 let actual = tile * scale;
3276 let child = slice_scope(&scope, *axis, offset, actual);
3277 child.validate()?;
3278 let args = ev_args!(rule);
3281 queue.push_back(WorkItem {
3282 scope: child,
3283 rule: rule.name.clone(),
3284 args,
3285 depth: depth + 1,
3286 taper: 0.0,
3287 face_profile_override: None,
3288 material: material.clone(),
3289 split_i: i,
3290 split_n: child_n,
3291 rng_state: fork!(),
3292 label: label.clone(),
3293 });
3294 offset += actual;
3295 }
3296 }
3297 return Ok(());
3298 }
3299
3300 ShapeOp::Comp(CompTarget::Edges(cases)) => {
3306 let descs = edge_descs(scope.size);
3307 if queue.len() + descs.len() > MAX_QUEUE {
3308 return Err(ShapeError::CapacityOverflow);
3309 }
3310 for (class, origin, dir, len) in descs {
3311 let Some(rule) = find_edge_rule(class, cases) else {
3312 continue;
3313 };
3314 if len <= 1e-9 {
3315 continue;
3316 }
3317 let pos = scope.position + scope.rotation * origin;
3318 let rot =
3320 (scope.rotation * Quat::from_rotation_arc(Vec3::X, dir)).normalize();
3321 let child = Scope::new(pos, rot, Vec3::new(len, 0.0, 0.0));
3322 child.validate()?;
3323 let args = ev_args!(rule);
3324 queue.push_back(WorkItem {
3325 scope: child,
3326 rule: rule.name.clone(),
3327 args,
3328 depth: depth + 1,
3329 taper: 0.0,
3330 face_profile_override: None,
3331 material: material.clone(),
3332 split_i,
3333 split_n,
3334 rng_state: fork!(),
3335 label: label.clone(),
3336 });
3337 }
3338 return Ok(());
3339 }
3340
3341 ShapeOp::Comp(CompTarget::Faces(cases)) => {
3342 if queue.len() + 6 > MAX_QUEUE {
3344 return Err(ShapeError::CapacityOverflow);
3345 }
3346 for (selector, offset_local, face_size, rot_delta) in face_descs(scope.size) {
3347 let rule = match find_face_rule(selector, cases) {
3348 Some(r) => r,
3349 None => continue,
3350 };
3351 let face_pos = scope.position + scope.rotation * offset_local;
3352 let face_rotation = scope.rotation * rot_delta;
3353 let face_scope = Scope::new(face_pos, face_rotation, face_size);
3354 face_scope.validate()?;
3355 let args = ev_args!(&rule);
3356 queue.push_back(WorkItem {
3357 scope: face_scope,
3358 rule: rule.name.clone(),
3359 args,
3360 depth: depth + 1,
3361 taper: 0.0,
3362 face_profile_override: None,
3363 material: material.clone(),
3364 split_i,
3365 split_n,
3366 rng_state: fork!(),
3367 label: label.clone(),
3368 });
3369 }
3370 return Ok(());
3371 }
3372
3373 ShapeOp::I(mesh_id) => {
3375 if model.len() >= self.max_terminals {
3376 return Err(ShapeError::CapacityOverflow);
3377 }
3378 let profile = face_profile
3379 .take()
3380 .unwrap_or_else(|| taper_to_profile(taper));
3381 let mut terminal = Terminal::new_profiled(scope, mesh_id, profile, material);
3382 terminal.label = label;
3383 model.push(terminal);
3384 return Ok(());
3385 }
3386
3387 ShapeOp::Rule(call) => {
3389 let args = ev_args!(call);
3390 queue.push_back(WorkItem {
3391 scope,
3392 rule: call.name.clone(),
3393 args,
3394 depth: depth + 1,
3395 taper,
3396 face_profile_override: face_profile,
3397 material,
3398 split_i,
3399 split_n,
3400 rng_state: fork!(),
3401 label: label.clone(),
3402 });
3403 return Ok(());
3404 }
3405 }
3406 }
3407
3408 Ok(())
3411 }
3412}
3413
3414#[cfg(test)]
3415mod tests {
3416 use super::*;
3417 use crate::ops::{Axis, SplitSize, SplitSlot};
3418 use crate::scope::{Quat, Vec3};
3419
3420 fn slot(size: SplitSize, rule: &str) -> SplitSlot {
3421 SplitSlot {
3422 size,
3423 rule: rule.into(),
3424 }
3425 }
3426
3427 fn qexpr(q: Quat) -> [Expr; 4] {
3429 [
3430 Expr::lit(q.w),
3431 Expr::lit(q.x),
3432 Expr::lit(q.y),
3433 Expr::lit(q.z),
3434 ]
3435 }
3436
3437 fn resolved(slots: &[SplitSlot]) -> Vec<ResolvedSize> {
3439 slots
3440 .iter()
3441 .map(|s| match &s.size {
3442 SplitSize::Absolute(e) => ResolvedSize::Absolute(e.as_lit().unwrap()),
3443 SplitSize::Relative(e) => ResolvedSize::Relative(e.as_lit().unwrap()),
3444 SplitSize::Floating(e) => ResolvedSize::Floating(e.as_lit().unwrap()),
3445 })
3446 .collect()
3447 }
3448
3449 #[test]
3450 fn test_resolve_split_absolute() {
3451 let slots = vec![
3452 slot(SplitSize::abs(3.0), "A"),
3453 slot(SplitSize::abs(7.0), "B"),
3454 ];
3455 let sizes = resolve_split_sizes(&resolved(&slots), 10.0).unwrap();
3456 assert!((sizes[0] - 3.0).abs() < 1e-9);
3457 assert!((sizes[1] - 7.0).abs() < 1e-9);
3458 }
3459
3460 #[test]
3461 fn test_resolve_split_floating_equal() {
3462 let slots = vec![
3463 slot(SplitSize::float(1.0), "A"),
3464 slot(SplitSize::float(1.0), "B"),
3465 ];
3466 let sizes = resolve_split_sizes(&resolved(&slots), 10.0).unwrap();
3467 assert!((sizes[0] - 5.0).abs() < 1e-9);
3468 assert!((sizes[1] - 5.0).abs() < 1e-9);
3469 }
3470
3471 #[test]
3472 fn test_resolve_split_mixed() {
3473 let slots = vec![
3474 slot(SplitSize::abs(2.0), "Base"),
3475 slot(SplitSize::float(1.0), "A"),
3476 slot(SplitSize::float(1.0), "B"),
3477 ];
3478 let sizes = resolve_split_sizes(&resolved(&slots), 10.0).unwrap();
3479 assert!((sizes[0] - 2.0).abs() < 1e-9);
3480 assert!((sizes[1] - 4.0).abs() < 1e-9);
3481 assert!((sizes[2] - 4.0).abs() < 1e-9);
3482 }
3483
3484 #[test]
3485 fn test_resolve_split_overflow_rejected() {
3486 let slots = vec![
3487 slot(SplitSize::abs(6.0), "A"),
3488 slot(SplitSize::abs(6.0), "B"),
3489 ];
3490 assert!(matches!(
3491 resolve_split_sizes(&resolved(&slots), 10.0),
3492 Err(ShapeError::SplitOverflow(_))
3493 ));
3494 }
3495
3496 #[test]
3497 fn test_derive_extrude_then_terminal() {
3498 let mut interp = Interpreter::new();
3499 interp.add_rule(
3500 "Lot",
3501 vec![
3502 ShapeOp::Extrude(Expr::lit(10.0)),
3503 ShapeOp::I("Building".to_string()),
3504 ],
3505 );
3506 let scope = Scope::unit();
3507 let model = interp.derive(scope, "Lot").unwrap();
3508 assert_eq!(model.len(), 1);
3509 assert_eq!(model.terminals[0].mesh_id, "Building");
3510 assert!((model.terminals[0].scope.size.y - 10.0).abs() < 1e-9);
3511 }
3512
3513 #[test]
3514 fn test_derive_split_y_three_floors() {
3515 let mut interp = Interpreter::new();
3516 interp.add_rule(
3517 "Building",
3518 vec![ShapeOp::Split {
3519 axis: Axis::Y,
3520 entries: vec![
3521 slot(SplitSize::abs(2.0), "Ground").into(),
3522 slot(SplitSize::float(1.0), "Upper").into(),
3523 slot(SplitSize::abs(1.5), "Roof").into(),
3524 ],
3525 snap: None,
3526 }],
3527 );
3528 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 10.0, 10.0));
3529 let model = interp.derive(scope, "Building").unwrap();
3530 assert_eq!(model.len(), 3);
3531 assert!((model.terminals[0].scope.size.y - 2.0).abs() < 1e-9);
3532 assert!((model.terminals[1].scope.size.y - 6.5).abs() < 1e-9);
3533 assert!((model.terminals[2].scope.size.y - 1.5).abs() < 1e-9);
3534 }
3535
3536 #[test]
3537 fn test_derive_depth_limit() {
3538 let mut interp = Interpreter::new();
3539 interp.add_rule("A", vec![ShapeOp::Rule("A".into())]);
3540 interp.max_depth = 5;
3541 let model = interp.derive(Scope::unit(), "A");
3542 assert!(matches!(model, Err(ShapeError::DepthLimitExceeded(_))));
3543 }
3544
3545 #[test]
3546 fn test_derive_comp_faces() {
3547 let mut interp = Interpreter::new();
3548 interp.add_rule(
3549 "Box",
3550 vec![ShapeOp::Comp(CompTarget::Faces(vec![
3551 crate::ops::CompFaceCase {
3552 selector: FaceSelector::Top,
3553 rule: "Roof".into(),
3554 },
3555 crate::ops::CompFaceCase {
3556 selector: FaceSelector::Side,
3557 rule: "Wall".into(),
3558 },
3559 crate::ops::CompFaceCase {
3560 selector: FaceSelector::Bottom,
3561 rule: "Base".into(),
3562 },
3563 ]))],
3564 );
3565 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(5.0, 3.0, 5.0));
3566 let model = interp.derive(scope, "Box").unwrap();
3567 assert_eq!(model.len(), 6);
3568 }
3569
3570 #[test]
3571 fn test_derive_repeat() {
3572 let mut interp = Interpreter::new();
3573 interp.add_rule(
3574 "Facade",
3575 vec![ShapeOp::Repeat {
3576 axis: Axis::X,
3577 tile_sizes: vec![Expr::lit(2.0)],
3578 rule: "Window".into(),
3579 }],
3580 );
3581 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 0.0));
3582 let model = interp.derive(scope, "Facade").unwrap();
3583 assert_eq!(model.len(), 5);
3585 }
3586
3587 #[test]
3588 fn test_derive_mat_propagates() {
3589 let mut interp = Interpreter::new();
3590 interp.add_rule(
3591 "R",
3592 vec![
3593 ShapeOp::Mat(Material::new("Brick")),
3594 ShapeOp::I("Wall".to_string()),
3595 ],
3596 );
3597 let model = interp.derive(Scope::unit(), "R").unwrap();
3598 assert_eq!(model.terminals[0].material, Some(Material::new("Brick")));
3599 }
3600
3601 #[test]
3604 fn test_empty_variants_discards_shape() {
3605 let mut interp = Interpreter::new();
3606 interp.add_weighted_rules("Empty", vec![]).unwrap();
3609 let model = interp.derive(Scope::unit(), "Empty").unwrap();
3610 assert_eq!(model.len(), 0);
3611 }
3612
3613 #[test]
3616 fn test_repeat_tiny_tile_size_rejected() {
3617 let mut interp = Interpreter::new();
3622 interp.add_rule(
3623 "R",
3624 vec![ShapeOp::Repeat {
3625 axis: Axis::X,
3626 tile_sizes: vec![Expr::lit(f64::MIN_POSITIVE)],
3627 rule: "Tile".into(),
3628 }],
3629 );
3630 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
3631 assert!(matches!(
3632 interp.derive(scope, "R"),
3633 Err(ShapeError::CapacityOverflow)
3634 ));
3635 }
3636
3637 #[test]
3640 fn test_scale_multiply_overflow_to_infinity_rejected() {
3641 let mut interp = Interpreter::new();
3644 interp.add_rule(
3645 "R",
3646 vec![
3647 ShapeOp::Scale([Expr::lit(1e200), Expr::lit(1.0), Expr::lit(1.0)]),
3648 ShapeOp::Scale([Expr::lit(1e200), Expr::lit(1.0), Expr::lit(1.0)]), ShapeOp::I("Mesh".to_string()),
3650 ],
3651 );
3652 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
3653 assert!(matches!(
3654 interp.derive(scope, "R"),
3655 Err(ShapeError::InvalidNumericValue)
3656 ));
3657 }
3658
3659 #[test]
3660 fn test_split_absolute_sum_overflow_rejected() {
3661 let slots = vec![
3663 slot(SplitSize::abs(f64::MAX), "A"),
3664 slot(SplitSize::abs(f64::MAX), "B"),
3665 ];
3666 assert!(matches!(
3667 resolve_split_sizes(&resolved(&slots), f64::MAX),
3668 Err(ShapeError::InvalidNumericValue)
3669 ));
3670 }
3671
3672 #[test]
3675 fn test_repeat_respects_combined_queue_limit() {
3676 let mut interp = Interpreter::new();
3682 interp.add_rule(
3684 "Big",
3685 vec![ShapeOp::Repeat {
3686 axis: Axis::X,
3687 tile_sizes: vec![Expr::lit(1e-1)],
3688 rule: "Tile".into(),
3689 }],
3690 );
3691 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1e10, 1.0, 1.0));
3692 assert!(matches!(
3693 interp.derive(scope, "Big"),
3694 Err(ShapeError::CapacityOverflow)
3695 ));
3696 }
3697
3698 #[test]
3701 fn test_api_negative_scale_rejected() {
3702 let mut interp = Interpreter::new();
3703 interp.add_rule(
3704 "R",
3705 vec![
3706 ShapeOp::Scale([Expr::lit(-1.0), Expr::lit(1.0), Expr::lit(1.0)]),
3707 ShapeOp::I("Mesh".to_string()),
3708 ],
3709 );
3710 assert!(matches!(
3711 interp.derive(Scope::unit(), "R"),
3712 Err(ShapeError::InvalidNumericValue)
3713 ));
3714 }
3715
3716 #[test]
3717 fn test_api_zero_scale_rejected() {
3718 let mut interp = Interpreter::new();
3719 interp.add_rule(
3720 "R",
3721 vec![
3722 ShapeOp::Scale([Expr::lit(0.0), Expr::lit(1.0), Expr::lit(1.0)]),
3723 ShapeOp::I("Mesh".to_string()),
3724 ],
3725 );
3726 assert!(matches!(
3727 interp.derive(Scope::unit(), "R"),
3728 Err(ShapeError::InvalidNumericValue)
3729 ));
3730 }
3731
3732 #[test]
3735 fn test_split_floating_large_remaining_no_overflow() {
3736 let slots = vec![
3740 slot(SplitSize::float(2.0), "A"),
3741 slot(SplitSize::float(1.0), "B"),
3742 ];
3743 let sizes = resolve_split_sizes(&resolved(&slots), 1e308).unwrap();
3744 assert!(sizes[0].is_finite(), "size[0] overflowed to {}", sizes[0]);
3745 assert!(sizes[1].is_finite(), "size[1] overflowed to {}", sizes[1]);
3746 assert!((sizes[0] / sizes[1] - 2.0).abs() < 1e-6);
3748 }
3749
3750 #[test]
3753 fn test_split_floating_weight_overflow_rejected() {
3754 let slots = vec![
3757 slot(SplitSize::float(f64::MAX), "A"),
3758 slot(SplitSize::float(f64::MAX), "B"),
3759 ];
3760 assert!(matches!(
3761 resolve_split_sizes(&resolved(&slots), 10.0),
3762 Err(ShapeError::InvalidNumericValue)
3763 ));
3764 }
3765
3766 #[test]
3767 fn test_stochastic_rule_deterministic_with_seed() {
3768 let mut interp = Interpreter::new();
3769 interp
3770 .add_weighted_rules(
3771 "Facade",
3772 vec![
3773 (70.0, vec![ShapeOp::I("Brick".to_string())]),
3774 (30.0, vec![ShapeOp::I("Glass".to_string())]),
3775 ],
3776 )
3777 .unwrap();
3778 interp.seed = 42;
3779 let m1 = interp.derive(Scope::unit(), "Facade").unwrap();
3781 let m2 = interp.derive(Scope::unit(), "Facade").unwrap();
3782 assert_eq!(m1.terminals[0].mesh_id, m2.terminals[0].mesh_id);
3783 }
3784
3785 #[test]
3786 fn test_face_comp_orientations() {
3787 let mut interp = Interpreter::new();
3791 interp.add_rule(
3792 "Box",
3793 vec![ShapeOp::Comp(CompTarget::Faces(vec![
3794 crate::ops::CompFaceCase {
3795 selector: FaceSelector::All,
3796 rule: "Face".into(),
3797 },
3798 ]))],
3799 );
3800 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 2.0));
3801 let model = interp.derive(scope, "Box").unwrap();
3802 assert_eq!(model.len(), 6);
3803
3804 let normals: Vec<Vec3> = model
3806 .terminals
3807 .iter()
3808 .map(|t| t.scope.rotation * Vec3::Z)
3809 .collect();
3810
3811 let expected = [
3813 Vec3::NEG_Y, Vec3::Y, Vec3::NEG_Z, Vec3::Z, Vec3::NEG_X, Vec3::X, ];
3820 for exp in &expected {
3821 assert!(
3822 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
3823 "missing normal {:?}, got {:?}",
3824 exp,
3825 normals
3826 );
3827 }
3828
3829 let pos = |i: usize| model.terminals[i].scope.position;
3833 assert!(
3834 (pos(0) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
3835 "Bottom pos"
3836 ); assert!(
3838 (pos(1) - Vec3::new(0.0, 3.0, 2.0)).length() < 1e-6,
3839 "Top pos"
3840 ); assert!(
3842 (pos(2) - Vec3::new(4.0, 0.0, 0.0)).length() < 1e-6,
3843 "Front pos"
3844 ); assert!(
3846 (pos(3) - Vec3::new(0.0, 0.0, 2.0)).length() < 1e-6,
3847 "Back pos"
3848 ); assert!(
3850 (pos(4) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
3851 "Left pos"
3852 ); assert!(
3854 (pos(5) - Vec3::new(4.0, 0.0, 2.0)).length() < 1e-6,
3855 "Right pos"
3856 ); }
3858
3859 #[test]
3862 fn test_negative_scope_size_rejected() {
3863 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(-1.0, 1.0, 1.0));
3864 let interp = Interpreter::new();
3865 assert!(matches!(
3866 interp.derive(scope, "Anything"),
3867 Err(ShapeError::InvalidNumericValue)
3868 ));
3869 }
3870
3871 #[test]
3872 fn test_zero_scope_size_accepted() {
3873 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 0.0, 10.0));
3875 let interp = Interpreter::new();
3876 let model = interp.derive(scope, "Footprint").unwrap();
3877 assert_eq!(model.len(), 1);
3878 }
3879
3880 #[test]
3883 fn test_unnormalized_root_quat_rejected() {
3884 let bad_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
3886 let scope = Scope::new(Vec3::ZERO, bad_q, Vec3::ONE);
3887 let interp = Interpreter::new();
3888 assert!(matches!(
3889 interp.derive(scope, "Anything"),
3890 Err(ShapeError::InvalidNumericValue)
3891 ));
3892 }
3893
3894 #[test]
3895 fn test_degenerate_rotate_op_rejected() {
3896 let zero = [
3898 Expr::lit(0.0),
3899 Expr::lit(0.0),
3900 Expr::lit(0.0),
3901 Expr::lit(0.0),
3902 ];
3903 let mut interp = Interpreter::new();
3904 interp.add_rule(
3905 "R",
3906 vec![ShapeOp::Rotate(zero), ShapeOp::I("M".to_string())],
3907 );
3908 assert!(matches!(
3909 interp.derive(Scope::unit(), "R"),
3910 Err(ShapeError::InvalidNumericValue)
3911 ));
3912 }
3913
3914 #[test]
3915 fn test_scaled_rotate_op_normalized() {
3916 let scaled_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0); let mut interp = Interpreter::new();
3920 interp.add_rule(
3921 "R",
3922 vec![
3923 ShapeOp::Rotate(qexpr(scaled_q)),
3924 ShapeOp::I("M".to_string()),
3925 ],
3926 );
3927 let model = interp.derive(Scope::unit(), "R").unwrap();
3929 assert_eq!(model.len(), 1);
3930 let r = model.terminals[0].scope.rotation;
3931 assert!(
3932 (r.length_squared() - 1.0).abs() < 1e-9,
3933 "rotation should be unit"
3934 );
3935 }
3936
3937 #[test]
3940 fn test_nan_weight_rejected() {
3941 let mut interp = Interpreter::new();
3942 assert!(matches!(
3943 interp.add_weighted_rules("R", vec![(f64::NAN, vec![ShapeOp::I("M".to_string())])]),
3944 Err(ShapeError::InvalidNumericValue)
3945 ));
3946 }
3947
3948 #[test]
3949 fn test_infinite_weight_rejected() {
3950 let mut interp = Interpreter::new();
3951 assert!(matches!(
3952 interp.add_weighted_rules(
3953 "R",
3954 vec![(f64::INFINITY, vec![ShapeOp::I("M".to_string())])]
3955 ),
3956 Err(ShapeError::InvalidNumericValue)
3957 ));
3958 }
3959
3960 #[test]
3961 fn test_negative_weight_rejected() {
3962 let mut interp = Interpreter::new();
3963 assert!(matches!(
3964 interp.add_weighted_rules("R", vec![(-1.0, vec![ShapeOp::I("M".to_string())])]),
3965 Err(ShapeError::InvalidNumericValue)
3966 ));
3967 }
3968
3969 #[test]
3972 fn test_align_y_to_world_up_when_rotated() {
3973 let mut interp = Interpreter::new();
3975 let ninety_z = Quat::from_axis_angle(Vec3::Z, std::f64::consts::FRAC_PI_2);
3976 interp.add_rule(
3977 "R",
3978 vec![
3979 ShapeOp::Rotate(qexpr(ninety_z)),
3980 ShapeOp::Align {
3981 local_axis: Axis::Y,
3982 target: Vec3::Y,
3983 },
3984 ShapeOp::I("M".to_string()),
3985 ],
3986 );
3987 let model = interp.derive(Scope::unit(), "R").unwrap();
3988 assert_eq!(model.len(), 1);
3989 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
3990 assert!(
3991 (world_y - Vec3::Y).length() < 1e-6,
3992 "expected Y=(0,1,0), got {:?}",
3993 world_y
3994 );
3995 }
3996
3997 #[test]
3998 fn test_align_already_aligned_is_noop() {
3999 let mut interp = Interpreter::new();
4000 interp.add_rule(
4001 "R",
4002 vec![
4003 ShapeOp::Align {
4004 local_axis: Axis::Y,
4005 target: Vec3::Y,
4006 },
4007 ShapeOp::I("M".to_string()),
4008 ],
4009 );
4010 let model = interp.derive(Scope::unit(), "R").unwrap();
4011 let rot = model.terminals[0].scope.rotation;
4012 assert!((rot.length_squared() - 1.0).abs() < 1e-9);
4013 let world_y = rot * Vec3::Y;
4015 assert!((world_y - Vec3::Y).length() < 1e-6);
4016 }
4017
4018 #[test]
4019 fn test_align_zero_target_rejected() {
4020 let mut interp = Interpreter::new();
4021 interp.add_rule(
4022 "R",
4023 vec![
4024 ShapeOp::Align {
4025 local_axis: Axis::Y,
4026 target: Vec3::ZERO,
4027 },
4028 ShapeOp::I("M".to_string()),
4029 ],
4030 );
4031 assert!(matches!(
4032 interp.derive(Scope::unit(), "R"),
4033 Err(ShapeError::InvalidAlignTarget)
4034 ));
4035 }
4036
4037 #[test]
4040 fn test_offset_inset_produces_inside_and_border() {
4041 let mut interp = Interpreter::new();
4042 interp.add_rule(
4043 "R",
4044 vec![ShapeOp::Offset {
4045 distance: Expr::lit(-0.5),
4046 cases: vec![
4047 crate::ops::OffsetCase {
4048 selector: crate::ops::OffsetSelector::Inside,
4049 rule: "Glass".into(),
4050 },
4051 crate::ops::OffsetCase {
4052 selector: crate::ops::OffsetSelector::Border,
4053 rule: "Frame".into(),
4054 },
4055 ],
4056 }],
4057 );
4058 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
4060 let model = interp.derive(scope, "R").unwrap();
4061 assert_eq!(model.len(), 5);
4063 let inside = model
4065 .terminals
4066 .iter()
4067 .find(|t| t.mesh_id == "Glass")
4068 .unwrap();
4069 assert!((inside.scope.size.x - 3.0).abs() < 1e-9);
4070 assert!((inside.scope.size.y - 2.0).abs() < 1e-9);
4071 assert!((inside.scope.position - Vec3::new(0.5, 0.5, 0.0)).length() < 1e-9);
4072 }
4073
4074 #[test]
4075 fn test_offset_too_large_rejected() {
4076 let mut interp = Interpreter::new();
4077 interp.add_rule(
4078 "R",
4079 vec![ShapeOp::Offset {
4080 distance: Expr::lit(-2.0), cases: vec![crate::ops::OffsetCase {
4082 selector: crate::ops::OffsetSelector::Inside,
4083 rule: "A".into(),
4084 }],
4085 }],
4086 );
4087 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
4088 assert!(matches!(
4089 interp.derive(scope, "R"),
4090 Err(ShapeError::OffsetTooLarge)
4091 ));
4092 }
4093
4094 #[test]
4095 fn test_offset_positive_distance_is_an_outset() {
4096 let mut interp = Interpreter::new();
4099 interp.add_rule(
4100 "R",
4101 vec![ShapeOp::Offset {
4102 distance: Expr::lit(0.2),
4103 cases: vec![crate::ops::OffsetCase {
4104 selector: crate::ops::OffsetSelector::Inside,
4105 rule: "A".into(),
4106 }],
4107 }],
4108 );
4109 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
4110 let model = interp.derive(scope, "R").unwrap();
4111 assert_eq!(model.len(), 1);
4112 let t = &model.terminals[0];
4113 assert!((t.scope.size.x - 4.4).abs() < 1e-9);
4114 assert!((t.scope.size.y - 3.4).abs() < 1e-9);
4115 assert!((t.scope.position.x - -0.2).abs() < 1e-9);
4116 assert!((t.scope.position.y - -0.2).abs() < 1e-9);
4117 let mut i2 = Interpreter::new();
4119 i2.add_rule(
4120 "R",
4121 vec![ShapeOp::Offset {
4122 distance: Expr::lit(0.0),
4123 cases: vec![crate::ops::OffsetCase {
4124 selector: crate::ops::OffsetSelector::Inside,
4125 rule: "A".into(),
4126 }],
4127 }],
4128 );
4129 assert!(matches!(
4130 i2.derive(Scope::unit(), "R"),
4131 Err(ShapeError::InvalidNumericValue)
4132 ));
4133 }
4134
4135 #[test]
4138 fn test_roof_shed_produces_one_slope() {
4139 let mut interp = Interpreter::new();
4140 interp.add_rule(
4141 "R",
4142 vec![ShapeOp::Roof {
4143 spec: RoofConfig::new(RoofType::Shed, 30.0).into(),
4144 cases: vec![crate::ops::RoofCase {
4145 selector: RoofFaceSelector::Slope,
4146 rule: "Tiles".into(),
4147 }],
4148 }],
4149 );
4150 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
4151 let model = interp.derive(scope, "R").unwrap();
4152 assert_eq!(model.len(), 1);
4153 assert_eq!(model.terminals[0].mesh_id, "Tiles");
4154 assert!((model.terminals[0].scope.position.y - 0.0).abs() < 1e-6);
4156 }
4157
4158 #[test]
4159 fn test_roof_gable_produces_four_panels() {
4160 let mut interp = Interpreter::new();
4161 interp.add_rule(
4162 "R",
4163 vec![ShapeOp::Roof {
4164 spec: RoofConfig::new(RoofType::Gable, 30.0).into(),
4165 cases: vec![
4166 crate::ops::RoofCase {
4167 selector: RoofFaceSelector::Slope,
4168 rule: "Tiles".into(),
4169 },
4170 crate::ops::RoofCase {
4171 selector: RoofFaceSelector::GableEnd,
4172 rule: "Bricks".into(),
4173 },
4174 ],
4175 }],
4176 );
4177 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
4178 let model = interp.derive(scope, "R").unwrap();
4179 assert_eq!(model.len(), 4);
4181 let tiles: Vec<_> = model
4182 .terminals
4183 .iter()
4184 .filter(|t| t.mesh_id == "Tiles")
4185 .collect();
4186 let bricks: Vec<_> = model
4187 .terminals
4188 .iter()
4189 .filter(|t| t.mesh_id == "Bricks")
4190 .collect();
4191 assert_eq!(tiles.len(), 2);
4192 assert_eq!(bricks.len(), 2);
4193 }
4194
4195 #[test]
4196 fn test_roof_hip_produces_four_slopes() {
4197 let mut interp = Interpreter::new();
4198 interp.add_rule(
4199 "R",
4200 vec![ShapeOp::Roof {
4201 spec: crate::ops::RoofSpec::from({
4202 let mut c = RoofConfig::new(RoofType::Hip, 45.0);
4203 c.overhang = 0.3;
4204 c
4205 }),
4206 cases: vec![crate::ops::RoofCase {
4207 selector: RoofFaceSelector::Slope,
4208 rule: "Tiles".into(),
4209 }],
4210 }],
4211 );
4212 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
4213 let model = interp.derive(scope, "R").unwrap();
4214 assert_eq!(model.len(), 4);
4215 }
4216
4217 #[test]
4218 fn test_roof_pyramid_produces_four_tapered_slopes() {
4219 let mut interp = Interpreter::new();
4220 interp.add_rule(
4221 "R",
4222 vec![ShapeOp::Roof {
4223 spec: RoofConfig::new(RoofType::Pyramid, 40.0).into(),
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(6.0, 3.0, 6.0));
4231 let model = interp.derive(scope, "R").unwrap();
4232 assert_eq!(model.len(), 4);
4233 for t in &model.terminals {
4235 assert!(
4236 matches!(t.face_profile, FaceProfile::Triangle { peak_offset } if (peak_offset - 0.5).abs() < 1e-9),
4237 "expected Triangle{{peak_offset=0.5}}, got {:?}",
4238 t.face_profile
4239 );
4240 }
4241 }
4242
4243 #[test]
4244 fn test_roof_slope_normals_outward() {
4245 let alpha: f64 = 30_f64.to_radians();
4250 let cos_a = alpha.cos();
4251 let sin_a = alpha.sin();
4252 let mut interp = Interpreter::new();
4253 interp.add_rule(
4254 "R",
4255 vec![ShapeOp::Roof {
4256 spec: RoofConfig::new(RoofType::Hip, 30.0).into(),
4257 cases: vec![crate::ops::RoofCase {
4258 selector: RoofFaceSelector::Slope,
4259 rule: "S".into(),
4260 }],
4261 }],
4262 );
4263 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
4264 let model = interp.derive(scope, "R").unwrap();
4265 assert_eq!(model.len(), 4);
4266 let normals: Vec<Vec3> = model
4267 .terminals
4268 .iter()
4269 .map(|t| t.scope.rotation * Vec3::Z)
4270 .collect();
4271 let expected = [
4272 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), ];
4277 for exp in &expected {
4278 assert!(
4279 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
4280 "missing outward normal {:?}; got {:?}",
4281 exp,
4282 normals
4283 );
4284 }
4285 for n in &normals {
4287 assert!(n.y > 0.0, "normal pointing downward: {:?}", n);
4288 }
4289 }
4290
4291 #[test]
4292 fn test_align_antiparallel_fallback_no_nan() {
4293 let flip_y = Quat::from_axis_angle(Vec3::Z, PI);
4297 let mut interp = Interpreter::new();
4298 interp.add_rule(
4299 "R",
4300 vec![
4301 ShapeOp::Rotate(qexpr(flip_y)),
4302 ShapeOp::Align {
4303 local_axis: Axis::Y,
4304 target: Vec3::Y,
4305 },
4306 ShapeOp::I("M".to_string()),
4307 ],
4308 );
4309 let model = interp.derive(Scope::unit(), "R").unwrap();
4310 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
4311 assert!(
4312 (world_y - Vec3::Y).length() < 1e-6,
4313 "anti-parallel Align should point Y to world up, got {:?}",
4314 world_y
4315 );
4316 let r = model.terminals[0].scope.rotation;
4318 assert!(
4319 (r.length_squared() - 1.0).abs() < 1e-9,
4320 "rotation not unit: length_sq={}",
4321 r.length_squared()
4322 );
4323 }
4324
4325 #[test]
4326 fn test_roof_invalid_angle_rejected() {
4327 let mut interp = Interpreter::new();
4328 interp.add_rule(
4329 "R",
4330 vec![ShapeOp::Roof {
4331 spec: RoofConfig::new(RoofType::Shed, 0.0).into(),
4332 cases: vec![],
4333 }],
4334 );
4335 assert!(matches!(
4336 interp.derive(Scope::unit(), "R"),
4337 Err(ShapeError::InvalidRoofAngle(_))
4338 ));
4339 }
4340}