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::model::{FaceProfile, Material, ShapeModel, Terminal, taper_to_profile};
18use crate::ops::{
19 AttachCase, AttachSelector, Axis, CompTarget, FaceSelector, OffsetCase, OffsetSelector,
20 RoofCase, RoofConfig, RoofFaceSelector, RoofType, ShapeOp, SplitSize, SplitSlot,
21};
22use crate::query::{
23 register_scope_snap_planes, scope_obb_overlaps_terminal, snap_split_boundaries,
24};
25use crate::scope::{Quat, Scope, Vec3};
26
27const MAX_DEPTH: usize = 64;
29const MAX_QUEUE: usize = 100_000;
30const MAX_TERMINALS: usize = 100_000;
31
32#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
36pub struct WeightedVariant {
37 pub weight: f64,
39 pub ops: Vec<ShapeOp>,
40}
41
42struct WorkItem {
45 scope: Scope,
46 rule: String,
47 depth: usize,
48 taper: f64,
52 face_profile_override: Option<FaceProfile>,
55 material: Option<Material>,
57}
58
59fn resolve_split_sizes(slots: &[SplitSlot], total_dim: f64) -> Result<Vec<f64>, ShapeError> {
63 if slots.is_empty() {
64 return Err(ShapeError::EmptySplit);
65 }
66 for slot in slots {
67 if !slot.size.is_valid() {
68 return match &slot.size {
69 SplitSize::Floating(v) => Err(ShapeError::InvalidFloatingSize(*v)),
70 _ => Err(ShapeError::InvalidNumericValue),
71 };
72 }
73 }
74
75 let mut fixed: Vec<Option<f64>> = Vec::with_capacity(slots.len());
76 let mut used = 0.0_f64;
77 let mut float_weight_total = 0.0_f64;
78
79 for slot in slots {
80 match slot.size {
81 SplitSize::Absolute(v) => {
82 fixed.push(Some(v));
83 used += v;
84 }
85 SplitSize::Relative(t) => {
86 let s = total_dim * t;
87 fixed.push(Some(s));
88 used += s;
89 }
90 SplitSize::Floating(w) => {
91 fixed.push(None);
92 float_weight_total += w;
93 }
94 }
95 }
96
97 if !used.is_finite() {
99 return Err(ShapeError::InvalidNumericValue);
100 }
101
102 if used > total_dim + 1e-9 {
103 return Err(ShapeError::SplitOverflow(total_dim));
104 }
105
106 let remaining = (total_dim - used).max(0.0);
107
108 if !float_weight_total.is_finite() {
110 return Err(ShapeError::InvalidNumericValue);
111 }
112
113 let mut result = Vec::with_capacity(slots.len());
114 for (i, slot) in slots.iter().enumerate() {
115 match fixed[i] {
116 Some(v) => result.push(v),
117 None => {
118 let w = match slot.size {
119 SplitSize::Floating(w) => w,
120 _ => unreachable!(),
121 };
122 if float_weight_total <= 0.0 {
123 return Err(ShapeError::NoFloatingSlots);
124 }
125 result.push(remaining * (w / float_weight_total));
128 }
129 }
130 }
131
132 Ok(result)
133}
134
135fn slice_scope(parent: &Scope, axis: Axis, offset: f64, size: f64) -> Scope {
140 let offset_vec = match axis {
141 Axis::X => Vec3::new(offset, 0.0, 0.0),
142 Axis::Y => Vec3::new(0.0, offset, 0.0),
143 Axis::Z => Vec3::new(0.0, 0.0, offset),
144 };
145
146 let child_position = parent.position + parent.rotation * offset_vec;
147
148 let child_size = match axis {
149 Axis::X => Vec3::new(size, parent.size.y, parent.size.z),
150 Axis::Y => Vec3::new(parent.size.x, size, parent.size.z),
151 Axis::Z => Vec3::new(parent.size.x, parent.size.y, size),
152 };
153
154 Scope::new(child_position, parent.rotation, child_size)
155}
156
157fn face_descs(scope_size: Vec3) -> [(FaceSelector, Vec3, Vec3, Quat); 6] {
171 let sx = scope_size.x;
172 let sy = scope_size.y;
173 let sz = scope_size.z;
174
175 [
176 (
179 FaceSelector::Bottom,
180 Vec3::new(0.0, 0.0, 0.0),
181 Vec3::new(sx, sz, 0.0),
182 Quat::from_axis_angle(Vec3::X, FRAC_PI_2),
183 ),
184 (
188 FaceSelector::Top,
189 Vec3::new(0.0, sy, sz),
190 Vec3::new(sx, sz, 0.0),
191 Quat::from_axis_angle(Vec3::X, -FRAC_PI_2),
192 ),
193 (
197 FaceSelector::Front,
198 Vec3::new(sx, 0.0, 0.0),
199 Vec3::new(sx, sy, 0.0),
200 Quat::from_axis_angle(Vec3::Y, PI),
201 ),
202 (
206 FaceSelector::Back,
207 Vec3::new(0.0, 0.0, sz),
208 Vec3::new(sx, sy, 0.0),
209 Quat::IDENTITY,
210 ),
211 (
215 FaceSelector::Left,
216 Vec3::new(0.0, 0.0, 0.0),
217 Vec3::new(sz, sy, 0.0),
218 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
219 ),
220 (
224 FaceSelector::Right,
225 Vec3::new(sx, 0.0, sz),
226 Vec3::new(sz, sy, 0.0),
227 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
228 ),
229 ]
230}
231
232fn find_face_rule(selector: FaceSelector, cases: &[crate::ops::CompFaceCase]) -> Option<&str> {
234 for case in cases {
235 if case.selector == selector {
236 return Some(&case.rule);
237 }
238 }
239 let is_side = matches!(
240 selector,
241 FaceSelector::Front | FaceSelector::Back | FaceSelector::Left | FaceSelector::Right
242 );
243 if is_side {
244 for case in cases {
245 if case.selector == FaceSelector::Side {
246 return Some(&case.rule);
247 }
248 }
249 }
250 for case in cases {
251 if case.selector == FaceSelector::All {
252 return Some(&case.rule);
253 }
254 }
255 None
256}
257
258fn axis_vec(axis: Axis) -> Vec3 {
260 match axis {
261 Axis::X => Vec3::X,
262 Axis::Y => Vec3::Y,
263 Axis::Z => Vec3::Z,
264 }
265}
266
267fn find_offset_rule(selector: OffsetSelector, cases: &[OffsetCase]) -> Option<&str> {
269 for c in cases {
270 if c.selector == selector {
271 return Some(&c.rule);
272 }
273 }
274 for c in cases {
275 if c.selector == OffsetSelector::All {
276 return Some(&c.rule);
277 }
278 }
279 None
280}
281
282fn find_roof_rule(selector: RoofFaceSelector, cases: &[RoofCase]) -> Option<&str> {
284 for c in cases {
285 if c.selector == selector {
286 return Some(&c.rule);
287 }
288 }
289 for c in cases {
290 if c.selector == RoofFaceSelector::All {
291 return Some(&c.rule);
292 }
293 }
294 None
295}
296
297fn find_attach_rule(selector: AttachSelector, cases: &[AttachCase]) -> Option<&str> {
299 for c in cases {
300 if c.selector == selector {
301 return Some(&c.rule);
302 }
303 }
304 for c in cases {
305 if c.selector == AttachSelector::All {
306 return Some(&c.rule);
307 }
308 }
309 None
310}
311
312fn roof_slope_rotations(alpha: f64) -> (Quat, Quat, Quat, Quat) {
325 let front_rot =
328 Quat::from_axis_angle(Vec3::X, FRAC_PI_2 - alpha) * Quat::from_axis_angle(Vec3::Y, PI);
329 let back_rot = Quat::from_axis_angle(Vec3::X, alpha - FRAC_PI_2);
332 let left_rot = Quat::from_axis_angle(Vec3::Z, alpha - FRAC_PI_2)
335 * Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2);
336 let right_rot = Quat::from_axis_angle(Vec3::Z, FRAC_PI_2 - alpha)
339 * Quat::from_axis_angle(Vec3::Y, FRAC_PI_2);
340 (front_rot, back_rot, left_rot, right_rot)
341}
342
343fn slope_to_wall_rot(slope_rot: Quat) -> Option<Quat> {
352 let eave_x = slope_rot * Vec3::X;
353 let slope_z = slope_rot * Vec3::Z;
354 let mut wall_z = Vec3::new(slope_z.x, 0.0, slope_z.z);
355 if wall_z.length_squared() < 1e-12 {
356 return None;
357 }
358 wall_z = wall_z.normalize();
359 let mut wall_x = Vec3::new(eave_x.x, 0.0, eave_x.z);
360 if wall_x.length_squared() < 1e-12 {
361 return None;
362 }
363 wall_x = wall_x.normalize();
364 let wall_y = wall_z.cross(wall_x);
365 let mat = glam::DMat3::from_cols(wall_x, wall_y, wall_z);
366 Some(Quat::from_mat3(&mat).normalize())
367}
368
369#[allow(clippy::too_many_arguments)]
378fn apply_roof(
379 config: &RoofConfig,
380 cases: &[RoofCase],
381 scope: &Scope,
382 depth: usize,
383 material: &Option<Material>,
384 queue: &mut VecDeque<WorkItem>,
385 model: &mut ShapeModel,
386 max_terminals: usize,
387) -> Result<(), ShapeError> {
388 if !config.pitch.is_finite() || config.pitch <= 0.0 || config.pitch >= 90.0 {
389 return Err(ShapeError::InvalidRoofAngle(config.pitch));
390 }
391 if !config.overhang.is_finite() || config.overhang < 0.0 {
392 return Err(ShapeError::InvalidNumericValue);
393 }
394
395 let sx = scope.size.x;
396 let sz = scope.size.z;
397 let o = config.overhang;
398 let alpha = config.pitch.to_radians();
399 let cos_a = alpha.cos();
400 let tan_a = alpha.tan();
401 let (front_rot, back_rot, left_rot, right_rot) = roof_slope_rotations(alpha);
402 let y_anchor = -o * tan_a;
404 let fb_len = (sz / 2.0 + o) / cos_a;
406 let lr_len = (sx / 2.0 + o) / cos_a;
407 let h = (sz / 2.0) * tan_a;
409
410 if !fb_len.is_finite() || !lr_len.is_finite() || !h.is_finite() {
411 return Err(ShapeError::InvalidNumericValue);
412 }
413
414 type Panel = (Vec3, Vec3, Quat, RoofFaceSelector, FaceProfile);
416
417 let mut panels: Vec<Panel> = match config.roof_type {
418 RoofType::Flat => vec![(
421 Vec3::new(0.0, 0.0, sz),
422 Vec3::new(sx, sz, 0.0),
423 Quat::from_axis_angle(Vec3::X, -FRAC_PI_2),
424 RoofFaceSelector::Slope,
425 FaceProfile::Rectangle,
426 )],
427
428 RoofType::Shed => {
431 let shed_h = sz * tan_a;
432 vec![
433 (
434 Vec3::new(sx + o, y_anchor, -o),
435 Vec3::new(sx + 2.0 * o, (sz + 2.0 * o) / cos_a, 0.0),
436 front_rot,
437 RoofFaceSelector::Slope,
438 FaceProfile::Rectangle,
439 ),
440 (
441 Vec3::new(0.0, 0.0, 0.0),
442 Vec3::new(sz, shed_h, 0.0),
443 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
444 RoofFaceSelector::GableEnd,
445 FaceProfile::Triangle { peak_offset: 1.0 },
446 ),
447 (
448 Vec3::new(sx, 0.0, sz),
449 Vec3::new(sz, shed_h, 0.0),
450 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
451 RoofFaceSelector::GableEnd,
452 FaceProfile::Triangle { peak_offset: 0.0 },
453 ),
454 ]
455 }
456
457 RoofType::Gable | RoofType::OpenGable | RoofType::BoxGable => {
459 let (slope_len, eave_len, ridge_h) = if sx >= sz {
460 ((sz / 2.0 + o) / cos_a, sx + 2.0 * o, (sz / 2.0) * tan_a)
461 } else {
462 ((sx / 2.0 + o) / cos_a, sz + 2.0 * o, (sx / 2.0) * tan_a)
463 };
464
465 let mut panels = if sx >= sz {
466 vec![
467 (
468 Vec3::new(sx + o, y_anchor, -o),
469 Vec3::new(eave_len, slope_len, 0.0),
470 front_rot,
471 RoofFaceSelector::Slope,
472 FaceProfile::Rectangle,
473 ),
474 (
475 Vec3::new(-o, y_anchor, sz + o),
476 Vec3::new(eave_len, slope_len, 0.0),
477 back_rot,
478 RoofFaceSelector::Slope,
479 FaceProfile::Rectangle,
480 ),
481 ]
482 } else {
483 vec![
484 (
485 Vec3::new(-o, y_anchor, -o),
486 Vec3::new(eave_len, slope_len, 0.0),
487 left_rot,
488 RoofFaceSelector::Slope,
489 FaceProfile::Rectangle,
490 ),
491 (
492 Vec3::new(sx + o, y_anchor, sz + o),
493 Vec3::new(eave_len, slope_len, 0.0),
494 right_rot,
495 RoofFaceSelector::Slope,
496 FaceProfile::Rectangle,
497 ),
498 ]
499 };
500
501 if config.roof_type != RoofType::OpenGable {
502 let profile = if config.roof_type == RoofType::BoxGable {
503 FaceProfile::Rectangle
504 } else {
505 FaceProfile::Triangle { peak_offset: 0.5 }
506 };
507
508 if sx >= sz {
509 panels.extend(vec![
510 (
511 Vec3::new(0.0, 0.0, 0.0),
512 Vec3::new(sz, ridge_h, 0.0),
513 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
514 RoofFaceSelector::GableEnd,
515 profile.clone(),
516 ),
517 (
518 Vec3::new(sx, 0.0, sz),
519 Vec3::new(sz, ridge_h, 0.0),
520 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
521 RoofFaceSelector::GableEnd,
522 profile,
523 ),
524 ]);
525 } else {
526 panels.extend(vec![
527 (
528 Vec3::new(sx, 0.0, 0.0),
529 Vec3::new(sx, ridge_h, 0.0),
530 Quat::from_axis_angle(Vec3::Y, PI),
531 RoofFaceSelector::GableEnd,
532 profile.clone(),
533 ),
534 (
535 Vec3::new(0.0, 0.0, sz),
536 Vec3::new(sx, ridge_h, 0.0),
537 Quat::IDENTITY,
538 RoofFaceSelector::GableEnd,
539 profile,
540 ),
541 ]);
542 }
543 }
544 panels
545 }
546
547 RoofType::Pyramid | RoofType::PyramidHip | RoofType::Hip => {
551 let eave_w = sx + 2.0 * o;
552 let eave_d = sz + 2.0 * o;
553 let max_run = sx.min(sz) / 2.0 + o;
554 let slope_len = max_run / cos_a;
555
556 let (fb_profile, lr_profile) = if sx > sz + 1e-5 {
557 let top_w = (sx - sz) / eave_w;
558 let off_x = (sz / 2.0 + o) / eave_w;
559 (
560 FaceProfile::Trapezoid {
561 top_width: top_w,
562 offset_x: off_x,
563 },
564 FaceProfile::Triangle { peak_offset: 0.5 },
565 )
566 } else if sz > sx + 1e-5 {
567 let top_w = (sz - sx) / eave_d;
568 let off_x = (sx / 2.0 + o) / eave_d;
569 (
570 FaceProfile::Triangle { peak_offset: 0.5 },
571 FaceProfile::Trapezoid {
572 top_width: top_w,
573 offset_x: off_x,
574 },
575 )
576 } else {
577 (
578 FaceProfile::Triangle { peak_offset: 0.5 },
579 FaceProfile::Triangle { peak_offset: 0.5 },
580 )
581 };
582
583 vec![
584 (
585 Vec3::new(sx + o, y_anchor, -o),
586 Vec3::new(eave_w, slope_len, 0.0),
587 front_rot,
588 RoofFaceSelector::Slope,
589 fb_profile.clone(),
590 ),
591 (
592 Vec3::new(-o, y_anchor, sz + o),
593 Vec3::new(eave_w, slope_len, 0.0),
594 back_rot,
595 RoofFaceSelector::Slope,
596 fb_profile,
597 ),
598 (
599 Vec3::new(-o, y_anchor, -o),
600 Vec3::new(eave_d, slope_len, 0.0),
601 left_rot,
602 RoofFaceSelector::Slope,
603 lr_profile.clone(),
604 ),
605 (
606 Vec3::new(sx + o, y_anchor, sz + o),
607 Vec3::new(eave_d, slope_len, 0.0),
608 right_rot,
609 RoofFaceSelector::Slope,
610 lr_profile,
611 ),
612 ]
613 }
614
615 RoofType::Butterfly => {
619 let y_valley = y_anchor - (sz / 2.0 + o) * tan_a;
621 if !y_valley.is_finite() {
622 return Err(ShapeError::InvalidNumericValue);
623 }
624 vec![
625 (
627 Vec3::new(-o, y_valley, sz / 2.0),
628 Vec3::new(sx + 2.0 * o, fb_len, 0.0),
629 back_rot,
630 RoofFaceSelector::ValleySlope,
631 FaceProfile::Rectangle,
632 ),
633 (
635 Vec3::new(sx + o, y_valley, sz / 2.0),
636 Vec3::new(sx + 2.0 * o, fb_len, 0.0),
637 front_rot,
638 RoofFaceSelector::ValleySlope,
639 FaceProfile::Rectangle,
640 ),
641 ]
642 }
643
644 RoofType::MShaped => {
648 let quarter = sz / 4.0;
649 let h_m = quarter * tan_a;
650 if !h_m.is_finite() {
651 return Err(ShapeError::InvalidNumericValue);
652 }
653 let slope_m = quarter / cos_a;
654 let y_valley = y_anchor - h_m; vec![
656 (
658 Vec3::new(sx + o, y_anchor, -o),
659 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
660 front_rot,
661 RoofFaceSelector::OuterSlope,
662 FaceProfile::Rectangle,
663 ),
664 (
666 Vec3::new(-o, y_valley, sz / 2.0),
667 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
668 back_rot,
669 RoofFaceSelector::InnerSlope,
670 FaceProfile::Rectangle,
671 ),
672 (
674 Vec3::new(sx + o, y_valley, sz / 2.0),
675 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
676 front_rot,
677 RoofFaceSelector::InnerSlope,
678 FaceProfile::Rectangle,
679 ),
680 (
682 Vec3::new(-o, y_anchor, sz + o),
683 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
684 back_rot,
685 RoofFaceSelector::OuterSlope,
686 FaceProfile::Rectangle,
687 ),
688 ]
689 }
690
691 RoofType::Gambrel => {
694 let alpha2 = config.secondary_pitch_or_default().to_radians();
695 if !alpha2.is_finite() || alpha2 <= 0.0 || alpha2 >= FRAC_PI_2 {
696 return Err(ShapeError::InvalidNumericValue);
697 }
698 let cos_a2 = alpha2.cos();
699 let tan_a2 = alpha2.tan();
700 let tier = config.tier_height_or(0.5).clamp(0.01, 0.99);
701 let (ufr, ubr, ulr, urr) = roof_slope_rotations(alpha2);
702
703 if sx >= sz {
704 let run_z = sz / 2.0 + o;
705 let break_run = (tier * run_z).clamp(o, run_z - 1e-3);
706 let h_break = break_run * tan_a;
707 if !h_break.is_finite() {
708 return Err(ShapeError::InvalidNumericValue);
709 }
710 let lower_slope = break_run / cos_a;
711 let upper_run = run_z - break_run;
712 let upper_slope = upper_run / cos_a2;
713 let upper_h = upper_run * tan_a2;
714 let y_break = y_anchor + h_break;
715 let eave_w = sx + 2.0 * o;
716 let wall_y_break = h_break - o * tan_a;
717 let wall_break_run = break_run - o;
718 let mid_w = (sz - 2.0 * wall_break_run).max(0.0);
719
720 let lower_gable_profile = if mid_w > 1e-9 {
721 FaceProfile::Trapezoid {
722 top_width: mid_w / sz,
723 offset_x: wall_break_run / sz,
724 }
725 } else {
726 FaceProfile::Triangle { peak_offset: 0.5 }
727 };
728
729 vec![
730 (
731 Vec3::new(sx + o, y_anchor, -o),
732 Vec3::new(eave_w, lower_slope, 0.0),
733 front_rot,
734 RoofFaceSelector::LowerSlope,
735 FaceProfile::Rectangle,
736 ),
737 (
738 Vec3::new(-o, y_anchor, sz + o),
739 Vec3::new(eave_w, lower_slope, 0.0),
740 back_rot,
741 RoofFaceSelector::LowerSlope,
742 FaceProfile::Rectangle,
743 ),
744 (
745 Vec3::new(sx + o, y_break, -o + break_run),
746 Vec3::new(eave_w, upper_slope, 0.0),
747 ufr,
748 RoofFaceSelector::UpperSlope,
749 FaceProfile::Rectangle,
750 ),
751 (
752 Vec3::new(-o, y_break, sz + o - break_run),
753 Vec3::new(eave_w, upper_slope, 0.0),
754 ubr,
755 RoofFaceSelector::UpperSlope,
756 FaceProfile::Rectangle,
757 ),
758 (
759 Vec3::new(0.0, 0.0, 0.0),
760 Vec3::new(sz, wall_y_break, 0.0),
761 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
762 RoofFaceSelector::GableEnd,
763 lower_gable_profile.clone(),
764 ),
765 (
766 Vec3::new(sx, 0.0, sz),
767 Vec3::new(sz, wall_y_break, 0.0),
768 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
769 RoofFaceSelector::GableEnd,
770 lower_gable_profile,
771 ),
772 (
773 Vec3::new(0.0, wall_y_break, wall_break_run),
774 Vec3::new(mid_w, upper_h, 0.0),
775 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
776 RoofFaceSelector::GableEnd,
777 FaceProfile::Triangle { peak_offset: 0.5 },
778 ),
779 (
780 Vec3::new(sx, wall_y_break, sz - wall_break_run),
781 Vec3::new(mid_w, upper_h, 0.0),
782 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
783 RoofFaceSelector::GableEnd,
784 FaceProfile::Triangle { peak_offset: 0.5 },
785 ),
786 ]
787 } else {
788 let run_x = sx / 2.0 + o;
789 let break_run = (tier * run_x).clamp(o, run_x - 1e-3);
790 let h_break = break_run * tan_a;
791 if !h_break.is_finite() {
792 return Err(ShapeError::InvalidNumericValue);
793 }
794 let lower_slope = break_run / cos_a;
795 let upper_run = run_x - break_run;
796 let upper_slope = upper_run / cos_a2;
797 let upper_h = upper_run * tan_a2;
798 let y_break = y_anchor + h_break;
799 let eave_d = sz + 2.0 * o;
800 let wall_y_break = h_break - o * tan_a;
801 let wall_break_run = break_run - o;
802 let mid_d = (sx - 2.0 * wall_break_run).max(0.0);
803
804 let lower_gable_profile = if mid_d > 1e-9 {
805 FaceProfile::Trapezoid {
806 top_width: mid_d / sx,
807 offset_x: wall_break_run / sx,
808 }
809 } else {
810 FaceProfile::Triangle { peak_offset: 0.5 }
811 };
812
813 vec![
814 (
815 Vec3::new(-o, y_anchor, -o),
816 Vec3::new(eave_d, lower_slope, 0.0),
817 left_rot,
818 RoofFaceSelector::LowerSlope,
819 FaceProfile::Rectangle,
820 ),
821 (
822 Vec3::new(sx + o, y_anchor, sz + o),
823 Vec3::new(eave_d, lower_slope, 0.0),
824 right_rot,
825 RoofFaceSelector::LowerSlope,
826 FaceProfile::Rectangle,
827 ),
828 (
829 Vec3::new(-o + break_run, y_break, -o),
830 Vec3::new(eave_d, upper_slope, 0.0),
831 ulr,
832 RoofFaceSelector::UpperSlope,
833 FaceProfile::Rectangle,
834 ),
835 (
836 Vec3::new(sx + o - break_run, y_break, sz + o),
837 Vec3::new(eave_d, upper_slope, 0.0),
838 urr,
839 RoofFaceSelector::UpperSlope,
840 FaceProfile::Rectangle,
841 ),
842 (
843 Vec3::new(sx, 0.0, 0.0),
844 Vec3::new(sx, wall_y_break, 0.0),
845 Quat::from_axis_angle(Vec3::Y, PI),
846 RoofFaceSelector::GableEnd,
847 lower_gable_profile.clone(),
848 ),
849 (
850 Vec3::new(0.0, 0.0, sz),
851 Vec3::new(sx, wall_y_break, 0.0),
852 Quat::IDENTITY,
853 RoofFaceSelector::GableEnd,
854 lower_gable_profile,
855 ),
856 (
857 Vec3::new(sx - wall_break_run, wall_y_break, 0.0),
858 Vec3::new(mid_d, upper_h, 0.0),
859 Quat::from_axis_angle(Vec3::Y, PI),
860 RoofFaceSelector::GableEnd,
861 FaceProfile::Triangle { peak_offset: 0.5 },
862 ),
863 (
864 Vec3::new(wall_break_run, wall_y_break, sz),
865 Vec3::new(mid_d, upper_h, 0.0),
866 Quat::IDENTITY,
867 RoofFaceSelector::GableEnd,
868 FaceProfile::Triangle { peak_offset: 0.5 },
869 ),
870 ]
871 }
872 }
873
874 RoofType::Mansard => {
877 let alpha2 = config.secondary_pitch_or_default().to_radians();
878 if !alpha2.is_finite() || alpha2 <= 0.0 || alpha2 >= FRAC_PI_2 {
879 return Err(ShapeError::InvalidNumericValue);
880 }
881 let cos_a2 = alpha2.cos();
882 let tier = config.tier_height_or(0.5).clamp(0.01, 0.99);
883
884 let max_run = sx.min(sz) / 2.0 + o;
885 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
886 let h_break = break_run * tan_a;
887
888 if !h_break.is_finite() {
889 return Err(ShapeError::InvalidNumericValue);
890 }
891
892 let lower_slope = break_run / cos_a;
893 let y_break = y_anchor + h_break;
894
895 let eave_w = sx + 2.0 * o;
896 let eave_d = sz + 2.0 * o;
897
898 let mid_w = (eave_w - 2.0 * break_run).max(0.0);
899 let mid_d = (eave_d - 2.0 * break_run).max(0.0);
900
901 let lower_fb_profile = if mid_w > 1e-9 {
902 FaceProfile::Trapezoid {
903 top_width: mid_w / eave_w,
904 offset_x: break_run / eave_w,
905 }
906 } else {
907 FaceProfile::Triangle { peak_offset: 0.5 }
908 };
909
910 let lower_lr_profile = if mid_d > 1e-9 {
911 FaceProfile::Trapezoid {
912 top_width: mid_d / eave_d,
913 offset_x: break_run / eave_d,
914 }
915 } else {
916 FaceProfile::Triangle { peak_offset: 0.5 }
917 };
918
919 let (ufr, ubr, ulr, urr) = roof_slope_rotations(alpha2);
920
921 let upper_run = mid_w.min(mid_d) / 2.0;
922 let upper_slope = upper_run / cos_a2;
923
924 let top_w = (mid_w - 2.0 * upper_run).max(0.0);
925 let top_d = (mid_d - 2.0 * upper_run).max(0.0);
926
927 let upper_fb_profile = if top_w > 1e-9 {
928 FaceProfile::Trapezoid {
929 top_width: top_w / mid_w,
930 offset_x: upper_run / mid_w,
931 }
932 } else {
933 FaceProfile::Triangle { peak_offset: 0.5 }
934 };
935
936 let upper_lr_profile = if top_d > 1e-9 {
937 FaceProfile::Trapezoid {
938 top_width: top_d / mid_d,
939 offset_x: upper_run / mid_d,
940 }
941 } else {
942 FaceProfile::Triangle { peak_offset: 0.5 }
943 };
944
945 vec![
946 (
948 Vec3::new(sx + o, y_anchor, -o),
949 Vec3::new(eave_w, lower_slope, 0.0),
950 front_rot,
951 RoofFaceSelector::LowerSlope,
952 lower_fb_profile.clone(),
953 ),
954 (
955 Vec3::new(-o, y_anchor, sz + o),
956 Vec3::new(eave_w, lower_slope, 0.0),
957 back_rot,
958 RoofFaceSelector::LowerSlope,
959 lower_fb_profile,
960 ),
961 (
962 Vec3::new(-o, y_anchor, -o),
963 Vec3::new(eave_d, lower_slope, 0.0),
964 left_rot,
965 RoofFaceSelector::LowerSlope,
966 lower_lr_profile.clone(),
967 ),
968 (
969 Vec3::new(sx + o, y_anchor, sz + o),
970 Vec3::new(eave_d, lower_slope, 0.0),
971 right_rot,
972 RoofFaceSelector::LowerSlope,
973 lower_lr_profile,
974 ),
975 (
977 Vec3::new(sx + o - break_run, y_break, -o + break_run),
978 Vec3::new(mid_w, upper_slope, 0.0),
979 ufr,
980 RoofFaceSelector::UpperSlope,
981 upper_fb_profile.clone(),
982 ),
983 (
984 Vec3::new(-o + break_run, y_break, sz + o - break_run),
985 Vec3::new(mid_w, upper_slope, 0.0),
986 ubr,
987 RoofFaceSelector::UpperSlope,
988 upper_fb_profile,
989 ),
990 (
991 Vec3::new(-o + break_run, y_break, -o + break_run),
992 Vec3::new(mid_d, upper_slope, 0.0),
993 ulr,
994 RoofFaceSelector::UpperSlope,
995 upper_lr_profile.clone(),
996 ),
997 (
998 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
999 Vec3::new(mid_d, upper_slope, 0.0),
1000 urr,
1001 RoofFaceSelector::UpperSlope,
1002 upper_lr_profile,
1003 ),
1004 ]
1005 }
1006
1007 RoofType::Saltbox => {
1011 let (orient_z, _width, depth) = if sx >= sz {
1012 (true, sx, sz)
1013 } else {
1014 (false, sz, sx)
1015 };
1016 let ridge_d = depth * config.ridge_offset;
1017 if !ridge_d.is_finite() || ridge_d <= 0.0 || ridge_d >= depth {
1018 return Err(ShapeError::InvalidNumericValue);
1019 }
1020 let h_s = ridge_d * tan_a;
1021 let back_depth = depth - ridge_d;
1022 let alpha_back = ((h_s) / (back_depth + o)).atan();
1023 let cos_ab = alpha_back.cos();
1024 if !h_s.is_finite() || !alpha_back.is_finite() || cos_ab < 1e-9 {
1025 return Err(ShapeError::InvalidNumericValue);
1026 }
1027 let front_len = (ridge_d + o) / cos_a;
1028 let back_len = (back_depth + o) / cos_ab;
1029 if !front_len.is_finite() || !back_len.is_finite() {
1030 return Err(ShapeError::InvalidNumericValue);
1031 }
1032 let (_, back_rot_s, _, right_rot_s) = roof_slope_rotations(alpha_back);
1033 let peak_fwd = ridge_d / depth;
1034
1035 if orient_z {
1036 vec![
1037 (
1038 Vec3::new(sx + o, y_anchor, -o),
1039 Vec3::new(sx + 2.0 * o, front_len, 0.0),
1040 front_rot,
1041 RoofFaceSelector::Slope,
1042 FaceProfile::Rectangle,
1043 ),
1044 (
1045 Vec3::new(-o, y_anchor, sz + o),
1046 Vec3::new(sx + 2.0 * o, back_len, 0.0),
1047 back_rot_s,
1048 RoofFaceSelector::Slope,
1049 FaceProfile::Rectangle,
1050 ),
1051 (
1052 Vec3::new(0.0, 0.0, 0.0),
1053 Vec3::new(sz, h_s, 0.0),
1054 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1055 RoofFaceSelector::GableEnd,
1056 FaceProfile::Triangle {
1057 peak_offset: peak_fwd,
1058 },
1059 ),
1060 (
1061 Vec3::new(sx, 0.0, sz),
1062 Vec3::new(sz, h_s, 0.0),
1063 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1064 RoofFaceSelector::GableEnd,
1065 FaceProfile::Triangle {
1066 peak_offset: 1.0 - peak_fwd,
1067 },
1068 ),
1069 ]
1070 } else {
1071 vec![
1072 (
1073 Vec3::new(-o, y_anchor, -o),
1074 Vec3::new(sz + 2.0 * o, front_len, 0.0),
1075 left_rot,
1076 RoofFaceSelector::Slope,
1077 FaceProfile::Rectangle,
1078 ),
1079 (
1080 Vec3::new(sx + o, y_anchor, sz + o),
1081 Vec3::new(sz + 2.0 * o, back_len, 0.0),
1082 right_rot_s,
1083 RoofFaceSelector::Slope,
1084 FaceProfile::Rectangle,
1085 ),
1086 (
1087 Vec3::new(sx, 0.0, 0.0),
1088 Vec3::new(sx, h_s, 0.0),
1089 Quat::from_axis_angle(Vec3::Y, PI),
1090 RoofFaceSelector::GableEnd,
1091 FaceProfile::Triangle {
1092 peak_offset: 1.0 - peak_fwd,
1093 },
1094 ),
1095 (
1096 Vec3::new(0.0, 0.0, sz),
1097 Vec3::new(sx, h_s, 0.0),
1098 Quat::IDENTITY,
1099 RoofFaceSelector::GableEnd,
1100 FaceProfile::Triangle {
1101 peak_offset: peak_fwd,
1102 },
1103 ),
1104 ]
1105 }
1106 }
1107
1108 RoofType::Jerkinhead => {
1112 let tier = config.tier_height_or(0.25).clamp(0.01, 0.99);
1113 let orient_z = sx >= sz;
1114 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1115
1116 let max_clip = (width / 2.0 + o).min(depth / 2.0);
1117 let clip_run = (tier * depth / 2.0).clamp(0.0, max_clip - 1e-3);
1118
1119 let eave_w = width + 2.0 * o;
1120 let top_w = (eave_w - 2.0 * clip_run).max(0.0);
1121
1122 let slope_len = (depth / 2.0 + o) / cos_a;
1123
1124 let slope_profile = if top_w > 1e-9 {
1125 FaceProfile::Trapezoid {
1126 top_width: top_w / eave_w,
1127 offset_x: clip_run / eave_w,
1128 }
1129 } else {
1130 FaceProfile::Triangle { peak_offset: 0.5 }
1131 };
1132
1133 let true_h = (depth / 2.0) * tan_a;
1134 let wall_h = (true_h - clip_run * tan_a).max(0.0);
1135 let wall_profile = if clip_run > 1e-9 {
1136 FaceProfile::Trapezoid {
1137 top_width: (2.0 * clip_run) / depth,
1138 offset_x: (depth / 2.0 - clip_run) / depth,
1139 }
1140 } else {
1141 FaceProfile::Triangle { peak_offset: 0.5 }
1142 };
1143
1144 let hip_base_w = 2.0 * clip_run + 2.0 * o;
1145 let hip_slope_len = (clip_run + o) / cos_a;
1146 let hip_profile = FaceProfile::Triangle { peak_offset: 0.5 };
1147
1148 if orient_z {
1149 let left_hip_origin = Vec3::new(-o, wall_h - o * tan_a, sz / 2.0 - clip_run - o);
1150 let right_hip_origin =
1151 Vec3::new(sx + o, wall_h - o * tan_a, sz / 2.0 + clip_run + o);
1152 vec![
1153 (
1154 Vec3::new(sx + o, y_anchor, -o),
1155 Vec3::new(eave_w, slope_len, 0.0),
1156 front_rot,
1157 RoofFaceSelector::Slope,
1158 slope_profile.clone(),
1159 ),
1160 (
1161 Vec3::new(-o, y_anchor, sz + o),
1162 Vec3::new(eave_w, slope_len, 0.0),
1163 back_rot,
1164 RoofFaceSelector::Slope,
1165 slope_profile,
1166 ),
1167 (
1168 Vec3::new(0.0, 0.0, 0.0),
1169 Vec3::new(sz, wall_h, 0.0),
1170 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1171 RoofFaceSelector::GableEnd,
1172 wall_profile.clone(),
1173 ),
1174 (
1175 Vec3::new(sx, 0.0, sz),
1176 Vec3::new(sz, wall_h, 0.0),
1177 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1178 RoofFaceSelector::GableEnd,
1179 wall_profile,
1180 ),
1181 (
1182 left_hip_origin,
1183 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1184 left_rot,
1185 RoofFaceSelector::HipEnd,
1186 hip_profile.clone(),
1187 ),
1188 (
1189 right_hip_origin,
1190 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1191 right_rot,
1192 RoofFaceSelector::HipEnd,
1193 hip_profile,
1194 ),
1195 ]
1196 } else {
1197 let front_hip_origin = Vec3::new(sx / 2.0 + clip_run + o, wall_h - o * tan_a, -o);
1198 let back_hip_origin =
1199 Vec3::new(sx / 2.0 - clip_run - o, wall_h - o * tan_a, sz + o);
1200 vec![
1201 (
1202 Vec3::new(-o, y_anchor, -o),
1203 Vec3::new(eave_w, slope_len, 0.0),
1204 left_rot,
1205 RoofFaceSelector::Slope,
1206 slope_profile.clone(),
1207 ),
1208 (
1209 Vec3::new(sx + o, y_anchor, sz + o),
1210 Vec3::new(eave_w, slope_len, 0.0),
1211 right_rot,
1212 RoofFaceSelector::Slope,
1213 slope_profile,
1214 ),
1215 (
1216 Vec3::new(sx, 0.0, 0.0),
1217 Vec3::new(sx, wall_h, 0.0),
1218 Quat::from_axis_angle(Vec3::Y, PI),
1219 RoofFaceSelector::GableEnd,
1220 wall_profile.clone(),
1221 ),
1222 (
1223 Vec3::new(0.0, 0.0, sz),
1224 Vec3::new(sx, wall_h, 0.0),
1225 Quat::IDENTITY,
1226 RoofFaceSelector::GableEnd,
1227 wall_profile,
1228 ),
1229 (
1230 front_hip_origin,
1231 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1232 front_rot,
1233 RoofFaceSelector::HipEnd,
1234 hip_profile.clone(),
1235 ),
1236 (
1237 back_hip_origin,
1238 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1239 back_rot,
1240 RoofFaceSelector::HipEnd,
1241 hip_profile,
1242 ),
1243 ]
1244 }
1245 }
1246
1247 RoofType::DutchGable => {
1251 let tier = config.tier_height_or(0.7).clamp(0.01, 0.99);
1252 let orient_z = sx >= sz;
1253 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1254
1255 let max_run = width.min(depth) / 2.0 + o;
1256 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
1257
1258 if !break_run.is_finite() {
1259 return Err(ShapeError::InvalidNumericValue);
1260 }
1261
1262 let y_break = y_anchor + break_run * tan_a;
1263 let eave_w = width + 2.0 * o;
1264 let eave_d = depth + 2.0 * o;
1265
1266 let top_w = (eave_w - 2.0 * break_run).max(0.0);
1267 let top_d = (eave_d - 2.0 * break_run).max(0.0);
1268
1269 let lower_slope_len = break_run / cos_a;
1270
1271 let fb_profile = if top_w > 1e-9 {
1272 FaceProfile::Trapezoid {
1273 top_width: top_w / eave_w,
1274 offset_x: break_run / eave_w,
1275 }
1276 } else {
1277 FaceProfile::Triangle { peak_offset: 0.5 }
1278 };
1279
1280 let lr_profile = if top_d > 1e-9 {
1281 FaceProfile::Trapezoid {
1282 top_width: top_d / eave_d,
1283 offset_x: break_run / eave_d,
1284 }
1285 } else {
1286 FaceProfile::Triangle { peak_offset: 0.5 }
1287 };
1288
1289 let upper_run = (depth / 2.0 + o) - break_run;
1290 let upper_slope_len = upper_run / cos_a;
1291 let upper_h = upper_run * tan_a;
1292
1293 if orient_z {
1294 vec![
1295 (
1297 Vec3::new(sx + o, y_anchor, -o),
1298 Vec3::new(eave_w, lower_slope_len, 0.0),
1299 front_rot,
1300 RoofFaceSelector::Slope,
1301 fb_profile.clone(),
1302 ),
1303 (
1304 Vec3::new(-o, y_anchor, sz + o),
1305 Vec3::new(eave_w, lower_slope_len, 0.0),
1306 back_rot,
1307 RoofFaceSelector::Slope,
1308 fb_profile,
1309 ),
1310 (
1312 Vec3::new(-o, y_anchor, -o),
1313 Vec3::new(eave_d, lower_slope_len, 0.0),
1314 left_rot,
1315 RoofFaceSelector::Slope,
1316 lr_profile.clone(),
1317 ),
1318 (
1319 Vec3::new(sx + o, y_anchor, sz + o),
1320 Vec3::new(eave_d, lower_slope_len, 0.0),
1321 right_rot,
1322 RoofFaceSelector::Slope,
1323 lr_profile,
1324 ),
1325 (
1327 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1328 Vec3::new(top_w, upper_slope_len, 0.0),
1329 front_rot,
1330 RoofFaceSelector::Slope,
1331 FaceProfile::Rectangle,
1332 ),
1333 (
1334 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1335 Vec3::new(top_w, upper_slope_len, 0.0),
1336 back_rot,
1337 RoofFaceSelector::Slope,
1338 FaceProfile::Rectangle,
1339 ),
1340 (
1342 Vec3::new(-o + break_run, y_break, -o + break_run),
1343 Vec3::new(top_d, upper_h, 0.0),
1344 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1345 RoofFaceSelector::GableEnd,
1346 FaceProfile::Triangle { peak_offset: 0.5 },
1347 ),
1348 (
1349 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1350 Vec3::new(top_d, upper_h, 0.0),
1351 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1352 RoofFaceSelector::GableEnd,
1353 FaceProfile::Triangle { peak_offset: 0.5 },
1354 ),
1355 ]
1356 } else {
1357 vec![
1358 (
1360 Vec3::new(-o, y_anchor, -o),
1361 Vec3::new(eave_w, lower_slope_len, 0.0),
1362 left_rot,
1363 RoofFaceSelector::Slope,
1364 fb_profile.clone(),
1365 ),
1366 (
1367 Vec3::new(sx + o, y_anchor, sz + o),
1368 Vec3::new(eave_w, lower_slope_len, 0.0),
1369 right_rot,
1370 RoofFaceSelector::Slope,
1371 fb_profile,
1372 ),
1373 (
1375 Vec3::new(sx + o, y_anchor, -o),
1376 Vec3::new(eave_d, lower_slope_len, 0.0),
1377 front_rot,
1378 RoofFaceSelector::Slope,
1379 lr_profile.clone(),
1380 ),
1381 (
1382 Vec3::new(-o, y_anchor, sz + o),
1383 Vec3::new(eave_d, lower_slope_len, 0.0),
1384 back_rot,
1385 RoofFaceSelector::Slope,
1386 lr_profile,
1387 ),
1388 (
1390 Vec3::new(-o + break_run, y_break, -o + break_run),
1391 Vec3::new(top_w, upper_slope_len, 0.0),
1392 left_rot,
1393 RoofFaceSelector::Slope,
1394 FaceProfile::Rectangle,
1395 ),
1396 (
1397 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1398 Vec3::new(top_w, upper_slope_len, 0.0),
1399 right_rot,
1400 RoofFaceSelector::Slope,
1401 FaceProfile::Rectangle,
1402 ),
1403 (
1405 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1406 Vec3::new(top_d, upper_h, 0.0),
1407 Quat::from_axis_angle(Vec3::Y, PI),
1408 RoofFaceSelector::GableEnd,
1409 FaceProfile::Triangle { peak_offset: 0.5 },
1410 ),
1411 (
1412 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1413 Vec3::new(top_d, upper_h, 0.0),
1414 Quat::IDENTITY,
1415 RoofFaceSelector::GableEnd,
1416 FaceProfile::Triangle { peak_offset: 0.5 },
1417 ),
1418 ]
1419 }
1420 }
1421 };
1422
1423 if config.fascia_depth.is_finite() && config.fascia_depth > 0.0 {
1427 let fascia_depth = config.fascia_depth;
1428 let mut fascia_panels: Vec<Panel> = Vec::new();
1429 for (local_off, face_size, rot_delta, selector, _profile) in &panels {
1430 let eave_bearing = matches!(
1431 selector,
1432 RoofFaceSelector::Slope
1433 | RoofFaceSelector::LowerSlope
1434 | RoofFaceSelector::OuterSlope
1435 );
1436 if !eave_bearing {
1437 continue;
1438 }
1439 if (local_off.y - y_anchor).abs() > 1e-6 {
1440 continue;
1441 }
1442 let Some(wall_rot) = slope_to_wall_rot(*rot_delta) else {
1443 continue;
1444 };
1445 fascia_panels.push((
1446 Vec3::new(local_off.x, local_off.y - fascia_depth, local_off.z),
1447 Vec3::new(face_size.x, fascia_depth, 0.0),
1448 wall_rot,
1449 RoofFaceSelector::Fascia,
1450 FaceProfile::Rectangle,
1451 ));
1452 }
1453 panels.extend(fascia_panels);
1454 }
1455
1456 if queue.len() + panels.len() > MAX_QUEUE {
1457 return Err(ShapeError::CapacityOverflow);
1458 }
1459 for (local_off, face_size, rot_delta, selector, profile) in panels {
1460 let Some(rule) = find_roof_rule(selector, cases) else {
1461 continue;
1462 };
1463 if face_size.x < 1e-9 || face_size.y < 1e-9 {
1465 continue;
1466 }
1467 let face_pos = scope.position + scope.rotation * local_off;
1468 let face_rot = (scope.rotation * rot_delta).normalize();
1469 let face_scope = Scope::new(face_pos, face_rot, face_size);
1470 face_scope.validate()?;
1471 if model.len() + queue.len() >= max_terminals {
1472 return Err(ShapeError::CapacityOverflow);
1473 }
1474 queue.push_back(WorkItem {
1475 scope: face_scope,
1476 rule: rule.to_string(),
1477 depth: depth + 1,
1478 taper: 0.0,
1479 face_profile_override: Some(profile),
1480 material: material.clone(),
1481 });
1482 }
1483
1484 Ok(())
1485}
1486
1487fn select_variant<'a>(variants: &'a [WeightedVariant], rng: &mut Pcg64) -> &'a [ShapeOp] {
1490 if variants.is_empty() {
1491 return &[];
1492 }
1493 if variants.len() == 1 {
1494 return &variants[0].ops;
1495 }
1496 let total: f64 = variants.iter().map(|v| v.weight).sum();
1497 use rand::Rng;
1498 let r: f64 = rng.random::<f64>() * total;
1499 let mut acc = 0.0;
1500 for v in variants {
1501 acc += v.weight;
1502 if r < acc {
1503 return &v.ops;
1504 }
1505 }
1506 &variants.last().unwrap().ops
1507}
1508
1509pub struct Interpreter {
1520 rules: HashMap<String, Vec<WeightedVariant>>,
1521 pub max_depth: usize,
1524 pub max_terminals: usize,
1528 pub seed: u64,
1532}
1533
1534impl Default for Interpreter {
1535 fn default() -> Self {
1536 Self::new()
1537 }
1538}
1539
1540impl Interpreter {
1541 pub fn new() -> Self {
1542 Self {
1543 rules: HashMap::new(),
1544 max_depth: MAX_DEPTH,
1545 max_terminals: MAX_TERMINALS,
1546 seed: 0,
1547 }
1548 }
1549
1550 pub fn rules(&self) -> &HashMap<String, Vec<WeightedVariant>> {
1552 &self.rules
1553 }
1554
1555 pub fn set_variants(&mut self, name: impl Into<String>, variants: Vec<WeightedVariant>) {
1560 self.rules.insert(name.into(), variants);
1561 }
1562
1563 pub fn add_rule(&mut self, name: impl Into<String>, ops: Vec<ShapeOp>) {
1565 self.rules
1566 .insert(name.into(), vec![WeightedVariant { weight: 1.0, ops }]);
1567 }
1568
1569 pub fn add_weighted_rules(
1576 &mut self,
1577 name: impl Into<String>,
1578 variants: Vec<(f64, Vec<ShapeOp>)>,
1579 ) -> Result<(), ShapeError> {
1580 for (weight, _) in &variants {
1581 if !weight.is_finite() || *weight < 0.0 {
1582 return Err(ShapeError::InvalidNumericValue);
1583 }
1584 }
1585 let wvs = variants
1586 .into_iter()
1587 .map(|(weight, ops)| WeightedVariant { weight, ops })
1588 .collect();
1589 self.rules.insert(name.into(), wvs);
1590 Ok(())
1591 }
1592
1593 pub fn has_rule(&self, name: &str) -> bool {
1595 self.rules.contains_key(name)
1596 }
1597
1598 pub fn derive(
1605 &self,
1606 root_scope: Scope,
1607 root_rule: impl Into<String>,
1608 ) -> Result<ShapeModel, ShapeError> {
1609 root_scope.validate()?;
1610
1611 let mut model = ShapeModel::new();
1612 let mut queue: VecDeque<WorkItem> = VecDeque::new();
1613 let mut rng = Pcg64::seed_from_u64(self.seed);
1614
1615 queue.push_back(WorkItem {
1616 scope: root_scope,
1617 rule: root_rule.into(),
1618 depth: 0,
1619 taper: 0.0,
1620 face_profile_override: None,
1621 material: None,
1622 });
1623
1624 while let Some(item) = queue.pop_front() {
1625 if queue.len() > MAX_QUEUE {
1626 return Err(ShapeError::CapacityOverflow);
1627 }
1628 if item.depth > self.max_depth {
1629 return Err(ShapeError::DepthLimitExceeded(self.max_depth));
1630 }
1631
1632 let ops = match self.rules.get(&item.rule) {
1633 Some(variants) => select_variant(variants, &mut rng),
1634 None => {
1635 if model.len() >= self.max_terminals {
1637 return Err(ShapeError::CapacityOverflow);
1638 }
1639 let profile = item
1640 .face_profile_override
1641 .unwrap_or_else(|| taper_to_profile(item.taper));
1642 model.push(Terminal::new_profiled(
1643 item.scope,
1644 &item.rule,
1645 profile,
1646 item.material,
1647 ));
1648 continue;
1649 }
1650 };
1651
1652 self.apply_ops(
1653 item.scope,
1654 item.taper,
1655 item.face_profile_override,
1656 item.material,
1657 ops,
1658 item.depth,
1659 &mut queue,
1660 &mut model,
1661 )?;
1662 }
1663
1664 Ok(model)
1665 }
1666
1667 #[allow(clippy::too_many_arguments)]
1673 fn apply_ops(
1674 &self,
1675 initial_scope: Scope,
1676 initial_taper: f64,
1677 initial_face_profile: Option<FaceProfile>,
1678 initial_material: Option<Material>,
1679 ops: &[ShapeOp],
1680 depth: usize,
1681 queue: &mut VecDeque<WorkItem>,
1682 model: &mut ShapeModel,
1683 ) -> Result<(), ShapeError> {
1684 let mut scope = initial_scope;
1685 let mut taper = initial_taper;
1686 let mut face_profile = initial_face_profile;
1687 let mut material = initial_material;
1688
1689 for op in ops {
1690 match op {
1691 ShapeOp::Extrude(h) => {
1693 if !h.is_finite() || *h <= 0.0 {
1694 return Err(ShapeError::InvalidNumericValue);
1695 }
1696 if scope.size.z.abs() < 1e-9 && scope.size.y.abs() > 1e-9 {
1702 scope.size.z = *h;
1703 } else {
1704 scope.size.y = *h;
1705 }
1706 }
1707
1708 ShapeOp::Taper(amount) => {
1709 if !amount.is_finite() {
1710 return Err(ShapeError::InvalidNumericValue);
1711 }
1712 taper = amount.clamp(0.0, 1.0);
1713 }
1714
1715 ShapeOp::Rotate(q) => {
1716 if !q.is_finite() {
1717 return Err(ShapeError::InvalidNumericValue);
1718 }
1719 let len_sq = q.length_squared();
1723 if !len_sq.is_finite() || len_sq < 1e-12 {
1724 return Err(ShapeError::InvalidNumericValue);
1725 }
1726 scope.rotation = (scope.rotation * q.normalize()).normalize();
1727 }
1728
1729 ShapeOp::Translate(v) => {
1730 if !v.is_finite() {
1731 return Err(ShapeError::InvalidNumericValue);
1732 }
1733 scope.position += scope.rotation * *v;
1734 if !scope.position.is_finite() {
1737 return Err(ShapeError::InvalidNumericValue);
1738 }
1739 }
1740
1741 ShapeOp::Scale(v) => {
1742 if !v.is_finite() || v.x <= 0.0 || v.y <= 0.0 || v.z <= 0.0 {
1743 return Err(ShapeError::InvalidNumericValue);
1744 }
1745 scope.size *= *v;
1746 if !scope.size.is_finite() {
1750 return Err(ShapeError::InvalidNumericValue);
1751 }
1752 }
1753
1754 ShapeOp::Mat(mat) => {
1755 material = Some(mat.clone());
1756 }
1757
1758 ShapeOp::Polygon(verts) => {
1759 if verts.len() < 3 {
1760 return Err(ShapeError::InvalidNumericValue);
1761 }
1762 for v in verts {
1763 if !v.is_finite() {
1764 return Err(ShapeError::InvalidNumericValue);
1765 }
1766 }
1767 face_profile = Some(FaceProfile::Polygon(verts.clone()));
1768 }
1769
1770 ShapeOp::RegSnap(label) => {
1772 register_scope_snap_planes(&scope, label, &mut model.snap_planes);
1773 }
1774
1775 ShapeOp::IfClear { rule } => {
1781 let occluded = model
1782 .terminals
1783 .iter()
1784 .any(|t| scope_obb_overlaps_terminal(&scope, t));
1785 if !occluded {
1786 if queue.len() >= MAX_QUEUE {
1787 return Err(ShapeError::CapacityOverflow);
1788 }
1789 queue.push_back(WorkItem {
1790 scope,
1791 rule: rule.clone(),
1792 depth: depth + 1,
1793 taper,
1794 face_profile_override: face_profile.take(),
1795 material: material.clone(),
1796 });
1797 }
1798 return Ok(());
1799 }
1800 ShapeOp::IfOccluded { rule } => {
1801 let occluded = model
1802 .terminals
1803 .iter()
1804 .any(|t| scope_obb_overlaps_terminal(&scope, t));
1805 if occluded {
1806 if queue.len() >= MAX_QUEUE {
1807 return Err(ShapeError::CapacityOverflow);
1808 }
1809 queue.push_back(WorkItem {
1810 scope,
1811 rule: rule.clone(),
1812 depth: depth + 1,
1813 taper,
1814 face_profile_override: face_profile.take(),
1815 material: material.clone(),
1816 });
1817 }
1818 return Ok(());
1819 }
1820
1821 ShapeOp::Align { local_axis, target } => {
1823 let len_sq = target.length_squared();
1828 if !target.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
1829 return Err(ShapeError::InvalidAlignTarget);
1830 }
1831 let target_norm = target.normalize();
1832 let current = scope.rotation * axis_vec(*local_axis);
1833 let dot = current.dot(target_norm);
1836 let q = if (dot + 1.0).abs() < 1e-9 {
1837 let perp = if current.x.abs() <= current.y.abs()
1843 && current.x.abs() <= current.z.abs()
1844 {
1845 current.cross(Vec3::X).normalize()
1846 } else if current.y.abs() <= current.z.abs() {
1847 current.cross(Vec3::Y).normalize()
1848 } else {
1849 current.cross(Vec3::Z).normalize()
1850 };
1851 Quat::from_axis_angle(perp, PI)
1852 } else {
1853 Quat::from_rotation_arc(current, target_norm)
1854 };
1855 scope.rotation = (q * scope.rotation).normalize();
1856 }
1857
1858 ShapeOp::Offset { distance, cases } => {
1860 if !distance.is_finite() {
1861 return Err(ShapeError::InvalidNumericValue);
1862 }
1863 let inset = -*distance;
1865 if inset <= 0.0 {
1866 return Err(ShapeError::InvalidNumericValue);
1867 }
1868 let sx = scope.size.x;
1869 let sy = scope.size.y;
1870 let inside_w = sx - 2.0 * inset;
1871 let inside_h = sy - 2.0 * inset;
1872 if !inside_w.is_finite()
1876 || !inside_h.is_finite()
1877 || inside_w < 0.0
1878 || inside_h < 0.0
1879 {
1880 return Err(ShapeError::OffsetTooLarge);
1881 }
1882 if let Some(rule) = find_offset_rule(OffsetSelector::Inside, cases) {
1883 if queue.len() >= MAX_QUEUE {
1884 return Err(ShapeError::CapacityOverflow);
1885 }
1886 let pos = scope.position + scope.rotation * Vec3::new(inset, inset, 0.0);
1887 let child_scope =
1888 Scope::new(pos, scope.rotation, Vec3::new(inside_w, inside_h, 0.0));
1889 child_scope.validate()?;
1890 queue.push_back(WorkItem {
1891 scope: child_scope,
1892 rule: rule.to_string(),
1893 depth: depth + 1,
1894 taper: 0.0,
1895 face_profile_override: None,
1896 material: material.clone(),
1897 });
1898 }
1899 if let Some(rule) = find_offset_rule(OffsetSelector::Border, cases) {
1900 let strips = [
1902 (Vec3::new(0.0, 0.0, 0.0), Vec3::new(sx, inset, 0.0)),
1903 (Vec3::new(0.0, sy - inset, 0.0), Vec3::new(sx, inset, 0.0)),
1904 (
1905 Vec3::new(0.0, inset, 0.0),
1906 Vec3::new(inset, sy - 2.0 * inset, 0.0),
1907 ),
1908 (
1909 Vec3::new(sx - inset, inset, 0.0),
1910 Vec3::new(inset, sy - 2.0 * inset, 0.0),
1911 ),
1912 ];
1913 if queue.len() + strips.len() > MAX_QUEUE {
1914 return Err(ShapeError::CapacityOverflow);
1915 }
1916 for (local_off, strip_size) in strips {
1917 let pos = scope.position + scope.rotation * local_off;
1918 let child_scope = Scope::new(pos, scope.rotation, strip_size);
1919 child_scope.validate()?;
1920 queue.push_back(WorkItem {
1921 scope: child_scope,
1922 rule: rule.to_string(),
1923 depth: depth + 1,
1924 taper: 0.0,
1925 face_profile_override: None,
1926 material: material.clone(),
1927 });
1928 }
1929 }
1930 return Ok(());
1931 }
1932
1933 ShapeOp::Roof { config, cases } => {
1935 apply_roof(
1936 config,
1937 cases,
1938 &scope,
1939 depth,
1940 &material,
1941 queue,
1942 model,
1943 self.max_terminals,
1944 )?;
1945 return Ok(());
1946 }
1947
1948 ShapeOp::Attach { world_axis, cases } => {
1950 let len_sq = world_axis.length_squared();
1951 if !world_axis.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
1952 return Err(ShapeError::InvalidAlignTarget);
1953 }
1954 let axis_norm = world_axis.normalize();
1955 let rot = Quat::from_rotation_arc(Vec3::Y, axis_norm);
1959 let attach_scope = Scope::new(
1960 scope.position,
1961 rot.normalize(),
1962 Vec3::new(scope.size.x, scope.size.y, 0.0),
1963 );
1964 attach_scope.validate()?;
1965 if let Some(rule) = find_attach_rule(crate::ops::AttachSelector::Surface, cases)
1966 {
1967 if queue.len() >= MAX_QUEUE {
1968 return Err(ShapeError::CapacityOverflow);
1969 }
1970 queue.push_back(WorkItem {
1971 scope: attach_scope,
1972 rule: rule.to_string(),
1973 depth: depth + 1,
1974 taper: 0.0,
1975 face_profile_override: None,
1976 material: material.clone(),
1977 });
1978 }
1979 return Ok(());
1980 }
1981
1982 ShapeOp::Split { axis, slots, snap } => {
1984 let total = match axis {
1985 Axis::X => scope.size.x,
1986 Axis::Y => scope.size.y,
1987 Axis::Z => scope.size.z,
1988 };
1989 let mut sizes = resolve_split_sizes(slots, total)?;
1990 if let Some(binding) = snap {
1995 let tol = binding.tolerance.unwrap_or(0.05 * total);
1996 snap_split_boundaries(
1997 &scope,
1998 *axis,
1999 &mut sizes,
2000 &binding.label,
2001 tol,
2002 &model.snap_planes,
2003 );
2004 }
2005 if queue.len() + slots.len() > MAX_QUEUE {
2006 return Err(ShapeError::CapacityOverflow);
2007 }
2008 let mut offset = 0.0;
2009 for (slot, size) in slots.iter().zip(sizes.iter()) {
2010 let child = slice_scope(&scope, *axis, offset, *size);
2011 child.validate()?;
2012 queue.push_back(WorkItem {
2013 scope: child,
2014 rule: slot.rule.clone(),
2015 depth: depth + 1,
2016 taper: 0.0,
2017 face_profile_override: None,
2018 material: material.clone(),
2019 });
2020 offset += size;
2021 }
2022 return Ok(());
2023 }
2024
2025 ShapeOp::Repeat {
2031 axis,
2032 tile_sizes,
2033 rule,
2034 } => {
2035 if tile_sizes.is_empty() {
2036 return Err(ShapeError::InvalidNumericValue);
2037 }
2038 for ts in tile_sizes {
2039 if !ts.is_finite() || *ts <= 0.0 {
2040 return Err(ShapeError::InvalidNumericValue);
2041 }
2042 }
2043 let total = match axis {
2044 Axis::X => scope.size.x,
2045 Axis::Y => scope.size.y,
2046 Axis::Z => scope.size.z,
2047 };
2048 if !total.is_finite() || total <= 0.0 {
2052 return Err(ShapeError::InvalidNumericValue);
2053 }
2054 let pattern_min = tile_sizes.iter().cloned().fold(f64::INFINITY, f64::min);
2055 if pattern_min <= 0.0 {
2056 return Err(ShapeError::InvalidNumericValue);
2057 }
2058 let n_max_f = (total / pattern_min).floor();
2063 if !n_max_f.is_finite() {
2064 return Err(ShapeError::CapacityOverflow);
2065 }
2066 let n_max = n_max_f as usize;
2067 if queue.len().saturating_add(n_max) > MAX_QUEUE {
2068 return Err(ShapeError::CapacityOverflow);
2069 }
2070 let mut placed: Vec<f64> = Vec::new();
2074 let mut acc = 0.0_f64;
2075 loop {
2076 let next = tile_sizes[placed.len() % tile_sizes.len()];
2077 if acc + next > total + 1e-12 {
2078 break;
2079 }
2080 placed.push(next);
2081 acc += next;
2082 }
2083 if !placed.is_empty() {
2084 let scale = total / acc;
2085 let mut offset = 0.0_f64;
2086 for tile in &placed {
2087 let actual = tile * scale;
2088 let child = slice_scope(&scope, *axis, offset, actual);
2089 child.validate()?;
2090 queue.push_back(WorkItem {
2091 scope: child,
2092 rule: rule.clone(),
2093 depth: depth + 1,
2094 taper: 0.0,
2095 face_profile_override: None,
2096 material: material.clone(),
2097 });
2098 offset += actual;
2099 }
2100 }
2101 return Ok(());
2102 }
2103
2104 ShapeOp::Comp(CompTarget::Faces(cases)) => {
2110 if queue.len() + 6 > MAX_QUEUE {
2112 return Err(ShapeError::CapacityOverflow);
2113 }
2114 for (selector, offset_local, face_size, rot_delta) in face_descs(scope.size) {
2115 let rule = match find_face_rule(selector, cases) {
2116 Some(r) => r,
2117 None => continue,
2118 };
2119 let face_pos = scope.position + scope.rotation * offset_local;
2120 let face_rotation = scope.rotation * rot_delta;
2121 let face_scope = Scope::new(face_pos, face_rotation, face_size);
2122 face_scope.validate()?;
2123 queue.push_back(WorkItem {
2124 scope: face_scope,
2125 rule: rule.to_string(),
2126 depth: depth + 1,
2127 taper: 0.0,
2128 face_profile_override: None,
2129 material: material.clone(),
2130 });
2131 }
2132 return Ok(());
2133 }
2134
2135 ShapeOp::I(mesh_id) => {
2137 if model.len() >= self.max_terminals {
2138 return Err(ShapeError::CapacityOverflow);
2139 }
2140 let profile = face_profile
2141 .take()
2142 .unwrap_or_else(|| taper_to_profile(taper));
2143 model.push(Terminal::new_profiled(scope, mesh_id, profile, material));
2144 return Ok(());
2145 }
2146
2147 ShapeOp::Rule(name) => {
2149 queue.push_back(WorkItem {
2150 scope,
2151 rule: name.clone(),
2152 depth: depth + 1,
2153 taper,
2154 face_profile_override: face_profile,
2155 material,
2156 });
2157 return Ok(());
2158 }
2159 }
2160 }
2161
2162 Ok(())
2165 }
2166}
2167
2168#[cfg(test)]
2169mod tests {
2170 use super::*;
2171 use crate::ops::{Axis, SplitSize, SplitSlot};
2172 use crate::scope::{Quat, Vec3};
2173
2174 fn slot(size: SplitSize, rule: &str) -> SplitSlot {
2175 SplitSlot {
2176 size,
2177 rule: rule.to_string(),
2178 }
2179 }
2180
2181 #[test]
2182 fn test_resolve_split_absolute() {
2183 let slots = vec![
2184 slot(SplitSize::Absolute(3.0), "A"),
2185 slot(SplitSize::Absolute(7.0), "B"),
2186 ];
2187 let sizes = resolve_split_sizes(&slots, 10.0).unwrap();
2188 assert!((sizes[0] - 3.0).abs() < 1e-9);
2189 assert!((sizes[1] - 7.0).abs() < 1e-9);
2190 }
2191
2192 #[test]
2193 fn test_resolve_split_floating_equal() {
2194 let slots = vec![
2195 slot(SplitSize::Floating(1.0), "A"),
2196 slot(SplitSize::Floating(1.0), "B"),
2197 ];
2198 let sizes = resolve_split_sizes(&slots, 10.0).unwrap();
2199 assert!((sizes[0] - 5.0).abs() < 1e-9);
2200 assert!((sizes[1] - 5.0).abs() < 1e-9);
2201 }
2202
2203 #[test]
2204 fn test_resolve_split_mixed() {
2205 let slots = vec![
2206 slot(SplitSize::Absolute(2.0), "Base"),
2207 slot(SplitSize::Floating(1.0), "A"),
2208 slot(SplitSize::Floating(1.0), "B"),
2209 ];
2210 let sizes = resolve_split_sizes(&slots, 10.0).unwrap();
2211 assert!((sizes[0] - 2.0).abs() < 1e-9);
2212 assert!((sizes[1] - 4.0).abs() < 1e-9);
2213 assert!((sizes[2] - 4.0).abs() < 1e-9);
2214 }
2215
2216 #[test]
2217 fn test_resolve_split_overflow_rejected() {
2218 let slots = vec![
2219 slot(SplitSize::Absolute(6.0), "A"),
2220 slot(SplitSize::Absolute(6.0), "B"),
2221 ];
2222 assert!(matches!(
2223 resolve_split_sizes(&slots, 10.0),
2224 Err(ShapeError::SplitOverflow(_))
2225 ));
2226 }
2227
2228 #[test]
2229 fn test_derive_extrude_then_terminal() {
2230 let mut interp = Interpreter::new();
2231 interp.add_rule(
2232 "Lot",
2233 vec![ShapeOp::Extrude(10.0), ShapeOp::I("Building".to_string())],
2234 );
2235 let scope = Scope::unit();
2236 let model = interp.derive(scope, "Lot").unwrap();
2237 assert_eq!(model.len(), 1);
2238 assert_eq!(model.terminals[0].mesh_id, "Building");
2239 assert!((model.terminals[0].scope.size.y - 10.0).abs() < 1e-9);
2240 }
2241
2242 #[test]
2243 fn test_derive_split_y_three_floors() {
2244 let mut interp = Interpreter::new();
2245 interp.add_rule(
2246 "Building",
2247 vec![ShapeOp::Split {
2248 axis: Axis::Y,
2249 slots: vec![
2250 slot(SplitSize::Absolute(2.0), "Ground"),
2251 slot(SplitSize::Floating(1.0), "Upper"),
2252 slot(SplitSize::Absolute(1.5), "Roof"),
2253 ],
2254 snap: None,
2255 }],
2256 );
2257 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 10.0, 10.0));
2258 let model = interp.derive(scope, "Building").unwrap();
2259 assert_eq!(model.len(), 3);
2260 assert!((model.terminals[0].scope.size.y - 2.0).abs() < 1e-9);
2261 assert!((model.terminals[1].scope.size.y - 6.5).abs() < 1e-9);
2262 assert!((model.terminals[2].scope.size.y - 1.5).abs() < 1e-9);
2263 }
2264
2265 #[test]
2266 fn test_derive_depth_limit() {
2267 let mut interp = Interpreter::new();
2268 interp.add_rule("A", vec![ShapeOp::Rule("A".to_string())]);
2269 interp.max_depth = 5;
2270 let model = interp.derive(Scope::unit(), "A");
2271 assert!(matches!(model, Err(ShapeError::DepthLimitExceeded(_))));
2272 }
2273
2274 #[test]
2275 fn test_derive_comp_faces() {
2276 let mut interp = Interpreter::new();
2277 interp.add_rule(
2278 "Box",
2279 vec![ShapeOp::Comp(CompTarget::Faces(vec![
2280 crate::ops::CompFaceCase {
2281 selector: FaceSelector::Top,
2282 rule: "Roof".to_string(),
2283 },
2284 crate::ops::CompFaceCase {
2285 selector: FaceSelector::Side,
2286 rule: "Wall".to_string(),
2287 },
2288 crate::ops::CompFaceCase {
2289 selector: FaceSelector::Bottom,
2290 rule: "Base".to_string(),
2291 },
2292 ]))],
2293 );
2294 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(5.0, 3.0, 5.0));
2295 let model = interp.derive(scope, "Box").unwrap();
2296 assert_eq!(model.len(), 6);
2297 }
2298
2299 #[test]
2300 fn test_derive_repeat() {
2301 let mut interp = Interpreter::new();
2302 interp.add_rule(
2303 "Facade",
2304 vec![ShapeOp::Repeat {
2305 axis: Axis::X,
2306 tile_sizes: vec![2.0],
2307 rule: "Window".to_string(),
2308 }],
2309 );
2310 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 0.0));
2311 let model = interp.derive(scope, "Facade").unwrap();
2312 assert_eq!(model.len(), 5);
2314 }
2315
2316 #[test]
2317 fn test_derive_mat_propagates() {
2318 let mut interp = Interpreter::new();
2319 interp.add_rule(
2320 "R",
2321 vec![
2322 ShapeOp::Mat(Material::new("Brick")),
2323 ShapeOp::I("Wall".to_string()),
2324 ],
2325 );
2326 let model = interp.derive(Scope::unit(), "R").unwrap();
2327 assert_eq!(model.terminals[0].material, Some(Material::new("Brick")));
2328 }
2329
2330 #[test]
2333 fn test_empty_variants_discards_shape() {
2334 let mut interp = Interpreter::new();
2335 interp.add_weighted_rules("Empty", vec![]).unwrap();
2338 let model = interp.derive(Scope::unit(), "Empty").unwrap();
2339 assert_eq!(model.len(), 0);
2340 }
2341
2342 #[test]
2345 fn test_repeat_tiny_tile_size_rejected() {
2346 let mut interp = Interpreter::new();
2351 interp.add_rule(
2352 "R",
2353 vec![ShapeOp::Repeat {
2354 axis: Axis::X,
2355 tile_sizes: vec![f64::MIN_POSITIVE],
2356 rule: "Tile".to_string(),
2357 }],
2358 );
2359 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
2360 assert!(matches!(
2361 interp.derive(scope, "R"),
2362 Err(ShapeError::CapacityOverflow)
2363 ));
2364 }
2365
2366 #[test]
2369 fn test_scale_multiply_overflow_to_infinity_rejected() {
2370 let mut interp = Interpreter::new();
2373 interp.add_rule(
2374 "R",
2375 vec![
2376 ShapeOp::Scale(Vec3::new(1e200, 1.0, 1.0)),
2377 ShapeOp::Scale(Vec3::new(1e200, 1.0, 1.0)), ShapeOp::I("Mesh".to_string()),
2379 ],
2380 );
2381 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
2382 assert!(matches!(
2383 interp.derive(scope, "R"),
2384 Err(ShapeError::InvalidNumericValue)
2385 ));
2386 }
2387
2388 #[test]
2389 fn test_split_absolute_sum_overflow_rejected() {
2390 let slots = vec![
2392 slot(SplitSize::Absolute(f64::MAX), "A"),
2393 slot(SplitSize::Absolute(f64::MAX), "B"),
2394 ];
2395 assert!(matches!(
2396 resolve_split_sizes(&slots, f64::MAX),
2397 Err(ShapeError::InvalidNumericValue)
2398 ));
2399 }
2400
2401 #[test]
2404 fn test_repeat_respects_combined_queue_limit() {
2405 let mut interp = Interpreter::new();
2411 interp.add_rule(
2413 "Big",
2414 vec![ShapeOp::Repeat {
2415 axis: Axis::X,
2416 tile_sizes: vec![1e-1],
2417 rule: "Tile".to_string(),
2418 }],
2419 );
2420 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1e10, 1.0, 1.0));
2421 assert!(matches!(
2422 interp.derive(scope, "Big"),
2423 Err(ShapeError::CapacityOverflow)
2424 ));
2425 }
2426
2427 #[test]
2430 fn test_api_negative_scale_rejected() {
2431 let mut interp = Interpreter::new();
2432 interp.add_rule(
2433 "R",
2434 vec![
2435 ShapeOp::Scale(Vec3::new(-1.0, 1.0, 1.0)),
2436 ShapeOp::I("Mesh".to_string()),
2437 ],
2438 );
2439 assert!(matches!(
2440 interp.derive(Scope::unit(), "R"),
2441 Err(ShapeError::InvalidNumericValue)
2442 ));
2443 }
2444
2445 #[test]
2446 fn test_api_zero_scale_rejected() {
2447 let mut interp = Interpreter::new();
2448 interp.add_rule(
2449 "R",
2450 vec![
2451 ShapeOp::Scale(Vec3::new(0.0, 1.0, 1.0)),
2452 ShapeOp::I("Mesh".to_string()),
2453 ],
2454 );
2455 assert!(matches!(
2456 interp.derive(Scope::unit(), "R"),
2457 Err(ShapeError::InvalidNumericValue)
2458 ));
2459 }
2460
2461 #[test]
2464 fn test_split_floating_large_remaining_no_overflow() {
2465 let slots = vec![
2469 slot(SplitSize::Floating(2.0), "A"),
2470 slot(SplitSize::Floating(1.0), "B"),
2471 ];
2472 let sizes = resolve_split_sizes(&slots, 1e308).unwrap();
2473 assert!(sizes[0].is_finite(), "size[0] overflowed to {}", sizes[0]);
2474 assert!(sizes[1].is_finite(), "size[1] overflowed to {}", sizes[1]);
2475 assert!((sizes[0] / sizes[1] - 2.0).abs() < 1e-6);
2477 }
2478
2479 #[test]
2482 fn test_split_floating_weight_overflow_rejected() {
2483 let slots = vec![
2486 slot(SplitSize::Floating(f64::MAX), "A"),
2487 slot(SplitSize::Floating(f64::MAX), "B"),
2488 ];
2489 assert!(matches!(
2490 resolve_split_sizes(&slots, 10.0),
2491 Err(ShapeError::InvalidNumericValue)
2492 ));
2493 }
2494
2495 #[test]
2496 fn test_stochastic_rule_deterministic_with_seed() {
2497 let mut interp = Interpreter::new();
2498 interp
2499 .add_weighted_rules(
2500 "Facade",
2501 vec![
2502 (70.0, vec![ShapeOp::I("Brick".to_string())]),
2503 (30.0, vec![ShapeOp::I("Glass".to_string())]),
2504 ],
2505 )
2506 .unwrap();
2507 interp.seed = 42;
2508 let m1 = interp.derive(Scope::unit(), "Facade").unwrap();
2510 let m2 = interp.derive(Scope::unit(), "Facade").unwrap();
2511 assert_eq!(m1.terminals[0].mesh_id, m2.terminals[0].mesh_id);
2512 }
2513
2514 #[test]
2515 fn test_face_comp_orientations() {
2516 let mut interp = Interpreter::new();
2520 interp.add_rule(
2521 "Box",
2522 vec![ShapeOp::Comp(CompTarget::Faces(vec![
2523 crate::ops::CompFaceCase {
2524 selector: FaceSelector::All,
2525 rule: "Face".to_string(),
2526 },
2527 ]))],
2528 );
2529 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 2.0));
2530 let model = interp.derive(scope, "Box").unwrap();
2531 assert_eq!(model.len(), 6);
2532
2533 let normals: Vec<Vec3> = model
2535 .terminals
2536 .iter()
2537 .map(|t| t.scope.rotation * Vec3::Z)
2538 .collect();
2539
2540 let expected = [
2542 Vec3::NEG_Y, Vec3::Y, Vec3::NEG_Z, Vec3::Z, Vec3::NEG_X, Vec3::X, ];
2549 for exp in &expected {
2550 assert!(
2551 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
2552 "missing normal {:?}, got {:?}",
2553 exp,
2554 normals
2555 );
2556 }
2557
2558 let pos = |i: usize| model.terminals[i].scope.position;
2562 assert!(
2563 (pos(0) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
2564 "Bottom pos"
2565 ); assert!(
2567 (pos(1) - Vec3::new(0.0, 3.0, 2.0)).length() < 1e-6,
2568 "Top pos"
2569 ); assert!(
2571 (pos(2) - Vec3::new(4.0, 0.0, 0.0)).length() < 1e-6,
2572 "Front pos"
2573 ); assert!(
2575 (pos(3) - Vec3::new(0.0, 0.0, 2.0)).length() < 1e-6,
2576 "Back pos"
2577 ); assert!(
2579 (pos(4) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
2580 "Left pos"
2581 ); assert!(
2583 (pos(5) - Vec3::new(4.0, 0.0, 2.0)).length() < 1e-6,
2584 "Right pos"
2585 ); }
2587
2588 #[test]
2591 fn test_negative_scope_size_rejected() {
2592 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(-1.0, 1.0, 1.0));
2593 let interp = Interpreter::new();
2594 assert!(matches!(
2595 interp.derive(scope, "Anything"),
2596 Err(ShapeError::InvalidNumericValue)
2597 ));
2598 }
2599
2600 #[test]
2601 fn test_zero_scope_size_accepted() {
2602 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 0.0, 10.0));
2604 let interp = Interpreter::new();
2605 let model = interp.derive(scope, "Footprint").unwrap();
2606 assert_eq!(model.len(), 1);
2607 }
2608
2609 #[test]
2612 fn test_unnormalized_root_quat_rejected() {
2613 let bad_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
2615 let scope = Scope::new(Vec3::ZERO, bad_q, Vec3::ONE);
2616 let interp = Interpreter::new();
2617 assert!(matches!(
2618 interp.derive(scope, "Anything"),
2619 Err(ShapeError::InvalidNumericValue)
2620 ));
2621 }
2622
2623 #[test]
2624 fn test_degenerate_rotate_op_rejected() {
2625 let zero_q = Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
2627 let mut interp = Interpreter::new();
2628 interp.add_rule(
2629 "R",
2630 vec![ShapeOp::Rotate(zero_q), ShapeOp::I("M".to_string())],
2631 );
2632 assert!(matches!(
2633 interp.derive(Scope::unit(), "R"),
2634 Err(ShapeError::InvalidNumericValue)
2635 ));
2636 }
2637
2638 #[test]
2639 fn test_scaled_rotate_op_normalized() {
2640 let scaled_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0); let mut interp = Interpreter::new();
2644 interp.add_rule(
2645 "R",
2646 vec![ShapeOp::Rotate(scaled_q), ShapeOp::I("M".to_string())],
2647 );
2648 let model = interp.derive(Scope::unit(), "R").unwrap();
2650 assert_eq!(model.len(), 1);
2651 let r = model.terminals[0].scope.rotation;
2652 assert!(
2653 (r.length_squared() - 1.0).abs() < 1e-9,
2654 "rotation should be unit"
2655 );
2656 }
2657
2658 #[test]
2661 fn test_nan_weight_rejected() {
2662 let mut interp = Interpreter::new();
2663 assert!(matches!(
2664 interp.add_weighted_rules("R", vec![(f64::NAN, vec![ShapeOp::I("M".to_string())])]),
2665 Err(ShapeError::InvalidNumericValue)
2666 ));
2667 }
2668
2669 #[test]
2670 fn test_infinite_weight_rejected() {
2671 let mut interp = Interpreter::new();
2672 assert!(matches!(
2673 interp.add_weighted_rules(
2674 "R",
2675 vec![(f64::INFINITY, vec![ShapeOp::I("M".to_string())])]
2676 ),
2677 Err(ShapeError::InvalidNumericValue)
2678 ));
2679 }
2680
2681 #[test]
2682 fn test_negative_weight_rejected() {
2683 let mut interp = Interpreter::new();
2684 assert!(matches!(
2685 interp.add_weighted_rules("R", vec![(-1.0, vec![ShapeOp::I("M".to_string())])]),
2686 Err(ShapeError::InvalidNumericValue)
2687 ));
2688 }
2689
2690 #[test]
2693 fn test_align_y_to_world_up_when_rotated() {
2694 let mut interp = Interpreter::new();
2696 let ninety_z = Quat::from_axis_angle(Vec3::Z, std::f64::consts::FRAC_PI_2);
2697 interp.add_rule(
2698 "R",
2699 vec![
2700 ShapeOp::Rotate(ninety_z),
2701 ShapeOp::Align {
2702 local_axis: Axis::Y,
2703 target: Vec3::Y,
2704 },
2705 ShapeOp::I("M".to_string()),
2706 ],
2707 );
2708 let model = interp.derive(Scope::unit(), "R").unwrap();
2709 assert_eq!(model.len(), 1);
2710 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
2711 assert!(
2712 (world_y - Vec3::Y).length() < 1e-6,
2713 "expected Y=(0,1,0), got {:?}",
2714 world_y
2715 );
2716 }
2717
2718 #[test]
2719 fn test_align_already_aligned_is_noop() {
2720 let mut interp = Interpreter::new();
2721 interp.add_rule(
2722 "R",
2723 vec![
2724 ShapeOp::Align {
2725 local_axis: Axis::Y,
2726 target: Vec3::Y,
2727 },
2728 ShapeOp::I("M".to_string()),
2729 ],
2730 );
2731 let model = interp.derive(Scope::unit(), "R").unwrap();
2732 let rot = model.terminals[0].scope.rotation;
2733 assert!((rot.length_squared() - 1.0).abs() < 1e-9);
2734 let world_y = rot * Vec3::Y;
2736 assert!((world_y - Vec3::Y).length() < 1e-6);
2737 }
2738
2739 #[test]
2740 fn test_align_zero_target_rejected() {
2741 let mut interp = Interpreter::new();
2742 interp.add_rule(
2743 "R",
2744 vec![
2745 ShapeOp::Align {
2746 local_axis: Axis::Y,
2747 target: Vec3::ZERO,
2748 },
2749 ShapeOp::I("M".to_string()),
2750 ],
2751 );
2752 assert!(matches!(
2753 interp.derive(Scope::unit(), "R"),
2754 Err(ShapeError::InvalidAlignTarget)
2755 ));
2756 }
2757
2758 #[test]
2761 fn test_offset_inset_produces_inside_and_border() {
2762 let mut interp = Interpreter::new();
2763 interp.add_rule(
2764 "R",
2765 vec![ShapeOp::Offset {
2766 distance: -0.5,
2767 cases: vec![
2768 crate::ops::OffsetCase {
2769 selector: crate::ops::OffsetSelector::Inside,
2770 rule: "Glass".to_string(),
2771 },
2772 crate::ops::OffsetCase {
2773 selector: crate::ops::OffsetSelector::Border,
2774 rule: "Frame".to_string(),
2775 },
2776 ],
2777 }],
2778 );
2779 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
2781 let model = interp.derive(scope, "R").unwrap();
2782 assert_eq!(model.len(), 5);
2784 let inside = model
2786 .terminals
2787 .iter()
2788 .find(|t| t.mesh_id == "Glass")
2789 .unwrap();
2790 assert!((inside.scope.size.x - 3.0).abs() < 1e-9);
2791 assert!((inside.scope.size.y - 2.0).abs() < 1e-9);
2792 assert!((inside.scope.position - Vec3::new(0.5, 0.5, 0.0)).length() < 1e-9);
2793 }
2794
2795 #[test]
2796 fn test_offset_too_large_rejected() {
2797 let mut interp = Interpreter::new();
2798 interp.add_rule(
2799 "R",
2800 vec![ShapeOp::Offset {
2801 distance: -2.0, cases: vec![crate::ops::OffsetCase {
2803 selector: crate::ops::OffsetSelector::Inside,
2804 rule: "A".to_string(),
2805 }],
2806 }],
2807 );
2808 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
2809 assert!(matches!(
2810 interp.derive(scope, "R"),
2811 Err(ShapeError::OffsetTooLarge)
2812 ));
2813 }
2814
2815 #[test]
2816 fn test_offset_positive_distance_rejected() {
2817 let mut interp = Interpreter::new();
2818 interp.add_rule(
2819 "R",
2820 vec![ShapeOp::Offset {
2821 distance: 0.2,
2822 cases: vec![crate::ops::OffsetCase {
2823 selector: crate::ops::OffsetSelector::Inside,
2824 rule: "A".to_string(),
2825 }],
2826 }],
2827 );
2828 assert!(matches!(
2829 interp.derive(Scope::unit(), "R"),
2830 Err(ShapeError::InvalidNumericValue)
2831 ));
2832 }
2833
2834 #[test]
2837 fn test_roof_shed_produces_one_slope() {
2838 let mut interp = Interpreter::new();
2839 interp.add_rule(
2840 "R",
2841 vec![ShapeOp::Roof {
2842 config: RoofConfig::new(RoofType::Shed, 30.0),
2843 cases: vec![crate::ops::RoofCase {
2844 selector: RoofFaceSelector::Slope,
2845 rule: "Tiles".to_string(),
2846 }],
2847 }],
2848 );
2849 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
2850 let model = interp.derive(scope, "R").unwrap();
2851 assert_eq!(model.len(), 1);
2852 assert_eq!(model.terminals[0].mesh_id, "Tiles");
2853 assert!((model.terminals[0].scope.position.y - 0.0).abs() < 1e-6);
2855 }
2856
2857 #[test]
2858 fn test_roof_gable_produces_four_panels() {
2859 let mut interp = Interpreter::new();
2860 interp.add_rule(
2861 "R",
2862 vec![ShapeOp::Roof {
2863 config: RoofConfig::new(RoofType::Gable, 30.0),
2864 cases: vec![
2865 crate::ops::RoofCase {
2866 selector: RoofFaceSelector::Slope,
2867 rule: "Tiles".to_string(),
2868 },
2869 crate::ops::RoofCase {
2870 selector: RoofFaceSelector::GableEnd,
2871 rule: "Bricks".to_string(),
2872 },
2873 ],
2874 }],
2875 );
2876 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
2877 let model = interp.derive(scope, "R").unwrap();
2878 assert_eq!(model.len(), 4);
2880 let tiles: Vec<_> = model
2881 .terminals
2882 .iter()
2883 .filter(|t| t.mesh_id == "Tiles")
2884 .collect();
2885 let bricks: Vec<_> = model
2886 .terminals
2887 .iter()
2888 .filter(|t| t.mesh_id == "Bricks")
2889 .collect();
2890 assert_eq!(tiles.len(), 2);
2891 assert_eq!(bricks.len(), 2);
2892 }
2893
2894 #[test]
2895 fn test_roof_hip_produces_four_slopes() {
2896 let mut interp = Interpreter::new();
2897 interp.add_rule(
2898 "R",
2899 vec![ShapeOp::Roof {
2900 config: {
2901 let mut c = RoofConfig::new(RoofType::Hip, 45.0);
2902 c.overhang = 0.3;
2903 c
2904 },
2905 cases: vec![crate::ops::RoofCase {
2906 selector: RoofFaceSelector::Slope,
2907 rule: "Tiles".to_string(),
2908 }],
2909 }],
2910 );
2911 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
2912 let model = interp.derive(scope, "R").unwrap();
2913 assert_eq!(model.len(), 4);
2914 }
2915
2916 #[test]
2917 fn test_roof_pyramid_produces_four_tapered_slopes() {
2918 let mut interp = Interpreter::new();
2919 interp.add_rule(
2920 "R",
2921 vec![ShapeOp::Roof {
2922 config: RoofConfig::new(RoofType::Pyramid, 40.0),
2923 cases: vec![crate::ops::RoofCase {
2924 selector: RoofFaceSelector::Slope,
2925 rule: "Tiles".to_string(),
2926 }],
2927 }],
2928 );
2929 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(6.0, 3.0, 6.0));
2930 let model = interp.derive(scope, "R").unwrap();
2931 assert_eq!(model.len(), 4);
2932 for t in &model.terminals {
2934 assert!(
2935 matches!(t.face_profile, FaceProfile::Triangle { peak_offset } if (peak_offset - 0.5).abs() < 1e-9),
2936 "expected Triangle{{peak_offset=0.5}}, got {:?}",
2937 t.face_profile
2938 );
2939 }
2940 }
2941
2942 #[test]
2943 fn test_roof_slope_normals_outward() {
2944 let alpha: f64 = 30_f64.to_radians();
2949 let cos_a = alpha.cos();
2950 let sin_a = alpha.sin();
2951 let mut interp = Interpreter::new();
2952 interp.add_rule(
2953 "R",
2954 vec![ShapeOp::Roof {
2955 config: RoofConfig::new(RoofType::Hip, 30.0),
2956 cases: vec![crate::ops::RoofCase {
2957 selector: RoofFaceSelector::Slope,
2958 rule: "S".to_string(),
2959 }],
2960 }],
2961 );
2962 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
2963 let model = interp.derive(scope, "R").unwrap();
2964 assert_eq!(model.len(), 4);
2965 let normals: Vec<Vec3> = model
2966 .terminals
2967 .iter()
2968 .map(|t| t.scope.rotation * Vec3::Z)
2969 .collect();
2970 let expected = [
2971 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), ];
2976 for exp in &expected {
2977 assert!(
2978 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
2979 "missing outward normal {:?}; got {:?}",
2980 exp,
2981 normals
2982 );
2983 }
2984 for n in &normals {
2986 assert!(n.y > 0.0, "normal pointing downward: {:?}", n);
2987 }
2988 }
2989
2990 #[test]
2991 fn test_align_antiparallel_fallback_no_nan() {
2992 let flip_y = Quat::from_axis_angle(Vec3::Z, PI);
2996 let mut interp = Interpreter::new();
2997 interp.add_rule(
2998 "R",
2999 vec![
3000 ShapeOp::Rotate(flip_y),
3001 ShapeOp::Align {
3002 local_axis: Axis::Y,
3003 target: Vec3::Y,
3004 },
3005 ShapeOp::I("M".to_string()),
3006 ],
3007 );
3008 let model = interp.derive(Scope::unit(), "R").unwrap();
3009 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
3010 assert!(
3011 (world_y - Vec3::Y).length() < 1e-6,
3012 "anti-parallel Align should point Y to world up, got {:?}",
3013 world_y
3014 );
3015 let r = model.terminals[0].scope.rotation;
3017 assert!(
3018 (r.length_squared() - 1.0).abs() < 1e-9,
3019 "rotation not unit: length_sq={}",
3020 r.length_squared()
3021 );
3022 }
3023
3024 #[test]
3025 fn test_roof_invalid_angle_rejected() {
3026 let mut interp = Interpreter::new();
3027 interp.add_rule(
3028 "R",
3029 vec![ShapeOp::Roof {
3030 config: RoofConfig::new(RoofType::Shed, 0.0),
3031 cases: vec![],
3032 }],
3033 );
3034 assert!(matches!(
3035 interp.derive(Scope::unit(), "R"),
3036 Err(ShapeError::InvalidRoofAngle(_))
3037 ));
3038 }
3039}