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, 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::scope::{Quat, Scope, Vec3};
23
24const MAX_DEPTH: usize = 64;
26const MAX_QUEUE: usize = 100_000;
27const MAX_TERMINALS: usize = 100_000;
28
29#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
33pub struct WeightedVariant {
34 pub weight: f64,
36 pub ops: Vec<ShapeOp>,
37}
38
39struct WorkItem {
42 scope: Scope,
43 rule: String,
44 depth: usize,
45 taper: f64,
49 face_profile_override: Option<FaceProfile>,
52 material: Option<String>,
54}
55
56fn resolve_split_sizes(slots: &[SplitSlot], total_dim: f64) -> Result<Vec<f64>, ShapeError> {
60 if slots.is_empty() {
61 return Err(ShapeError::EmptySplit);
62 }
63 for slot in slots {
64 if !slot.size.is_valid() {
65 return match &slot.size {
66 SplitSize::Floating(v) => Err(ShapeError::InvalidFloatingSize(*v)),
67 _ => Err(ShapeError::InvalidNumericValue),
68 };
69 }
70 }
71
72 let mut fixed: Vec<Option<f64>> = Vec::with_capacity(slots.len());
73 let mut used = 0.0_f64;
74 let mut float_weight_total = 0.0_f64;
75
76 for slot in slots {
77 match slot.size {
78 SplitSize::Absolute(v) => {
79 fixed.push(Some(v));
80 used += v;
81 }
82 SplitSize::Relative(t) => {
83 let s = total_dim * t;
84 fixed.push(Some(s));
85 used += s;
86 }
87 SplitSize::Floating(w) => {
88 fixed.push(None);
89 float_weight_total += w;
90 }
91 }
92 }
93
94 if !used.is_finite() {
96 return Err(ShapeError::InvalidNumericValue);
97 }
98
99 if used > total_dim + 1e-9 {
100 return Err(ShapeError::SplitOverflow(total_dim));
101 }
102
103 let remaining = (total_dim - used).max(0.0);
104
105 if !float_weight_total.is_finite() {
107 return Err(ShapeError::InvalidNumericValue);
108 }
109
110 let mut result = Vec::with_capacity(slots.len());
111 for (i, slot) in slots.iter().enumerate() {
112 match fixed[i] {
113 Some(v) => result.push(v),
114 None => {
115 let w = match slot.size {
116 SplitSize::Floating(w) => w,
117 _ => unreachable!(),
118 };
119 if float_weight_total <= 0.0 {
120 return Err(ShapeError::NoFloatingSlots);
121 }
122 result.push(remaining * (w / float_weight_total));
125 }
126 }
127 }
128
129 Ok(result)
130}
131
132fn slice_scope(parent: &Scope, axis: Axis, offset: f64, size: f64) -> Scope {
137 let offset_vec = match axis {
138 Axis::X => Vec3::new(offset, 0.0, 0.0),
139 Axis::Y => Vec3::new(0.0, offset, 0.0),
140 Axis::Z => Vec3::new(0.0, 0.0, offset),
141 };
142
143 let child_position = parent.position + parent.rotation * offset_vec;
144
145 let child_size = match axis {
146 Axis::X => Vec3::new(size, parent.size.y, parent.size.z),
147 Axis::Y => Vec3::new(parent.size.x, size, parent.size.z),
148 Axis::Z => Vec3::new(parent.size.x, parent.size.y, size),
149 };
150
151 Scope::new(child_position, parent.rotation, child_size)
152}
153
154fn face_descs(scope_size: Vec3) -> [(FaceSelector, Vec3, Vec3, Quat); 6] {
168 let sx = scope_size.x;
169 let sy = scope_size.y;
170 let sz = scope_size.z;
171
172 [
173 (
176 FaceSelector::Bottom,
177 Vec3::new(0.0, 0.0, 0.0),
178 Vec3::new(sx, sz, 0.0),
179 Quat::from_axis_angle(Vec3::X, FRAC_PI_2),
180 ),
181 (
185 FaceSelector::Top,
186 Vec3::new(0.0, sy, sz),
187 Vec3::new(sx, sz, 0.0),
188 Quat::from_axis_angle(Vec3::X, -FRAC_PI_2),
189 ),
190 (
194 FaceSelector::Front,
195 Vec3::new(sx, 0.0, 0.0),
196 Vec3::new(sx, sy, 0.0),
197 Quat::from_axis_angle(Vec3::Y, PI),
198 ),
199 (
203 FaceSelector::Back,
204 Vec3::new(0.0, 0.0, sz),
205 Vec3::new(sx, sy, 0.0),
206 Quat::IDENTITY,
207 ),
208 (
212 FaceSelector::Left,
213 Vec3::new(0.0, 0.0, 0.0),
214 Vec3::new(sz, sy, 0.0),
215 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
216 ),
217 (
221 FaceSelector::Right,
222 Vec3::new(sx, 0.0, sz),
223 Vec3::new(sz, sy, 0.0),
224 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
225 ),
226 ]
227}
228
229fn find_face_rule(selector: FaceSelector, cases: &[crate::ops::CompFaceCase]) -> Option<&str> {
231 for case in cases {
232 if case.selector == selector {
233 return Some(&case.rule);
234 }
235 }
236 let is_side = matches!(
237 selector,
238 FaceSelector::Front | FaceSelector::Back | FaceSelector::Left | FaceSelector::Right
239 );
240 if is_side {
241 for case in cases {
242 if case.selector == FaceSelector::Side {
243 return Some(&case.rule);
244 }
245 }
246 }
247 for case in cases {
248 if case.selector == FaceSelector::All {
249 return Some(&case.rule);
250 }
251 }
252 None
253}
254
255fn axis_vec(axis: Axis) -> Vec3 {
257 match axis {
258 Axis::X => Vec3::X,
259 Axis::Y => Vec3::Y,
260 Axis::Z => Vec3::Z,
261 }
262}
263
264fn find_offset_rule(selector: OffsetSelector, cases: &[OffsetCase]) -> Option<&str> {
266 for c in cases {
267 if c.selector == selector {
268 return Some(&c.rule);
269 }
270 }
271 for c in cases {
272 if c.selector == OffsetSelector::All {
273 return Some(&c.rule);
274 }
275 }
276 None
277}
278
279fn find_roof_rule(selector: RoofFaceSelector, cases: &[RoofCase]) -> Option<&str> {
281 for c in cases {
282 if c.selector == selector {
283 return Some(&c.rule);
284 }
285 }
286 for c in cases {
287 if c.selector == RoofFaceSelector::All {
288 return Some(&c.rule);
289 }
290 }
291 None
292}
293
294fn find_attach_rule(selector: AttachSelector, cases: &[AttachCase]) -> Option<&str> {
296 for c in cases {
297 if c.selector == selector {
298 return Some(&c.rule);
299 }
300 }
301 for c in cases {
302 if c.selector == AttachSelector::All {
303 return Some(&c.rule);
304 }
305 }
306 None
307}
308
309fn roof_slope_rotations(alpha: f64) -> (Quat, Quat, Quat, Quat) {
322 let front_rot =
325 Quat::from_axis_angle(Vec3::X, FRAC_PI_2 - alpha) * Quat::from_axis_angle(Vec3::Y, PI);
326 let back_rot = Quat::from_axis_angle(Vec3::X, alpha - FRAC_PI_2);
329 let left_rot = Quat::from_axis_angle(Vec3::Z, alpha - FRAC_PI_2)
332 * Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2);
333 let right_rot = Quat::from_axis_angle(Vec3::Z, FRAC_PI_2 - alpha)
336 * Quat::from_axis_angle(Vec3::Y, FRAC_PI_2);
337 (front_rot, back_rot, left_rot, right_rot)
338}
339
340#[allow(clippy::too_many_arguments)]
349fn apply_roof(
350 config: &RoofConfig,
351 cases: &[RoofCase],
352 scope: &Scope,
353 depth: usize,
354 material: &Option<String>,
355 queue: &mut VecDeque<WorkItem>,
356 model: &mut ShapeModel,
357 max_terminals: usize,
358) -> Result<(), ShapeError> {
359 if !config.pitch.is_finite() || config.pitch <= 0.0 || config.pitch >= 90.0 {
360 return Err(ShapeError::InvalidRoofAngle(config.pitch));
361 }
362 if !config.overhang.is_finite() || config.overhang < 0.0 {
363 return Err(ShapeError::InvalidNumericValue);
364 }
365
366 let sx = scope.size.x;
367 let sz = scope.size.z;
368 let o = config.overhang;
369 let alpha = config.pitch.to_radians();
370 let cos_a = alpha.cos();
371 let tan_a = alpha.tan();
372 let (front_rot, back_rot, left_rot, right_rot) = roof_slope_rotations(alpha);
373 let y_anchor = -o * tan_a;
375 let fb_len = (sz / 2.0 + o) / cos_a;
377 let lr_len = (sx / 2.0 + o) / cos_a;
378 let h = (sz / 2.0) * tan_a;
380
381 if !fb_len.is_finite() || !lr_len.is_finite() || !h.is_finite() {
382 return Err(ShapeError::InvalidNumericValue);
383 }
384
385 type Panel = (Vec3, Vec3, Quat, RoofFaceSelector, FaceProfile);
387
388 let panels: Vec<Panel> = match config.roof_type {
389 RoofType::Flat => vec![(
392 Vec3::new(0.0, 0.0, sz),
393 Vec3::new(sx, sz, 0.0),
394 Quat::from_axis_angle(Vec3::X, -FRAC_PI_2),
395 RoofFaceSelector::Slope,
396 FaceProfile::Rectangle,
397 )],
398
399 RoofType::Shed => {
402 let shed_h = sz * tan_a;
403 vec![
404 (
405 Vec3::new(sx + o, y_anchor, -o),
406 Vec3::new(sx + 2.0 * o, (sz + 2.0 * o) / cos_a, 0.0),
407 front_rot,
408 RoofFaceSelector::Slope,
409 FaceProfile::Rectangle,
410 ),
411 (
412 Vec3::new(0.0, 0.0, 0.0),
413 Vec3::new(sz, shed_h, 0.0),
414 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
415 RoofFaceSelector::GableEnd,
416 FaceProfile::Triangle { peak_offset: 1.0 },
417 ),
418 (
419 Vec3::new(sx, 0.0, sz),
420 Vec3::new(sz, shed_h, 0.0),
421 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
422 RoofFaceSelector::GableEnd,
423 FaceProfile::Triangle { peak_offset: 0.0 },
424 ),
425 ]
426 }
427
428 RoofType::Gable | RoofType::OpenGable | RoofType::BoxGable => {
430 let (slope_len, eave_len, ridge_h) = if sx >= sz {
431 ((sz / 2.0 + o) / cos_a, sx + 2.0 * o, (sz / 2.0) * tan_a)
432 } else {
433 ((sx / 2.0 + o) / cos_a, sz + 2.0 * o, (sx / 2.0) * tan_a)
434 };
435
436 let mut panels = if sx >= sz {
437 vec![
438 (
439 Vec3::new(sx + o, y_anchor, -o),
440 Vec3::new(eave_len, slope_len, 0.0),
441 front_rot,
442 RoofFaceSelector::Slope,
443 FaceProfile::Rectangle,
444 ),
445 (
446 Vec3::new(-o, y_anchor, sz + o),
447 Vec3::new(eave_len, slope_len, 0.0),
448 back_rot,
449 RoofFaceSelector::Slope,
450 FaceProfile::Rectangle,
451 ),
452 ]
453 } else {
454 vec![
455 (
456 Vec3::new(-o, y_anchor, -o),
457 Vec3::new(eave_len, slope_len, 0.0),
458 left_rot,
459 RoofFaceSelector::Slope,
460 FaceProfile::Rectangle,
461 ),
462 (
463 Vec3::new(sx + o, y_anchor, sz + o),
464 Vec3::new(eave_len, slope_len, 0.0),
465 right_rot,
466 RoofFaceSelector::Slope,
467 FaceProfile::Rectangle,
468 ),
469 ]
470 };
471
472 if config.roof_type != RoofType::OpenGable {
473 let profile = if config.roof_type == RoofType::BoxGable {
474 FaceProfile::Rectangle
475 } else {
476 FaceProfile::Triangle { peak_offset: 0.5 }
477 };
478
479 if sx >= sz {
480 panels.extend(vec![
481 (
482 Vec3::new(0.0, 0.0, 0.0),
483 Vec3::new(sz, ridge_h, 0.0),
484 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
485 RoofFaceSelector::GableEnd,
486 profile.clone(),
487 ),
488 (
489 Vec3::new(sx, 0.0, sz),
490 Vec3::new(sz, ridge_h, 0.0),
491 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
492 RoofFaceSelector::GableEnd,
493 profile,
494 ),
495 ]);
496 } else {
497 panels.extend(vec![
498 (
499 Vec3::new(sx, 0.0, 0.0),
500 Vec3::new(sx, ridge_h, 0.0),
501 Quat::from_axis_angle(Vec3::Y, PI),
502 RoofFaceSelector::GableEnd,
503 profile.clone(),
504 ),
505 (
506 Vec3::new(0.0, 0.0, sz),
507 Vec3::new(sx, ridge_h, 0.0),
508 Quat::IDENTITY,
509 RoofFaceSelector::GableEnd,
510 profile,
511 ),
512 ]);
513 }
514 }
515 panels
516 }
517
518 RoofType::Pyramid | RoofType::PyramidHip | RoofType::Hip => {
522 let eave_w = sx + 2.0 * o;
523 let eave_d = sz + 2.0 * o;
524 let max_run = sx.min(sz) / 2.0 + o;
525 let slope_len = max_run / cos_a;
526
527 let (fb_profile, lr_profile) = if sx > sz + 1e-5 {
528 let top_w = (sx - sz) / eave_w;
529 let off_x = (sz / 2.0 + o) / eave_w;
530 (
531 FaceProfile::Trapezoid {
532 top_width: top_w,
533 offset_x: off_x,
534 },
535 FaceProfile::Triangle { peak_offset: 0.5 },
536 )
537 } else if sz > sx + 1e-5 {
538 let top_w = (sz - sx) / eave_d;
539 let off_x = (sx / 2.0 + o) / eave_d;
540 (
541 FaceProfile::Triangle { peak_offset: 0.5 },
542 FaceProfile::Trapezoid {
543 top_width: top_w,
544 offset_x: off_x,
545 },
546 )
547 } else {
548 (
549 FaceProfile::Triangle { peak_offset: 0.5 },
550 FaceProfile::Triangle { peak_offset: 0.5 },
551 )
552 };
553
554 vec![
555 (
556 Vec3::new(sx + o, y_anchor, -o),
557 Vec3::new(eave_w, slope_len, 0.0),
558 front_rot,
559 RoofFaceSelector::Slope,
560 fb_profile.clone(),
561 ),
562 (
563 Vec3::new(-o, y_anchor, sz + o),
564 Vec3::new(eave_w, slope_len, 0.0),
565 back_rot,
566 RoofFaceSelector::Slope,
567 fb_profile,
568 ),
569 (
570 Vec3::new(-o, y_anchor, -o),
571 Vec3::new(eave_d, slope_len, 0.0),
572 left_rot,
573 RoofFaceSelector::Slope,
574 lr_profile.clone(),
575 ),
576 (
577 Vec3::new(sx + o, y_anchor, sz + o),
578 Vec3::new(eave_d, slope_len, 0.0),
579 right_rot,
580 RoofFaceSelector::Slope,
581 lr_profile,
582 ),
583 ]
584 }
585
586 RoofType::Butterfly => {
590 let y_valley = y_anchor - (sz / 2.0 + o) * tan_a;
592 if !y_valley.is_finite() {
593 return Err(ShapeError::InvalidNumericValue);
594 }
595 vec![
596 (
598 Vec3::new(-o, y_valley, sz / 2.0),
599 Vec3::new(sx + 2.0 * o, fb_len, 0.0),
600 back_rot,
601 RoofFaceSelector::ValleySlope,
602 FaceProfile::Rectangle,
603 ),
604 (
606 Vec3::new(sx + o, y_valley, sz / 2.0),
607 Vec3::new(sx + 2.0 * o, fb_len, 0.0),
608 front_rot,
609 RoofFaceSelector::ValleySlope,
610 FaceProfile::Rectangle,
611 ),
612 ]
613 }
614
615 RoofType::MShaped => {
619 let quarter = sz / 4.0;
620 let h_m = quarter * tan_a;
621 if !h_m.is_finite() {
622 return Err(ShapeError::InvalidNumericValue);
623 }
624 let slope_m = quarter / cos_a;
625 let y_valley = y_anchor - h_m; vec![
627 (
629 Vec3::new(sx + o, y_anchor, -o),
630 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
631 front_rot,
632 RoofFaceSelector::OuterSlope,
633 FaceProfile::Rectangle,
634 ),
635 (
637 Vec3::new(-o, y_valley, sz / 2.0),
638 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
639 back_rot,
640 RoofFaceSelector::InnerSlope,
641 FaceProfile::Rectangle,
642 ),
643 (
645 Vec3::new(sx + o, y_valley, sz / 2.0),
646 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
647 front_rot,
648 RoofFaceSelector::InnerSlope,
649 FaceProfile::Rectangle,
650 ),
651 (
653 Vec3::new(-o, y_anchor, sz + o),
654 Vec3::new(sx + 2.0 * o, slope_m, 0.0),
655 back_rot,
656 RoofFaceSelector::OuterSlope,
657 FaceProfile::Rectangle,
658 ),
659 ]
660 }
661
662 RoofType::Gambrel => {
665 let alpha2 = config.secondary_pitch_or_default().to_radians();
666 if !alpha2.is_finite() || alpha2 <= 0.0 || alpha2 >= FRAC_PI_2 {
667 return Err(ShapeError::InvalidNumericValue);
668 }
669 let cos_a2 = alpha2.cos();
670 let tan_a2 = alpha2.tan();
671 let tier = config.tier_height_or(0.5).clamp(0.01, 0.99);
672 let (ufr, ubr, ulr, urr) = roof_slope_rotations(alpha2);
673
674 if sx >= sz {
675 let run_z = sz / 2.0 + o;
676 let break_run = (tier * run_z).clamp(o, run_z - 1e-3);
677 let h_break = break_run * tan_a;
678 if !h_break.is_finite() {
679 return Err(ShapeError::InvalidNumericValue);
680 }
681 let lower_slope = break_run / cos_a;
682 let upper_run = run_z - break_run;
683 let upper_slope = upper_run / cos_a2;
684 let upper_h = upper_run * tan_a2;
685 let y_break = y_anchor + h_break;
686 let eave_w = sx + 2.0 * o;
687 let wall_y_break = h_break - o * tan_a;
688 let wall_break_run = break_run - o;
689 let mid_w = (sz - 2.0 * wall_break_run).max(0.0);
690
691 let lower_gable_profile = if mid_w > 1e-9 {
692 FaceProfile::Trapezoid {
693 top_width: mid_w / sz,
694 offset_x: wall_break_run / sz,
695 }
696 } else {
697 FaceProfile::Triangle { peak_offset: 0.5 }
698 };
699
700 vec![
701 (
702 Vec3::new(sx + o, y_anchor, -o),
703 Vec3::new(eave_w, lower_slope, 0.0),
704 front_rot,
705 RoofFaceSelector::LowerSlope,
706 FaceProfile::Rectangle,
707 ),
708 (
709 Vec3::new(-o, y_anchor, sz + o),
710 Vec3::new(eave_w, lower_slope, 0.0),
711 back_rot,
712 RoofFaceSelector::LowerSlope,
713 FaceProfile::Rectangle,
714 ),
715 (
716 Vec3::new(sx + o, y_break, -o + break_run),
717 Vec3::new(eave_w, upper_slope, 0.0),
718 ufr,
719 RoofFaceSelector::UpperSlope,
720 FaceProfile::Rectangle,
721 ),
722 (
723 Vec3::new(-o, y_break, sz + o - break_run),
724 Vec3::new(eave_w, upper_slope, 0.0),
725 ubr,
726 RoofFaceSelector::UpperSlope,
727 FaceProfile::Rectangle,
728 ),
729 (
730 Vec3::new(0.0, 0.0, 0.0),
731 Vec3::new(sz, wall_y_break, 0.0),
732 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
733 RoofFaceSelector::GableEnd,
734 lower_gable_profile.clone(),
735 ),
736 (
737 Vec3::new(sx, 0.0, sz),
738 Vec3::new(sz, wall_y_break, 0.0),
739 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
740 RoofFaceSelector::GableEnd,
741 lower_gable_profile,
742 ),
743 (
744 Vec3::new(0.0, wall_y_break, wall_break_run),
745 Vec3::new(mid_w, upper_h, 0.0),
746 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
747 RoofFaceSelector::GableEnd,
748 FaceProfile::Triangle { peak_offset: 0.5 },
749 ),
750 (
751 Vec3::new(sx, wall_y_break, sz - wall_break_run),
752 Vec3::new(mid_w, upper_h, 0.0),
753 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
754 RoofFaceSelector::GableEnd,
755 FaceProfile::Triangle { peak_offset: 0.5 },
756 ),
757 ]
758 } else {
759 let run_x = sx / 2.0 + o;
760 let break_run = (tier * run_x).clamp(o, run_x - 1e-3);
761 let h_break = break_run * tan_a;
762 if !h_break.is_finite() {
763 return Err(ShapeError::InvalidNumericValue);
764 }
765 let lower_slope = break_run / cos_a;
766 let upper_run = run_x - break_run;
767 let upper_slope = upper_run / cos_a2;
768 let upper_h = upper_run * tan_a2;
769 let y_break = y_anchor + h_break;
770 let eave_d = sz + 2.0 * o;
771 let wall_y_break = h_break - o * tan_a;
772 let wall_break_run = break_run - o;
773 let mid_d = (sx - 2.0 * wall_break_run).max(0.0);
774
775 let lower_gable_profile = if mid_d > 1e-9 {
776 FaceProfile::Trapezoid {
777 top_width: mid_d / sx,
778 offset_x: wall_break_run / sx,
779 }
780 } else {
781 FaceProfile::Triangle { peak_offset: 0.5 }
782 };
783
784 vec![
785 (
786 Vec3::new(-o, y_anchor, -o),
787 Vec3::new(eave_d, lower_slope, 0.0),
788 left_rot,
789 RoofFaceSelector::LowerSlope,
790 FaceProfile::Rectangle,
791 ),
792 (
793 Vec3::new(sx + o, y_anchor, sz + o),
794 Vec3::new(eave_d, lower_slope, 0.0),
795 right_rot,
796 RoofFaceSelector::LowerSlope,
797 FaceProfile::Rectangle,
798 ),
799 (
800 Vec3::new(-o + break_run, y_break, -o),
801 Vec3::new(eave_d, upper_slope, 0.0),
802 ulr,
803 RoofFaceSelector::UpperSlope,
804 FaceProfile::Rectangle,
805 ),
806 (
807 Vec3::new(sx + o - break_run, y_break, sz + o),
808 Vec3::new(eave_d, upper_slope, 0.0),
809 urr,
810 RoofFaceSelector::UpperSlope,
811 FaceProfile::Rectangle,
812 ),
813 (
814 Vec3::new(sx, 0.0, 0.0),
815 Vec3::new(sx, wall_y_break, 0.0),
816 Quat::from_axis_angle(Vec3::Y, PI),
817 RoofFaceSelector::GableEnd,
818 lower_gable_profile.clone(),
819 ),
820 (
821 Vec3::new(0.0, 0.0, sz),
822 Vec3::new(sx, wall_y_break, 0.0),
823 Quat::IDENTITY,
824 RoofFaceSelector::GableEnd,
825 lower_gable_profile,
826 ),
827 (
828 Vec3::new(sx - wall_break_run, wall_y_break, 0.0),
829 Vec3::new(mid_d, upper_h, 0.0),
830 Quat::from_axis_angle(Vec3::Y, PI),
831 RoofFaceSelector::GableEnd,
832 FaceProfile::Triangle { peak_offset: 0.5 },
833 ),
834 (
835 Vec3::new(wall_break_run, wall_y_break, sz),
836 Vec3::new(mid_d, upper_h, 0.0),
837 Quat::IDENTITY,
838 RoofFaceSelector::GableEnd,
839 FaceProfile::Triangle { peak_offset: 0.5 },
840 ),
841 ]
842 }
843 }
844
845 RoofType::Mansard => {
848 let alpha2 = config.secondary_pitch_or_default().to_radians();
849 if !alpha2.is_finite() || alpha2 <= 0.0 || alpha2 >= FRAC_PI_2 {
850 return Err(ShapeError::InvalidNumericValue);
851 }
852 let cos_a2 = alpha2.cos();
853 let tier = config.tier_height_or(0.5).clamp(0.01, 0.99);
854
855 let max_run = sx.min(sz) / 2.0 + o;
856 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
857 let h_break = break_run * tan_a;
858
859 if !h_break.is_finite() {
860 return Err(ShapeError::InvalidNumericValue);
861 }
862
863 let lower_slope = break_run / cos_a;
864 let y_break = y_anchor + h_break;
865
866 let eave_w = sx + 2.0 * o;
867 let eave_d = sz + 2.0 * o;
868
869 let mid_w = (eave_w - 2.0 * break_run).max(0.0);
870 let mid_d = (eave_d - 2.0 * break_run).max(0.0);
871
872 let lower_fb_profile = if mid_w > 1e-9 {
873 FaceProfile::Trapezoid {
874 top_width: mid_w / eave_w,
875 offset_x: break_run / eave_w,
876 }
877 } else {
878 FaceProfile::Triangle { peak_offset: 0.5 }
879 };
880
881 let lower_lr_profile = if mid_d > 1e-9 {
882 FaceProfile::Trapezoid {
883 top_width: mid_d / eave_d,
884 offset_x: break_run / eave_d,
885 }
886 } else {
887 FaceProfile::Triangle { peak_offset: 0.5 }
888 };
889
890 let (ufr, ubr, ulr, urr) = roof_slope_rotations(alpha2);
891
892 let upper_run = mid_w.min(mid_d) / 2.0;
893 let upper_slope = upper_run / cos_a2;
894
895 let top_w = (mid_w - 2.0 * upper_run).max(0.0);
896 let top_d = (mid_d - 2.0 * upper_run).max(0.0);
897
898 let upper_fb_profile = if top_w > 1e-9 {
899 FaceProfile::Trapezoid {
900 top_width: top_w / mid_w,
901 offset_x: upper_run / mid_w,
902 }
903 } else {
904 FaceProfile::Triangle { peak_offset: 0.5 }
905 };
906
907 let upper_lr_profile = if top_d > 1e-9 {
908 FaceProfile::Trapezoid {
909 top_width: top_d / mid_d,
910 offset_x: upper_run / mid_d,
911 }
912 } else {
913 FaceProfile::Triangle { peak_offset: 0.5 }
914 };
915
916 vec![
917 (
919 Vec3::new(sx + o, y_anchor, -o),
920 Vec3::new(eave_w, lower_slope, 0.0),
921 front_rot,
922 RoofFaceSelector::LowerSlope,
923 lower_fb_profile.clone(),
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 lower_fb_profile,
931 ),
932 (
933 Vec3::new(-o, y_anchor, -o),
934 Vec3::new(eave_d, lower_slope, 0.0),
935 left_rot,
936 RoofFaceSelector::LowerSlope,
937 lower_lr_profile.clone(),
938 ),
939 (
940 Vec3::new(sx + o, y_anchor, sz + o),
941 Vec3::new(eave_d, lower_slope, 0.0),
942 right_rot,
943 RoofFaceSelector::LowerSlope,
944 lower_lr_profile,
945 ),
946 (
948 Vec3::new(sx + o - break_run, y_break, -o + break_run),
949 Vec3::new(mid_w, upper_slope, 0.0),
950 ufr,
951 RoofFaceSelector::UpperSlope,
952 upper_fb_profile.clone(),
953 ),
954 (
955 Vec3::new(-o + break_run, y_break, sz + o - break_run),
956 Vec3::new(mid_w, upper_slope, 0.0),
957 ubr,
958 RoofFaceSelector::UpperSlope,
959 upper_fb_profile,
960 ),
961 (
962 Vec3::new(-o + break_run, y_break, -o + break_run),
963 Vec3::new(mid_d, upper_slope, 0.0),
964 ulr,
965 RoofFaceSelector::UpperSlope,
966 upper_lr_profile.clone(),
967 ),
968 (
969 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
970 Vec3::new(mid_d, upper_slope, 0.0),
971 urr,
972 RoofFaceSelector::UpperSlope,
973 upper_lr_profile,
974 ),
975 ]
976 }
977
978 RoofType::Saltbox => {
982 let (orient_z, _width, depth) = if sx >= sz {
983 (true, sx, sz)
984 } else {
985 (false, sz, sx)
986 };
987 let ridge_d = depth * config.ridge_offset;
988 if !ridge_d.is_finite() || ridge_d <= 0.0 || ridge_d >= depth {
989 return Err(ShapeError::InvalidNumericValue);
990 }
991 let h_s = ridge_d * tan_a;
992 let back_depth = depth - ridge_d;
993 let alpha_back = ((h_s) / (back_depth + o)).atan();
994 let cos_ab = alpha_back.cos();
995 if !h_s.is_finite() || !alpha_back.is_finite() || cos_ab < 1e-9 {
996 return Err(ShapeError::InvalidNumericValue);
997 }
998 let front_len = (ridge_d + o) / cos_a;
999 let back_len = (back_depth + o) / cos_ab;
1000 if !front_len.is_finite() || !back_len.is_finite() {
1001 return Err(ShapeError::InvalidNumericValue);
1002 }
1003 let (_, back_rot_s, _, right_rot_s) = roof_slope_rotations(alpha_back);
1004 let peak_fwd = ridge_d / depth;
1005
1006 if orient_z {
1007 vec![
1008 (
1009 Vec3::new(sx + o, y_anchor, -o),
1010 Vec3::new(sx + 2.0 * o, front_len, 0.0),
1011 front_rot,
1012 RoofFaceSelector::Slope,
1013 FaceProfile::Rectangle,
1014 ),
1015 (
1016 Vec3::new(-o, y_anchor, sz + o),
1017 Vec3::new(sx + 2.0 * o, back_len, 0.0),
1018 back_rot_s,
1019 RoofFaceSelector::Slope,
1020 FaceProfile::Rectangle,
1021 ),
1022 (
1023 Vec3::new(0.0, 0.0, 0.0),
1024 Vec3::new(sz, h_s, 0.0),
1025 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1026 RoofFaceSelector::GableEnd,
1027 FaceProfile::Triangle {
1028 peak_offset: peak_fwd,
1029 },
1030 ),
1031 (
1032 Vec3::new(sx, 0.0, sz),
1033 Vec3::new(sz, h_s, 0.0),
1034 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1035 RoofFaceSelector::GableEnd,
1036 FaceProfile::Triangle {
1037 peak_offset: 1.0 - peak_fwd,
1038 },
1039 ),
1040 ]
1041 } else {
1042 vec![
1043 (
1044 Vec3::new(-o, y_anchor, -o),
1045 Vec3::new(sz + 2.0 * o, front_len, 0.0),
1046 left_rot,
1047 RoofFaceSelector::Slope,
1048 FaceProfile::Rectangle,
1049 ),
1050 (
1051 Vec3::new(sx + o, y_anchor, sz + o),
1052 Vec3::new(sz + 2.0 * o, back_len, 0.0),
1053 right_rot_s,
1054 RoofFaceSelector::Slope,
1055 FaceProfile::Rectangle,
1056 ),
1057 (
1058 Vec3::new(sx, 0.0, 0.0),
1059 Vec3::new(sx, h_s, 0.0),
1060 Quat::from_axis_angle(Vec3::Y, PI),
1061 RoofFaceSelector::GableEnd,
1062 FaceProfile::Triangle {
1063 peak_offset: 1.0 - peak_fwd,
1064 },
1065 ),
1066 (
1067 Vec3::new(0.0, 0.0, sz),
1068 Vec3::new(sx, h_s, 0.0),
1069 Quat::IDENTITY,
1070 RoofFaceSelector::GableEnd,
1071 FaceProfile::Triangle {
1072 peak_offset: peak_fwd,
1073 },
1074 ),
1075 ]
1076 }
1077 }
1078
1079 RoofType::Jerkinhead => {
1083 let tier = config.tier_height_or(0.25).clamp(0.01, 0.99);
1084 let orient_z = sx >= sz;
1085 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1086
1087 let max_clip = (width / 2.0 + o).min(depth / 2.0);
1088 let clip_run = (tier * depth / 2.0).clamp(0.0, max_clip - 1e-3);
1089
1090 let eave_w = width + 2.0 * o;
1091 let top_w = (eave_w - 2.0 * clip_run).max(0.0);
1092
1093 let slope_len = (depth / 2.0 + o) / cos_a;
1094
1095 let slope_profile = if top_w > 1e-9 {
1096 FaceProfile::Trapezoid {
1097 top_width: top_w / eave_w,
1098 offset_x: clip_run / eave_w,
1099 }
1100 } else {
1101 FaceProfile::Triangle { peak_offset: 0.5 }
1102 };
1103
1104 let true_h = (depth / 2.0) * tan_a;
1105 let wall_h = (true_h - clip_run * tan_a).max(0.0);
1106 let wall_profile = if clip_run > 1e-9 {
1107 FaceProfile::Trapezoid {
1108 top_width: (2.0 * clip_run) / depth,
1109 offset_x: (depth / 2.0 - clip_run) / depth,
1110 }
1111 } else {
1112 FaceProfile::Triangle { peak_offset: 0.5 }
1113 };
1114
1115 let hip_base_w = 2.0 * clip_run + 2.0 * o;
1116 let hip_slope_len = (clip_run + o) / cos_a;
1117 let hip_profile = FaceProfile::Triangle { peak_offset: 0.5 };
1118
1119 if orient_z {
1120 let left_hip_origin = Vec3::new(-o, wall_h - o * tan_a, sz / 2.0 - clip_run - o);
1121 let right_hip_origin =
1122 Vec3::new(sx + o, wall_h - o * tan_a, sz / 2.0 + clip_run + o);
1123 vec![
1124 (
1125 Vec3::new(sx + o, y_anchor, -o),
1126 Vec3::new(eave_w, slope_len, 0.0),
1127 front_rot,
1128 RoofFaceSelector::Slope,
1129 slope_profile.clone(),
1130 ),
1131 (
1132 Vec3::new(-o, y_anchor, sz + o),
1133 Vec3::new(eave_w, slope_len, 0.0),
1134 back_rot,
1135 RoofFaceSelector::Slope,
1136 slope_profile,
1137 ),
1138 (
1139 Vec3::new(0.0, 0.0, 0.0),
1140 Vec3::new(sz, wall_h, 0.0),
1141 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1142 RoofFaceSelector::GableEnd,
1143 wall_profile.clone(),
1144 ),
1145 (
1146 Vec3::new(sx, 0.0, sz),
1147 Vec3::new(sz, wall_h, 0.0),
1148 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1149 RoofFaceSelector::GableEnd,
1150 wall_profile,
1151 ),
1152 (
1153 left_hip_origin,
1154 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1155 left_rot,
1156 RoofFaceSelector::HipEnd,
1157 hip_profile.clone(),
1158 ),
1159 (
1160 right_hip_origin,
1161 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1162 right_rot,
1163 RoofFaceSelector::HipEnd,
1164 hip_profile,
1165 ),
1166 ]
1167 } else {
1168 let front_hip_origin = Vec3::new(sx / 2.0 + clip_run + o, wall_h - o * tan_a, -o);
1169 let back_hip_origin =
1170 Vec3::new(sx / 2.0 - clip_run - o, wall_h - o * tan_a, sz + o);
1171 vec![
1172 (
1173 Vec3::new(-o, y_anchor, -o),
1174 Vec3::new(eave_w, slope_len, 0.0),
1175 left_rot,
1176 RoofFaceSelector::Slope,
1177 slope_profile.clone(),
1178 ),
1179 (
1180 Vec3::new(sx + o, y_anchor, sz + o),
1181 Vec3::new(eave_w, slope_len, 0.0),
1182 right_rot,
1183 RoofFaceSelector::Slope,
1184 slope_profile,
1185 ),
1186 (
1187 Vec3::new(sx, 0.0, 0.0),
1188 Vec3::new(sx, wall_h, 0.0),
1189 Quat::from_axis_angle(Vec3::Y, PI),
1190 RoofFaceSelector::GableEnd,
1191 wall_profile.clone(),
1192 ),
1193 (
1194 Vec3::new(0.0, 0.0, sz),
1195 Vec3::new(sx, wall_h, 0.0),
1196 Quat::IDENTITY,
1197 RoofFaceSelector::GableEnd,
1198 wall_profile,
1199 ),
1200 (
1201 front_hip_origin,
1202 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1203 front_rot,
1204 RoofFaceSelector::HipEnd,
1205 hip_profile.clone(),
1206 ),
1207 (
1208 back_hip_origin,
1209 Vec3::new(hip_base_w, hip_slope_len, 0.0),
1210 back_rot,
1211 RoofFaceSelector::HipEnd,
1212 hip_profile,
1213 ),
1214 ]
1215 }
1216 }
1217
1218 RoofType::DutchGable => {
1222 let tier = config.tier_height_or(0.7).clamp(0.01, 0.99);
1223 let orient_z = sx >= sz;
1224 let (width, depth) = if orient_z { (sx, sz) } else { (sz, sx) };
1225
1226 let max_run = width.min(depth) / 2.0 + o;
1227 let break_run = (tier * max_run).clamp(o, max_run - 1e-3);
1228
1229 if !break_run.is_finite() {
1230 return Err(ShapeError::InvalidNumericValue);
1231 }
1232
1233 let y_break = y_anchor + break_run * tan_a;
1234 let eave_w = width + 2.0 * o;
1235 let eave_d = depth + 2.0 * o;
1236
1237 let top_w = (eave_w - 2.0 * break_run).max(0.0);
1238 let top_d = (eave_d - 2.0 * break_run).max(0.0);
1239
1240 let lower_slope_len = break_run / cos_a;
1241
1242 let fb_profile = if top_w > 1e-9 {
1243 FaceProfile::Trapezoid {
1244 top_width: top_w / eave_w,
1245 offset_x: break_run / eave_w,
1246 }
1247 } else {
1248 FaceProfile::Triangle { peak_offset: 0.5 }
1249 };
1250
1251 let lr_profile = if top_d > 1e-9 {
1252 FaceProfile::Trapezoid {
1253 top_width: top_d / eave_d,
1254 offset_x: break_run / eave_d,
1255 }
1256 } else {
1257 FaceProfile::Triangle { peak_offset: 0.5 }
1258 };
1259
1260 let upper_run = (depth / 2.0 + o) - break_run;
1261 let upper_slope_len = upper_run / cos_a;
1262 let upper_h = upper_run * tan_a;
1263
1264 if orient_z {
1265 vec![
1266 (
1268 Vec3::new(sx + o, y_anchor, -o),
1269 Vec3::new(eave_w, lower_slope_len, 0.0),
1270 front_rot,
1271 RoofFaceSelector::Slope,
1272 fb_profile.clone(),
1273 ),
1274 (
1275 Vec3::new(-o, y_anchor, sz + o),
1276 Vec3::new(eave_w, lower_slope_len, 0.0),
1277 back_rot,
1278 RoofFaceSelector::Slope,
1279 fb_profile,
1280 ),
1281 (
1283 Vec3::new(-o, y_anchor, -o),
1284 Vec3::new(eave_d, lower_slope_len, 0.0),
1285 left_rot,
1286 RoofFaceSelector::Slope,
1287 lr_profile.clone(),
1288 ),
1289 (
1290 Vec3::new(sx + o, y_anchor, sz + o),
1291 Vec3::new(eave_d, lower_slope_len, 0.0),
1292 right_rot,
1293 RoofFaceSelector::Slope,
1294 lr_profile,
1295 ),
1296 (
1298 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1299 Vec3::new(top_w, upper_slope_len, 0.0),
1300 front_rot,
1301 RoofFaceSelector::Slope,
1302 FaceProfile::Rectangle,
1303 ),
1304 (
1305 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1306 Vec3::new(top_w, upper_slope_len, 0.0),
1307 back_rot,
1308 RoofFaceSelector::Slope,
1309 FaceProfile::Rectangle,
1310 ),
1311 (
1313 Vec3::new(-o + break_run, y_break, -o + break_run),
1314 Vec3::new(top_d, upper_h, 0.0),
1315 Quat::from_axis_angle(Vec3::Y, -FRAC_PI_2),
1316 RoofFaceSelector::GableEnd,
1317 FaceProfile::Triangle { peak_offset: 0.5 },
1318 ),
1319 (
1320 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1321 Vec3::new(top_d, upper_h, 0.0),
1322 Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
1323 RoofFaceSelector::GableEnd,
1324 FaceProfile::Triangle { peak_offset: 0.5 },
1325 ),
1326 ]
1327 } else {
1328 vec![
1329 (
1331 Vec3::new(-o, y_anchor, -o),
1332 Vec3::new(eave_w, lower_slope_len, 0.0),
1333 left_rot,
1334 RoofFaceSelector::Slope,
1335 fb_profile.clone(),
1336 ),
1337 (
1338 Vec3::new(sx + o, y_anchor, sz + o),
1339 Vec3::new(eave_w, lower_slope_len, 0.0),
1340 right_rot,
1341 RoofFaceSelector::Slope,
1342 fb_profile,
1343 ),
1344 (
1346 Vec3::new(sx + o, y_anchor, -o),
1347 Vec3::new(eave_d, lower_slope_len, 0.0),
1348 front_rot,
1349 RoofFaceSelector::Slope,
1350 lr_profile.clone(),
1351 ),
1352 (
1353 Vec3::new(-o, y_anchor, sz + o),
1354 Vec3::new(eave_d, lower_slope_len, 0.0),
1355 back_rot,
1356 RoofFaceSelector::Slope,
1357 lr_profile,
1358 ),
1359 (
1361 Vec3::new(-o + break_run, y_break, -o + break_run),
1362 Vec3::new(top_w, upper_slope_len, 0.0),
1363 left_rot,
1364 RoofFaceSelector::Slope,
1365 FaceProfile::Rectangle,
1366 ),
1367 (
1368 Vec3::new(sx + o - break_run, y_break, sz + o - break_run),
1369 Vec3::new(top_w, upper_slope_len, 0.0),
1370 right_rot,
1371 RoofFaceSelector::Slope,
1372 FaceProfile::Rectangle,
1373 ),
1374 (
1376 Vec3::new(sx + o - break_run, y_break, -o + break_run),
1377 Vec3::new(top_d, upper_h, 0.0),
1378 Quat::from_axis_angle(Vec3::Y, PI),
1379 RoofFaceSelector::GableEnd,
1380 FaceProfile::Triangle { peak_offset: 0.5 },
1381 ),
1382 (
1383 Vec3::new(-o + break_run, y_break, sz + o - break_run),
1384 Vec3::new(top_d, upper_h, 0.0),
1385 Quat::IDENTITY,
1386 RoofFaceSelector::GableEnd,
1387 FaceProfile::Triangle { peak_offset: 0.5 },
1388 ),
1389 ]
1390 }
1391 }
1392 };
1393
1394 if queue.len() + panels.len() > MAX_QUEUE {
1395 return Err(ShapeError::CapacityOverflow);
1396 }
1397 for (local_off, face_size, rot_delta, selector, profile) in panels {
1398 let Some(rule) = find_roof_rule(selector, cases) else {
1399 continue;
1400 };
1401 if face_size.x < 1e-9 || face_size.y < 1e-9 {
1403 continue;
1404 }
1405 let face_pos = scope.position + scope.rotation * local_off;
1406 let face_rot = (scope.rotation * rot_delta).normalize();
1407 let face_scope = Scope::new(face_pos, face_rot, face_size);
1408 face_scope.validate()?;
1409 if model.len() + queue.len() >= max_terminals {
1410 return Err(ShapeError::CapacityOverflow);
1411 }
1412 queue.push_back(WorkItem {
1413 scope: face_scope,
1414 rule: rule.to_string(),
1415 depth: depth + 1,
1416 taper: 0.0,
1417 face_profile_override: Some(profile),
1418 material: material.clone(),
1419 });
1420 }
1421
1422 Ok(())
1423}
1424
1425fn select_variant<'a>(variants: &'a [WeightedVariant], rng: &mut Pcg64) -> &'a [ShapeOp] {
1428 if variants.is_empty() {
1429 return &[];
1430 }
1431 if variants.len() == 1 {
1432 return &variants[0].ops;
1433 }
1434 let total: f64 = variants.iter().map(|v| v.weight).sum();
1435 use rand::Rng;
1436 let r: f64 = rng.random::<f64>() * total;
1437 let mut acc = 0.0;
1438 for v in variants {
1439 acc += v.weight;
1440 if r < acc {
1441 return &v.ops;
1442 }
1443 }
1444 &variants.last().unwrap().ops
1445}
1446
1447pub struct Interpreter {
1458 rules: HashMap<String, Vec<WeightedVariant>>,
1459 pub max_depth: usize,
1460 pub max_terminals: usize,
1461 pub seed: u64,
1463}
1464
1465impl Default for Interpreter {
1466 fn default() -> Self {
1467 Self::new()
1468 }
1469}
1470
1471impl Interpreter {
1472 pub fn new() -> Self {
1473 Self {
1474 rules: HashMap::new(),
1475 max_depth: MAX_DEPTH,
1476 max_terminals: MAX_TERMINALS,
1477 seed: 0,
1478 }
1479 }
1480
1481 pub fn rules(&self) -> &HashMap<String, Vec<WeightedVariant>> {
1483 &self.rules
1484 }
1485
1486 pub fn set_variants(&mut self, name: impl Into<String>, variants: Vec<WeightedVariant>) {
1490 self.rules.insert(name.into(), variants);
1491 }
1492
1493 pub fn add_rule(&mut self, name: impl Into<String>, ops: Vec<ShapeOp>) {
1495 self.rules
1496 .insert(name.into(), vec![WeightedVariant { weight: 1.0, ops }]);
1497 }
1498
1499 pub fn add_weighted_rules(
1506 &mut self,
1507 name: impl Into<String>,
1508 variants: Vec<(f64, Vec<ShapeOp>)>,
1509 ) -> Result<(), ShapeError> {
1510 for (weight, _) in &variants {
1511 if !weight.is_finite() || *weight < 0.0 {
1512 return Err(ShapeError::InvalidNumericValue);
1513 }
1514 }
1515 let wvs = variants
1516 .into_iter()
1517 .map(|(weight, ops)| WeightedVariant { weight, ops })
1518 .collect();
1519 self.rules.insert(name.into(), wvs);
1520 Ok(())
1521 }
1522
1523 pub fn has_rule(&self, name: &str) -> bool {
1525 self.rules.contains_key(name)
1526 }
1527
1528 pub fn derive(
1535 &self,
1536 root_scope: Scope,
1537 root_rule: impl Into<String>,
1538 ) -> Result<ShapeModel, ShapeError> {
1539 root_scope.validate()?;
1540
1541 let mut model = ShapeModel::new();
1542 let mut queue: VecDeque<WorkItem> = VecDeque::new();
1543 let mut rng = Pcg64::seed_from_u64(self.seed);
1544
1545 queue.push_back(WorkItem {
1546 scope: root_scope,
1547 rule: root_rule.into(),
1548 depth: 0,
1549 taper: 0.0,
1550 face_profile_override: None,
1551 material: None,
1552 });
1553
1554 while let Some(item) = queue.pop_front() {
1555 if queue.len() > MAX_QUEUE {
1556 return Err(ShapeError::CapacityOverflow);
1557 }
1558 if item.depth > self.max_depth {
1559 return Err(ShapeError::DepthLimitExceeded(self.max_depth));
1560 }
1561
1562 let ops = match self.rules.get(&item.rule) {
1563 Some(variants) => select_variant(variants, &mut rng),
1564 None => {
1565 if model.len() >= self.max_terminals {
1567 return Err(ShapeError::CapacityOverflow);
1568 }
1569 let profile = item
1570 .face_profile_override
1571 .unwrap_or_else(|| taper_to_profile(item.taper));
1572 model.push(Terminal::new_profiled(
1573 item.scope,
1574 &item.rule,
1575 profile,
1576 item.material,
1577 ));
1578 continue;
1579 }
1580 };
1581
1582 self.apply_ops(
1583 item.scope,
1584 item.taper,
1585 item.face_profile_override,
1586 item.material,
1587 ops,
1588 item.depth,
1589 &mut queue,
1590 &mut model,
1591 )?;
1592 }
1593
1594 Ok(model)
1595 }
1596
1597 #[allow(clippy::too_many_arguments)]
1603 fn apply_ops(
1604 &self,
1605 initial_scope: Scope,
1606 initial_taper: f64,
1607 initial_face_profile: Option<FaceProfile>,
1608 initial_material: Option<String>,
1609 ops: &[ShapeOp],
1610 depth: usize,
1611 queue: &mut VecDeque<WorkItem>,
1612 model: &mut ShapeModel,
1613 ) -> Result<(), ShapeError> {
1614 let mut scope = initial_scope;
1615 let mut taper = initial_taper;
1616 let mut face_profile = initial_face_profile;
1617 let mut material = initial_material;
1618
1619 for op in ops {
1620 match op {
1621 ShapeOp::Extrude(h) => {
1623 if !h.is_finite() || *h <= 0.0 {
1624 return Err(ShapeError::InvalidNumericValue);
1625 }
1626 if scope.size.z.abs() < 1e-9 && scope.size.y.abs() > 1e-9 {
1632 scope.size.z = *h;
1633 } else {
1634 scope.size.y = *h;
1635 }
1636 }
1637
1638 ShapeOp::Taper(amount) => {
1639 if !amount.is_finite() {
1640 return Err(ShapeError::InvalidNumericValue);
1641 }
1642 taper = amount.clamp(0.0, 1.0);
1643 }
1644
1645 ShapeOp::Rotate(q) => {
1646 if !q.is_finite() {
1647 return Err(ShapeError::InvalidNumericValue);
1648 }
1649 let len_sq = q.length_squared();
1653 if !len_sq.is_finite() || len_sq < 1e-12 {
1654 return Err(ShapeError::InvalidNumericValue);
1655 }
1656 scope.rotation = (scope.rotation * q.normalize()).normalize();
1657 }
1658
1659 ShapeOp::Translate(v) => {
1660 if !v.is_finite() {
1661 return Err(ShapeError::InvalidNumericValue);
1662 }
1663 scope.position += scope.rotation * *v;
1664 if !scope.position.is_finite() {
1667 return Err(ShapeError::InvalidNumericValue);
1668 }
1669 }
1670
1671 ShapeOp::Scale(v) => {
1672 if !v.is_finite() || v.x <= 0.0 || v.y <= 0.0 || v.z <= 0.0 {
1673 return Err(ShapeError::InvalidNumericValue);
1674 }
1675 scope.size *= *v;
1676 if !scope.size.is_finite() {
1680 return Err(ShapeError::InvalidNumericValue);
1681 }
1682 }
1683
1684 ShapeOp::Mat(mat_id) => {
1685 material = Some(mat_id.clone());
1686 }
1687
1688 ShapeOp::Align { local_axis, target } => {
1690 let len_sq = target.length_squared();
1695 if !target.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
1696 return Err(ShapeError::InvalidAlignTarget);
1697 }
1698 let target_norm = target.normalize();
1699 let current = scope.rotation * axis_vec(*local_axis);
1700 let dot = current.dot(target_norm);
1703 let q = if (dot + 1.0).abs() < 1e-9 {
1704 let perp = if current.x.abs() <= current.y.abs()
1710 && current.x.abs() <= current.z.abs()
1711 {
1712 current.cross(Vec3::X).normalize()
1713 } else if current.y.abs() <= current.z.abs() {
1714 current.cross(Vec3::Y).normalize()
1715 } else {
1716 current.cross(Vec3::Z).normalize()
1717 };
1718 Quat::from_axis_angle(perp, PI)
1719 } else {
1720 Quat::from_rotation_arc(current, target_norm)
1721 };
1722 scope.rotation = (q * scope.rotation).normalize();
1723 }
1724
1725 ShapeOp::Offset { distance, cases } => {
1727 if !distance.is_finite() {
1728 return Err(ShapeError::InvalidNumericValue);
1729 }
1730 let inset = -*distance;
1732 if inset <= 0.0 {
1733 return Err(ShapeError::InvalidNumericValue);
1734 }
1735 let sx = scope.size.x;
1736 let sy = scope.size.y;
1737 let inside_w = sx - 2.0 * inset;
1738 let inside_h = sy - 2.0 * inset;
1739 if !inside_w.is_finite()
1743 || !inside_h.is_finite()
1744 || inside_w < 0.0
1745 || inside_h < 0.0
1746 {
1747 return Err(ShapeError::OffsetTooLarge);
1748 }
1749 if let Some(rule) = find_offset_rule(OffsetSelector::Inside, cases) {
1750 if queue.len() >= MAX_QUEUE {
1751 return Err(ShapeError::CapacityOverflow);
1752 }
1753 let pos = scope.position + scope.rotation * Vec3::new(inset, inset, 0.0);
1754 let child_scope =
1755 Scope::new(pos, scope.rotation, Vec3::new(inside_w, inside_h, 0.0));
1756 child_scope.validate()?;
1757 queue.push_back(WorkItem {
1758 scope: child_scope,
1759 rule: rule.to_string(),
1760 depth: depth + 1,
1761 taper: 0.0,
1762 face_profile_override: None,
1763 material: material.clone(),
1764 });
1765 }
1766 if let Some(rule) = find_offset_rule(OffsetSelector::Border, cases) {
1767 let strips = [
1769 (Vec3::new(0.0, 0.0, 0.0), Vec3::new(sx, inset, 0.0)),
1770 (Vec3::new(0.0, sy - inset, 0.0), Vec3::new(sx, inset, 0.0)),
1771 (
1772 Vec3::new(0.0, inset, 0.0),
1773 Vec3::new(inset, sy - 2.0 * inset, 0.0),
1774 ),
1775 (
1776 Vec3::new(sx - inset, inset, 0.0),
1777 Vec3::new(inset, sy - 2.0 * inset, 0.0),
1778 ),
1779 ];
1780 if queue.len() + strips.len() > MAX_QUEUE {
1781 return Err(ShapeError::CapacityOverflow);
1782 }
1783 for (local_off, strip_size) in strips {
1784 let pos = scope.position + scope.rotation * local_off;
1785 let child_scope = Scope::new(pos, scope.rotation, strip_size);
1786 child_scope.validate()?;
1787 queue.push_back(WorkItem {
1788 scope: child_scope,
1789 rule: rule.to_string(),
1790 depth: depth + 1,
1791 taper: 0.0,
1792 face_profile_override: None,
1793 material: material.clone(),
1794 });
1795 }
1796 }
1797 return Ok(());
1798 }
1799
1800 ShapeOp::Roof { config, cases } => {
1802 apply_roof(
1803 config,
1804 cases,
1805 &scope,
1806 depth,
1807 &material,
1808 queue,
1809 model,
1810 self.max_terminals,
1811 )?;
1812 return Ok(());
1813 }
1814
1815 ShapeOp::Attach { world_axis, cases } => {
1817 let len_sq = world_axis.length_squared();
1818 if !world_axis.is_finite() || !len_sq.is_finite() || len_sq < 1e-12 {
1819 return Err(ShapeError::InvalidAlignTarget);
1820 }
1821 let axis_norm = world_axis.normalize();
1822 let rot = Quat::from_rotation_arc(Vec3::Y, axis_norm);
1826 let attach_scope = Scope::new(
1827 scope.position,
1828 rot.normalize(),
1829 Vec3::new(scope.size.x, scope.size.y, 0.0),
1830 );
1831 attach_scope.validate()?;
1832 if let Some(rule) = find_attach_rule(crate::ops::AttachSelector::Surface, cases)
1833 {
1834 if queue.len() >= MAX_QUEUE {
1835 return Err(ShapeError::CapacityOverflow);
1836 }
1837 queue.push_back(WorkItem {
1838 scope: attach_scope,
1839 rule: rule.to_string(),
1840 depth: depth + 1,
1841 taper: 0.0,
1842 face_profile_override: None,
1843 material: material.clone(),
1844 });
1845 }
1846 return Ok(());
1847 }
1848
1849 ShapeOp::Split { axis, slots } => {
1851 let total = match axis {
1852 Axis::X => scope.size.x,
1853 Axis::Y => scope.size.y,
1854 Axis::Z => scope.size.z,
1855 };
1856 let sizes = resolve_split_sizes(slots, total)?;
1857 if queue.len() + slots.len() > MAX_QUEUE {
1858 return Err(ShapeError::CapacityOverflow);
1859 }
1860 let mut offset = 0.0;
1861 for (slot, size) in slots.iter().zip(sizes.iter()) {
1862 let child = slice_scope(&scope, *axis, offset, *size);
1863 child.validate()?;
1864 queue.push_back(WorkItem {
1865 scope: child,
1866 rule: slot.rule.clone(),
1867 depth: depth + 1,
1868 taper: 0.0,
1869 face_profile_override: None,
1870 material: material.clone(),
1871 });
1872 offset += size;
1873 }
1874 return Ok(());
1875 }
1876
1877 ShapeOp::Repeat {
1883 axis,
1884 tile_size,
1885 rule,
1886 } => {
1887 if !tile_size.is_finite() || *tile_size <= 0.0 {
1888 return Err(ShapeError::InvalidNumericValue);
1889 }
1890 let total = match axis {
1891 Axis::X => scope.size.x,
1892 Axis::Y => scope.size.y,
1893 Axis::Z => scope.size.z,
1894 };
1895 if !total.is_finite() || total <= 0.0 {
1899 return Err(ShapeError::InvalidNumericValue);
1900 }
1901 let n_tiles_f = (total / tile_size).floor();
1907 if !n_tiles_f.is_finite() {
1908 return Err(ShapeError::CapacityOverflow);
1909 }
1910 let n_tiles = n_tiles_f as usize;
1911 if queue.len().saturating_add(n_tiles) > MAX_QUEUE {
1912 return Err(ShapeError::CapacityOverflow);
1913 }
1914 if n_tiles > 0 {
1915 let actual_size = total / n_tiles as f64;
1916 for i in 0..n_tiles {
1917 let offset = i as f64 * actual_size;
1918 let child = slice_scope(&scope, *axis, offset, actual_size);
1919 child.validate()?;
1920 queue.push_back(WorkItem {
1921 scope: child,
1922 rule: rule.clone(),
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::Comp(CompTarget::Faces(cases)) => {
1939 if queue.len() + 6 > MAX_QUEUE {
1941 return Err(ShapeError::CapacityOverflow);
1942 }
1943 for (selector, offset_local, face_size, rot_delta) in face_descs(scope.size) {
1944 let rule = match find_face_rule(selector, cases) {
1945 Some(r) => r,
1946 None => continue,
1947 };
1948 let face_pos = scope.position + scope.rotation * offset_local;
1949 let face_rotation = scope.rotation * rot_delta;
1950 let face_scope = Scope::new(face_pos, face_rotation, face_size);
1951 face_scope.validate()?;
1952 queue.push_back(WorkItem {
1953 scope: face_scope,
1954 rule: rule.to_string(),
1955 depth: depth + 1,
1956 taper: 0.0,
1957 face_profile_override: None,
1958 material: material.clone(),
1959 });
1960 }
1961 return Ok(());
1962 }
1963
1964 ShapeOp::I(mesh_id) => {
1966 if model.len() >= self.max_terminals {
1967 return Err(ShapeError::CapacityOverflow);
1968 }
1969 let profile = face_profile
1970 .take()
1971 .unwrap_or_else(|| taper_to_profile(taper));
1972 model.push(Terminal::new_profiled(scope, mesh_id, profile, material));
1973 return Ok(());
1974 }
1975
1976 ShapeOp::Rule(name) => {
1978 queue.push_back(WorkItem {
1979 scope,
1980 rule: name.clone(),
1981 depth: depth + 1,
1982 taper,
1983 face_profile_override: face_profile,
1984 material,
1985 });
1986 return Ok(());
1987 }
1988 }
1989 }
1990
1991 Ok(())
1994 }
1995}
1996
1997#[cfg(test)]
1998mod tests {
1999 use super::*;
2000 use crate::ops::{Axis, SplitSize, SplitSlot};
2001 use crate::scope::{Quat, Vec3};
2002
2003 fn slot(size: SplitSize, rule: &str) -> SplitSlot {
2004 SplitSlot {
2005 size,
2006 rule: rule.to_string(),
2007 }
2008 }
2009
2010 #[test]
2011 fn test_resolve_split_absolute() {
2012 let slots = vec![
2013 slot(SplitSize::Absolute(3.0), "A"),
2014 slot(SplitSize::Absolute(7.0), "B"),
2015 ];
2016 let sizes = resolve_split_sizes(&slots, 10.0).unwrap();
2017 assert!((sizes[0] - 3.0).abs() < 1e-9);
2018 assert!((sizes[1] - 7.0).abs() < 1e-9);
2019 }
2020
2021 #[test]
2022 fn test_resolve_split_floating_equal() {
2023 let slots = vec![
2024 slot(SplitSize::Floating(1.0), "A"),
2025 slot(SplitSize::Floating(1.0), "B"),
2026 ];
2027 let sizes = resolve_split_sizes(&slots, 10.0).unwrap();
2028 assert!((sizes[0] - 5.0).abs() < 1e-9);
2029 assert!((sizes[1] - 5.0).abs() < 1e-9);
2030 }
2031
2032 #[test]
2033 fn test_resolve_split_mixed() {
2034 let slots = vec![
2035 slot(SplitSize::Absolute(2.0), "Base"),
2036 slot(SplitSize::Floating(1.0), "A"),
2037 slot(SplitSize::Floating(1.0), "B"),
2038 ];
2039 let sizes = resolve_split_sizes(&slots, 10.0).unwrap();
2040 assert!((sizes[0] - 2.0).abs() < 1e-9);
2041 assert!((sizes[1] - 4.0).abs() < 1e-9);
2042 assert!((sizes[2] - 4.0).abs() < 1e-9);
2043 }
2044
2045 #[test]
2046 fn test_resolve_split_overflow_rejected() {
2047 let slots = vec![
2048 slot(SplitSize::Absolute(6.0), "A"),
2049 slot(SplitSize::Absolute(6.0), "B"),
2050 ];
2051 assert!(matches!(
2052 resolve_split_sizes(&slots, 10.0),
2053 Err(ShapeError::SplitOverflow(_))
2054 ));
2055 }
2056
2057 #[test]
2058 fn test_derive_extrude_then_terminal() {
2059 let mut interp = Interpreter::new();
2060 interp.add_rule(
2061 "Lot",
2062 vec![ShapeOp::Extrude(10.0), ShapeOp::I("Building".to_string())],
2063 );
2064 let scope = Scope::unit();
2065 let model = interp.derive(scope, "Lot").unwrap();
2066 assert_eq!(model.len(), 1);
2067 assert_eq!(model.terminals[0].mesh_id, "Building");
2068 assert!((model.terminals[0].scope.size.y - 10.0).abs() < 1e-9);
2069 }
2070
2071 #[test]
2072 fn test_derive_split_y_three_floors() {
2073 let mut interp = Interpreter::new();
2074 interp.add_rule(
2075 "Building",
2076 vec![ShapeOp::Split {
2077 axis: Axis::Y,
2078 slots: vec![
2079 slot(SplitSize::Absolute(2.0), "Ground"),
2080 slot(SplitSize::Floating(1.0), "Upper"),
2081 slot(SplitSize::Absolute(1.5), "Roof"),
2082 ],
2083 }],
2084 );
2085 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 10.0, 10.0));
2086 let model = interp.derive(scope, "Building").unwrap();
2087 assert_eq!(model.len(), 3);
2088 assert!((model.terminals[0].scope.size.y - 2.0).abs() < 1e-9);
2089 assert!((model.terminals[1].scope.size.y - 6.5).abs() < 1e-9);
2090 assert!((model.terminals[2].scope.size.y - 1.5).abs() < 1e-9);
2091 }
2092
2093 #[test]
2094 fn test_derive_depth_limit() {
2095 let mut interp = Interpreter::new();
2096 interp.add_rule("A", vec![ShapeOp::Rule("A".to_string())]);
2097 interp.max_depth = 5;
2098 let model = interp.derive(Scope::unit(), "A");
2099 assert!(matches!(model, Err(ShapeError::DepthLimitExceeded(_))));
2100 }
2101
2102 #[test]
2103 fn test_derive_comp_faces() {
2104 let mut interp = Interpreter::new();
2105 interp.add_rule(
2106 "Box",
2107 vec![ShapeOp::Comp(CompTarget::Faces(vec![
2108 crate::ops::CompFaceCase {
2109 selector: FaceSelector::Top,
2110 rule: "Roof".to_string(),
2111 },
2112 crate::ops::CompFaceCase {
2113 selector: FaceSelector::Side,
2114 rule: "Wall".to_string(),
2115 },
2116 crate::ops::CompFaceCase {
2117 selector: FaceSelector::Bottom,
2118 rule: "Base".to_string(),
2119 },
2120 ]))],
2121 );
2122 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(5.0, 3.0, 5.0));
2123 let model = interp.derive(scope, "Box").unwrap();
2124 assert_eq!(model.len(), 6);
2125 }
2126
2127 #[test]
2128 fn test_derive_repeat() {
2129 let mut interp = Interpreter::new();
2130 interp.add_rule(
2131 "Facade",
2132 vec![ShapeOp::Repeat {
2133 axis: Axis::X,
2134 tile_size: 2.0,
2135 rule: "Window".to_string(),
2136 }],
2137 );
2138 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 0.0));
2139 let model = interp.derive(scope, "Facade").unwrap();
2140 assert_eq!(model.len(), 5);
2142 }
2143
2144 #[test]
2145 fn test_derive_mat_propagates() {
2146 let mut interp = Interpreter::new();
2147 interp.add_rule(
2148 "R",
2149 vec![
2150 ShapeOp::Mat("Brick".to_string()),
2151 ShapeOp::I("Wall".to_string()),
2152 ],
2153 );
2154 let model = interp.derive(Scope::unit(), "R").unwrap();
2155 assert_eq!(model.terminals[0].material, Some("Brick".to_string()));
2156 }
2157
2158 #[test]
2161 fn test_empty_variants_discards_shape() {
2162 let mut interp = Interpreter::new();
2163 interp.add_weighted_rules("Empty", vec![]).unwrap();
2166 let model = interp.derive(Scope::unit(), "Empty").unwrap();
2167 assert_eq!(model.len(), 0);
2168 }
2169
2170 #[test]
2173 fn test_repeat_tiny_tile_size_rejected() {
2174 let mut interp = Interpreter::new();
2179 interp.add_rule(
2180 "R",
2181 vec![ShapeOp::Repeat {
2182 axis: Axis::X,
2183 tile_size: f64::MIN_POSITIVE,
2184 rule: "Tile".to_string(),
2185 }],
2186 );
2187 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
2188 assert!(matches!(
2189 interp.derive(scope, "R"),
2190 Err(ShapeError::CapacityOverflow)
2191 ));
2192 }
2193
2194 #[test]
2197 fn test_scale_multiply_overflow_to_infinity_rejected() {
2198 let mut interp = Interpreter::new();
2201 interp.add_rule(
2202 "R",
2203 vec![
2204 ShapeOp::Scale(Vec3::new(1e200, 1.0, 1.0)),
2205 ShapeOp::Scale(Vec3::new(1e200, 1.0, 1.0)), ShapeOp::I("Mesh".to_string()),
2207 ],
2208 );
2209 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 1.0, 1.0));
2210 assert!(matches!(
2211 interp.derive(scope, "R"),
2212 Err(ShapeError::InvalidNumericValue)
2213 ));
2214 }
2215
2216 #[test]
2217 fn test_split_absolute_sum_overflow_rejected() {
2218 let slots = vec![
2220 slot(SplitSize::Absolute(f64::MAX), "A"),
2221 slot(SplitSize::Absolute(f64::MAX), "B"),
2222 ];
2223 assert!(matches!(
2224 resolve_split_sizes(&slots, f64::MAX),
2225 Err(ShapeError::InvalidNumericValue)
2226 ));
2227 }
2228
2229 #[test]
2232 fn test_repeat_respects_combined_queue_limit() {
2233 let mut interp = Interpreter::new();
2239 interp.add_rule(
2241 "Big",
2242 vec![ShapeOp::Repeat {
2243 axis: Axis::X,
2244 tile_size: 1e-1,
2245 rule: "Tile".to_string(),
2246 }],
2247 );
2248 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1e10, 1.0, 1.0));
2249 assert!(matches!(
2250 interp.derive(scope, "Big"),
2251 Err(ShapeError::CapacityOverflow)
2252 ));
2253 }
2254
2255 #[test]
2258 fn test_api_negative_scale_rejected() {
2259 let mut interp = Interpreter::new();
2260 interp.add_rule(
2261 "R",
2262 vec![
2263 ShapeOp::Scale(Vec3::new(-1.0, 1.0, 1.0)),
2264 ShapeOp::I("Mesh".to_string()),
2265 ],
2266 );
2267 assert!(matches!(
2268 interp.derive(Scope::unit(), "R"),
2269 Err(ShapeError::InvalidNumericValue)
2270 ));
2271 }
2272
2273 #[test]
2274 fn test_api_zero_scale_rejected() {
2275 let mut interp = Interpreter::new();
2276 interp.add_rule(
2277 "R",
2278 vec![
2279 ShapeOp::Scale(Vec3::new(0.0, 1.0, 1.0)),
2280 ShapeOp::I("Mesh".to_string()),
2281 ],
2282 );
2283 assert!(matches!(
2284 interp.derive(Scope::unit(), "R"),
2285 Err(ShapeError::InvalidNumericValue)
2286 ));
2287 }
2288
2289 #[test]
2292 fn test_split_floating_large_remaining_no_overflow() {
2293 let slots = vec![
2297 slot(SplitSize::Floating(2.0), "A"),
2298 slot(SplitSize::Floating(1.0), "B"),
2299 ];
2300 let sizes = resolve_split_sizes(&slots, 1e308).unwrap();
2301 assert!(sizes[0].is_finite(), "size[0] overflowed to {}", sizes[0]);
2302 assert!(sizes[1].is_finite(), "size[1] overflowed to {}", sizes[1]);
2303 assert!((sizes[0] / sizes[1] - 2.0).abs() < 1e-6);
2305 }
2306
2307 #[test]
2310 fn test_split_floating_weight_overflow_rejected() {
2311 let slots = vec![
2314 slot(SplitSize::Floating(f64::MAX), "A"),
2315 slot(SplitSize::Floating(f64::MAX), "B"),
2316 ];
2317 assert!(matches!(
2318 resolve_split_sizes(&slots, 10.0),
2319 Err(ShapeError::InvalidNumericValue)
2320 ));
2321 }
2322
2323 #[test]
2324 fn test_stochastic_rule_deterministic_with_seed() {
2325 let mut interp = Interpreter::new();
2326 interp
2327 .add_weighted_rules(
2328 "Facade",
2329 vec![
2330 (70.0, vec![ShapeOp::I("Brick".to_string())]),
2331 (30.0, vec![ShapeOp::I("Glass".to_string())]),
2332 ],
2333 )
2334 .unwrap();
2335 interp.seed = 42;
2336 let m1 = interp.derive(Scope::unit(), "Facade").unwrap();
2338 let m2 = interp.derive(Scope::unit(), "Facade").unwrap();
2339 assert_eq!(m1.terminals[0].mesh_id, m2.terminals[0].mesh_id);
2340 }
2341
2342 #[test]
2343 fn test_face_comp_orientations() {
2344 let mut interp = Interpreter::new();
2348 interp.add_rule(
2349 "Box",
2350 vec![ShapeOp::Comp(CompTarget::Faces(vec![
2351 crate::ops::CompFaceCase {
2352 selector: FaceSelector::All,
2353 rule: "Face".to_string(),
2354 },
2355 ]))],
2356 );
2357 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 2.0));
2358 let model = interp.derive(scope, "Box").unwrap();
2359 assert_eq!(model.len(), 6);
2360
2361 let normals: Vec<Vec3> = model
2363 .terminals
2364 .iter()
2365 .map(|t| t.scope.rotation * Vec3::Z)
2366 .collect();
2367
2368 let expected = [
2370 Vec3::NEG_Y, Vec3::Y, Vec3::NEG_Z, Vec3::Z, Vec3::NEG_X, Vec3::X, ];
2377 for exp in &expected {
2378 assert!(
2379 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
2380 "missing normal {:?}, got {:?}",
2381 exp,
2382 normals
2383 );
2384 }
2385
2386 let pos = |i: usize| model.terminals[i].scope.position;
2390 assert!(
2391 (pos(0) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
2392 "Bottom pos"
2393 ); assert!(
2395 (pos(1) - Vec3::new(0.0, 3.0, 2.0)).length() < 1e-6,
2396 "Top pos"
2397 ); assert!(
2399 (pos(2) - Vec3::new(4.0, 0.0, 0.0)).length() < 1e-6,
2400 "Front pos"
2401 ); assert!(
2403 (pos(3) - Vec3::new(0.0, 0.0, 2.0)).length() < 1e-6,
2404 "Back pos"
2405 ); assert!(
2407 (pos(4) - Vec3::new(0.0, 0.0, 0.0)).length() < 1e-6,
2408 "Left pos"
2409 ); assert!(
2411 (pos(5) - Vec3::new(4.0, 0.0, 2.0)).length() < 1e-6,
2412 "Right pos"
2413 ); }
2415
2416 #[test]
2419 fn test_negative_scope_size_rejected() {
2420 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(-1.0, 1.0, 1.0));
2421 let interp = Interpreter::new();
2422 assert!(matches!(
2423 interp.derive(scope, "Anything"),
2424 Err(ShapeError::InvalidNumericValue)
2425 ));
2426 }
2427
2428 #[test]
2429 fn test_zero_scope_size_accepted() {
2430 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 0.0, 10.0));
2432 let interp = Interpreter::new();
2433 let model = interp.derive(scope, "Footprint").unwrap();
2434 assert_eq!(model.len(), 1);
2435 }
2436
2437 #[test]
2440 fn test_unnormalized_root_quat_rejected() {
2441 let bad_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
2443 let scope = Scope::new(Vec3::ZERO, bad_q, Vec3::ONE);
2444 let interp = Interpreter::new();
2445 assert!(matches!(
2446 interp.derive(scope, "Anything"),
2447 Err(ShapeError::InvalidNumericValue)
2448 ));
2449 }
2450
2451 #[test]
2452 fn test_degenerate_rotate_op_rejected() {
2453 let zero_q = Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
2455 let mut interp = Interpreter::new();
2456 interp.add_rule(
2457 "R",
2458 vec![ShapeOp::Rotate(zero_q), ShapeOp::I("M".to_string())],
2459 );
2460 assert!(matches!(
2461 interp.derive(Scope::unit(), "R"),
2462 Err(ShapeError::InvalidNumericValue)
2463 ));
2464 }
2465
2466 #[test]
2467 fn test_scaled_rotate_op_normalized() {
2468 let scaled_q = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0); let mut interp = Interpreter::new();
2472 interp.add_rule(
2473 "R",
2474 vec![ShapeOp::Rotate(scaled_q), ShapeOp::I("M".to_string())],
2475 );
2476 let model = interp.derive(Scope::unit(), "R").unwrap();
2478 assert_eq!(model.len(), 1);
2479 let r = model.terminals[0].scope.rotation;
2480 assert!(
2481 (r.length_squared() - 1.0).abs() < 1e-9,
2482 "rotation should be unit"
2483 );
2484 }
2485
2486 #[test]
2489 fn test_nan_weight_rejected() {
2490 let mut interp = Interpreter::new();
2491 assert!(matches!(
2492 interp.add_weighted_rules("R", vec![(f64::NAN, vec![ShapeOp::I("M".to_string())])]),
2493 Err(ShapeError::InvalidNumericValue)
2494 ));
2495 }
2496
2497 #[test]
2498 fn test_infinite_weight_rejected() {
2499 let mut interp = Interpreter::new();
2500 assert!(matches!(
2501 interp.add_weighted_rules(
2502 "R",
2503 vec![(f64::INFINITY, vec![ShapeOp::I("M".to_string())])]
2504 ),
2505 Err(ShapeError::InvalidNumericValue)
2506 ));
2507 }
2508
2509 #[test]
2510 fn test_negative_weight_rejected() {
2511 let mut interp = Interpreter::new();
2512 assert!(matches!(
2513 interp.add_weighted_rules("R", vec![(-1.0, vec![ShapeOp::I("M".to_string())])]),
2514 Err(ShapeError::InvalidNumericValue)
2515 ));
2516 }
2517
2518 #[test]
2521 fn test_align_y_to_world_up_when_rotated() {
2522 let mut interp = Interpreter::new();
2524 let ninety_z = Quat::from_axis_angle(Vec3::Z, std::f64::consts::FRAC_PI_2);
2525 interp.add_rule(
2526 "R",
2527 vec![
2528 ShapeOp::Rotate(ninety_z),
2529 ShapeOp::Align {
2530 local_axis: Axis::Y,
2531 target: Vec3::Y,
2532 },
2533 ShapeOp::I("M".to_string()),
2534 ],
2535 );
2536 let model = interp.derive(Scope::unit(), "R").unwrap();
2537 assert_eq!(model.len(), 1);
2538 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
2539 assert!(
2540 (world_y - Vec3::Y).length() < 1e-6,
2541 "expected Y=(0,1,0), got {:?}",
2542 world_y
2543 );
2544 }
2545
2546 #[test]
2547 fn test_align_already_aligned_is_noop() {
2548 let mut interp = Interpreter::new();
2549 interp.add_rule(
2550 "R",
2551 vec![
2552 ShapeOp::Align {
2553 local_axis: Axis::Y,
2554 target: Vec3::Y,
2555 },
2556 ShapeOp::I("M".to_string()),
2557 ],
2558 );
2559 let model = interp.derive(Scope::unit(), "R").unwrap();
2560 let rot = model.terminals[0].scope.rotation;
2561 assert!((rot.length_squared() - 1.0).abs() < 1e-9);
2562 let world_y = rot * Vec3::Y;
2564 assert!((world_y - Vec3::Y).length() < 1e-6);
2565 }
2566
2567 #[test]
2568 fn test_align_zero_target_rejected() {
2569 let mut interp = Interpreter::new();
2570 interp.add_rule(
2571 "R",
2572 vec![
2573 ShapeOp::Align {
2574 local_axis: Axis::Y,
2575 target: Vec3::ZERO,
2576 },
2577 ShapeOp::I("M".to_string()),
2578 ],
2579 );
2580 assert!(matches!(
2581 interp.derive(Scope::unit(), "R"),
2582 Err(ShapeError::InvalidAlignTarget)
2583 ));
2584 }
2585
2586 #[test]
2589 fn test_offset_inset_produces_inside_and_border() {
2590 let mut interp = Interpreter::new();
2591 interp.add_rule(
2592 "R",
2593 vec![ShapeOp::Offset {
2594 distance: -0.5,
2595 cases: vec![
2596 crate::ops::OffsetCase {
2597 selector: crate::ops::OffsetSelector::Inside,
2598 rule: "Glass".to_string(),
2599 },
2600 crate::ops::OffsetCase {
2601 selector: crate::ops::OffsetSelector::Border,
2602 rule: "Frame".to_string(),
2603 },
2604 ],
2605 }],
2606 );
2607 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
2609 let model = interp.derive(scope, "R").unwrap();
2610 assert_eq!(model.len(), 5);
2612 let inside = model
2614 .terminals
2615 .iter()
2616 .find(|t| t.mesh_id == "Glass")
2617 .unwrap();
2618 assert!((inside.scope.size.x - 3.0).abs() < 1e-9);
2619 assert!((inside.scope.size.y - 2.0).abs() < 1e-9);
2620 assert!((inside.scope.position - Vec3::new(0.5, 0.5, 0.0)).length() < 1e-9);
2621 }
2622
2623 #[test]
2624 fn test_offset_too_large_rejected() {
2625 let mut interp = Interpreter::new();
2626 interp.add_rule(
2627 "R",
2628 vec![ShapeOp::Offset {
2629 distance: -2.0, cases: vec![crate::ops::OffsetCase {
2631 selector: crate::ops::OffsetSelector::Inside,
2632 rule: "A".to_string(),
2633 }],
2634 }],
2635 );
2636 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 3.0, 0.0));
2637 assert!(matches!(
2638 interp.derive(scope, "R"),
2639 Err(ShapeError::OffsetTooLarge)
2640 ));
2641 }
2642
2643 #[test]
2644 fn test_offset_positive_distance_rejected() {
2645 let mut interp = Interpreter::new();
2646 interp.add_rule(
2647 "R",
2648 vec![ShapeOp::Offset {
2649 distance: 0.2,
2650 cases: vec![crate::ops::OffsetCase {
2651 selector: crate::ops::OffsetSelector::Inside,
2652 rule: "A".to_string(),
2653 }],
2654 }],
2655 );
2656 assert!(matches!(
2657 interp.derive(Scope::unit(), "R"),
2658 Err(ShapeError::InvalidNumericValue)
2659 ));
2660 }
2661
2662 #[test]
2665 fn test_roof_shed_produces_one_slope() {
2666 let mut interp = Interpreter::new();
2667 interp.add_rule(
2668 "R",
2669 vec![ShapeOp::Roof {
2670 config: RoofConfig::new(RoofType::Shed, 30.0),
2671 cases: vec![crate::ops::RoofCase {
2672 selector: RoofFaceSelector::Slope,
2673 rule: "Tiles".to_string(),
2674 }],
2675 }],
2676 );
2677 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
2678 let model = interp.derive(scope, "R").unwrap();
2679 assert_eq!(model.len(), 1);
2680 assert_eq!(model.terminals[0].mesh_id, "Tiles");
2681 assert!((model.terminals[0].scope.position.y - 0.0).abs() < 1e-6);
2683 }
2684
2685 #[test]
2686 fn test_roof_gable_produces_four_panels() {
2687 let mut interp = Interpreter::new();
2688 interp.add_rule(
2689 "R",
2690 vec![ShapeOp::Roof {
2691 config: RoofConfig::new(RoofType::Gable, 30.0),
2692 cases: vec![
2693 crate::ops::RoofCase {
2694 selector: RoofFaceSelector::Slope,
2695 rule: "Tiles".to_string(),
2696 },
2697 crate::ops::RoofCase {
2698 selector: RoofFaceSelector::GableEnd,
2699 rule: "Bricks".to_string(),
2700 },
2701 ],
2702 }],
2703 );
2704 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 5.0, 8.0));
2705 let model = interp.derive(scope, "R").unwrap();
2706 assert_eq!(model.len(), 4);
2708 let tiles: Vec<_> = model
2709 .terminals
2710 .iter()
2711 .filter(|t| t.mesh_id == "Tiles")
2712 .collect();
2713 let bricks: Vec<_> = model
2714 .terminals
2715 .iter()
2716 .filter(|t| t.mesh_id == "Bricks")
2717 .collect();
2718 assert_eq!(tiles.len(), 2);
2719 assert_eq!(bricks.len(), 2);
2720 }
2721
2722 #[test]
2723 fn test_roof_hip_produces_four_slopes() {
2724 let mut interp = Interpreter::new();
2725 interp.add_rule(
2726 "R",
2727 vec![ShapeOp::Roof {
2728 config: {
2729 let mut c = RoofConfig::new(RoofType::Hip, 45.0);
2730 c.overhang = 0.3;
2731 c
2732 },
2733 cases: vec![crate::ops::RoofCase {
2734 selector: RoofFaceSelector::Slope,
2735 rule: "Tiles".to_string(),
2736 }],
2737 }],
2738 );
2739 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
2740 let model = interp.derive(scope, "R").unwrap();
2741 assert_eq!(model.len(), 4);
2742 }
2743
2744 #[test]
2745 fn test_roof_pyramid_produces_four_tapered_slopes() {
2746 let mut interp = Interpreter::new();
2747 interp.add_rule(
2748 "R",
2749 vec![ShapeOp::Roof {
2750 config: RoofConfig::new(RoofType::Pyramid, 40.0),
2751 cases: vec![crate::ops::RoofCase {
2752 selector: RoofFaceSelector::Slope,
2753 rule: "Tiles".to_string(),
2754 }],
2755 }],
2756 );
2757 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(6.0, 3.0, 6.0));
2758 let model = interp.derive(scope, "R").unwrap();
2759 assert_eq!(model.len(), 4);
2760 for t in &model.terminals {
2762 assert!(
2763 matches!(t.face_profile, FaceProfile::Triangle { peak_offset } if (peak_offset - 0.5).abs() < 1e-9),
2764 "expected Triangle{{peak_offset=0.5}}, got {:?}",
2765 t.face_profile
2766 );
2767 }
2768 }
2769
2770 #[test]
2771 fn test_roof_slope_normals_outward() {
2772 let alpha: f64 = 30_f64.to_radians();
2777 let cos_a = alpha.cos();
2778 let sin_a = alpha.sin();
2779 let mut interp = Interpreter::new();
2780 interp.add_rule(
2781 "R",
2782 vec![ShapeOp::Roof {
2783 config: RoofConfig::new(RoofType::Hip, 30.0),
2784 cases: vec![crate::ops::RoofCase {
2785 selector: RoofFaceSelector::Slope,
2786 rule: "S".to_string(),
2787 }],
2788 }],
2789 );
2790 let scope = Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(10.0, 4.0, 8.0));
2791 let model = interp.derive(scope, "R").unwrap();
2792 assert_eq!(model.len(), 4);
2793 let normals: Vec<Vec3> = model
2794 .terminals
2795 .iter()
2796 .map(|t| t.scope.rotation * Vec3::Z)
2797 .collect();
2798 let expected = [
2799 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), ];
2804 for exp in &expected {
2805 assert!(
2806 normals.iter().any(|n| (*n - *exp).length() < 1e-6),
2807 "missing outward normal {:?}; got {:?}",
2808 exp,
2809 normals
2810 );
2811 }
2812 for n in &normals {
2814 assert!(n.y > 0.0, "normal pointing downward: {:?}", n);
2815 }
2816 }
2817
2818 #[test]
2819 fn test_align_antiparallel_fallback_no_nan() {
2820 let flip_y = Quat::from_axis_angle(Vec3::Z, PI);
2824 let mut interp = Interpreter::new();
2825 interp.add_rule(
2826 "R",
2827 vec![
2828 ShapeOp::Rotate(flip_y),
2829 ShapeOp::Align {
2830 local_axis: Axis::Y,
2831 target: Vec3::Y,
2832 },
2833 ShapeOp::I("M".to_string()),
2834 ],
2835 );
2836 let model = interp.derive(Scope::unit(), "R").unwrap();
2837 let world_y = model.terminals[0].scope.rotation * Vec3::Y;
2838 assert!(
2839 (world_y - Vec3::Y).length() < 1e-6,
2840 "anti-parallel Align should point Y to world up, got {:?}",
2841 world_y
2842 );
2843 let r = model.terminals[0].scope.rotation;
2845 assert!(
2846 (r.length_squared() - 1.0).abs() < 1e-9,
2847 "rotation not unit: length_sq={}",
2848 r.length_squared()
2849 );
2850 }
2851
2852 #[test]
2853 fn test_roof_invalid_angle_rejected() {
2854 let mut interp = Interpreter::new();
2855 interp.add_rule(
2856 "R",
2857 vec![ShapeOp::Roof {
2858 config: RoofConfig::new(RoofType::Shed, 0.0),
2859 cases: vec![],
2860 }],
2861 );
2862 assert!(matches!(
2863 interp.derive(Scope::unit(), "R"),
2864 Err(ShapeError::InvalidRoofAngle(_))
2865 ));
2866 }
2867}