Skip to main content

symbios_shape/
ops.rs

1use serde::{Deserialize, Serialize};
2
3use crate::scope::{Quat, Vec3};
4
5/// The axis along which a `Split` or `Repeat` operation acts.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum Axis {
8    X,
9    Y,
10    Z,
11}
12
13/// Sizing mode for a single slot within a `Split` operation.
14///
15/// Mirrors CityEngine CGA syntax:
16/// - `Absolute(n)`: a fixed world-unit size.
17/// - `Relative(t)`: a fraction `t` of the scope's total dimension (prefix `'` in CGA text).
18/// - `Floating(n)`: a weight that shares the remaining space after absolutes are consumed
19///   (prefix `~` in CGA text). Multiple floating slots divide the remainder proportionally.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum SplitSize {
22    Absolute(f64),
23    Relative(f64),
24    Floating(f64),
25}
26
27impl SplitSize {
28    pub fn is_valid(&self) -> bool {
29        match self {
30            SplitSize::Absolute(v) | SplitSize::Relative(v) | SplitSize::Floating(v) => {
31                v.is_finite() && *v > 0.0
32            }
33        }
34    }
35}
36
37/// A single slot in a `Split` operation: a size mode paired with a successor rule name.
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct SplitSlot {
40    pub size: SplitSize,
41    /// The name of the shape rule to invoke on the resulting child scope.
42    pub rule: String,
43}
44
45/// Face selectors for the `Comp(Faces)` decomposition.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub enum FaceSelector {
48    Top,
49    Bottom,
50    Front,
51    Back,
52    Left,
53    Right,
54    /// Matches all non-top, non-bottom faces (shorthand for the four sides).
55    Side,
56    /// Matches all faces not otherwise mapped.
57    All,
58}
59
60impl FaceSelector {
61    pub fn parse(s: &str) -> Option<Self> {
62        match s {
63            "top" | "Top" => Some(Self::Top),
64            "bottom" | "Bottom" => Some(Self::Bottom),
65            "front" | "Front" => Some(Self::Front),
66            "back" | "Back" => Some(Self::Back),
67            "left" | "Left" => Some(Self::Left),
68            "right" | "Right" => Some(Self::Right),
69            "side" | "Side" => Some(Self::Side),
70            "all" | "All" | "_" => Some(Self::All),
71            _ => None,
72        }
73    }
74}
75
76/// A single mapping in a `Comp(Faces)` block: a face selector → rule name.
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct CompFaceCase {
79    pub selector: FaceSelector,
80    pub rule: String,
81}
82
83/// The decomposition target for a `Comp` operation.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum CompTarget {
86    /// Decomposes the volume into its six axis-aligned face scopes.
87    Faces(Vec<CompFaceCase>),
88}
89
90/// Face selectors for the `Offset` operation.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
92pub enum OffsetSelector {
93    /// The inset/outset region (the area inside the border).
94    Inside,
95    /// The surrounding border strips.
96    Border,
97    /// Matches any selector not otherwise mapped.
98    All,
99}
100
101impl OffsetSelector {
102    pub fn parse(s: &str) -> Option<Self> {
103        match s {
104            "inside" | "Inside" => Some(Self::Inside),
105            "border" | "Border" => Some(Self::Border),
106            "all" | "All" | "_" => Some(Self::All),
107            _ => None,
108        }
109    }
110}
111
112/// A single mapping in an `Offset` block: a selector → rule name.
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct OffsetCase {
115    pub selector: OffsetSelector,
116    pub rule: String,
117}
118
119/// Roof shape types for the `Roof` operation.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121pub enum RoofType {
122    // ── Original types ────────────────────────────────────────────────────────
123    /// Four triangular slope panels meeting at a single apex.
124    Pyramid,
125    /// Single slope panel from front eave to back eave.
126    Shed,
127    /// Two slope panels meeting at a horizontal ridge; two triangular gable ends.
128    Gable,
129    /// Four trapezoidal slope panels meeting at a horizontal ridge.
130    Hip,
131
132    // ── New types ─────────────────────────────────────────────────────────────
133    /// Flat horizontal roof — a single horizontal panel covering the scope top.
134    Flat,
135    /// Two rectangular slope panels only (Gable without the triangular end panels).
136    OpenGable,
137    /// Two slope panels + two rectangular (non-tapered) gable-end wall panels.
138    BoxGable,
139    /// Four panels from a rectangular base meeting at a single apex point (no ridge).
140    /// Equivalent to `Pyramid` for square footprints; left/right end panels are triangular.
141    PyramidHip,
142    /// Two inward-tilting slopes forming a central valley (inverted Gable).
143    Butterfly,
144    /// Four panels forming two parallel ridges with a central valley between them (M profile).
145    MShaped,
146    /// Two pitches per slope: steeper lower zone + shallower upper zone (barn roof).
147    /// Requires `secondary_pitch` in `RoofConfig`.
148    Gambrel,
149    /// Gambrel applied to all four sides: 4 steep lower panels + 4 shallow upper panels.
150    /// Requires `secondary_pitch` in `RoofConfig`.
151    Mansard,
152    /// Asymmetric Gable: the ridge is offset toward one end (`ridge_offset` in `RoofConfig`).
153    /// Front slope is steeper; back slope is shallower. Gable ends are asymmetric triangles.
154    Saltbox,
155    /// Gable with clipped hip ends: the upper corners of each gable end are replaced by
156    /// small triangular hip panels. Controlled by `tier_height` in `RoofConfig`.
157    Jerkinhead,
158    /// Hip roof with a small gable rising from the ridge centre.
159    /// Controlled by `tier_height` (fraction of slope from base where the gable starts).
160    DutchGable,
161}
162
163impl RoofType {
164    pub fn parse(s: &str) -> Option<Self> {
165        match s {
166            "pyramid" | "Pyramid" => Some(Self::Pyramid),
167            "shed" | "Shed" => Some(Self::Shed),
168            "gable" | "Gable" => Some(Self::Gable),
169            "hip" | "Hip" => Some(Self::Hip),
170            "flat" | "Flat" => Some(Self::Flat),
171            "openGable" | "OpenGable" => Some(Self::OpenGable),
172            "boxGable" | "BoxGable" => Some(Self::BoxGable),
173            "pyramidHip" | "PyramidHip" => Some(Self::PyramidHip),
174            "butterfly" | "Butterfly" => Some(Self::Butterfly),
175            "mShaped" | "MShaped" => Some(Self::MShaped),
176            "gambrel" | "Gambrel" => Some(Self::Gambrel),
177            "mansard" | "Mansard" => Some(Self::Mansard),
178            "saltbox" | "Saltbox" => Some(Self::Saltbox),
179            "jerkinhead" | "Jerkinhead" => Some(Self::Jerkinhead),
180            "dutchGable" | "DutchGable" => Some(Self::DutchGable),
181            _ => None,
182        }
183    }
184}
185
186/// Face selectors for the `Roof` operation.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
188pub enum RoofFaceSelector {
189    /// The main sloped panel(s) — front/back in most roof types.
190    Slope,
191    /// The triangular vertical end panels of a Gable or Saltbox roof.
192    GableEnd,
193    /// The steeper, lower zone of a Gambrel or Mansard roof.
194    LowerSlope,
195    /// The shallower, upper zone of a Gambrel or Mansard roof.
196    UpperSlope,
197    /// The small triangular hip panels at the clipped ends of a Jerkinhead roof.
198    HipEnd,
199    /// The inward-facing slopes of a Butterfly or MShaped valley.
200    ValleySlope,
201    /// The outer slopes of an MShaped roof (facing away from the valley).
202    OuterSlope,
203    /// The inner slopes of an MShaped roof (facing toward the valley).
204    InnerSlope,
205    /// Matches any selector not otherwise mapped.
206    All,
207}
208
209impl RoofFaceSelector {
210    pub fn parse(s: &str) -> Option<Self> {
211        match s {
212            "slope" | "Slope" => Some(Self::Slope),
213            "gable" | "GableEnd" | "gableEnd" => Some(Self::GableEnd),
214            "lowerSlope" | "LowerSlope" => Some(Self::LowerSlope),
215            "upperSlope" | "UpperSlope" => Some(Self::UpperSlope),
216            "hipEnd" | "HipEnd" => Some(Self::HipEnd),
217            "valleySlope" | "ValleySlope" => Some(Self::ValleySlope),
218            "outerSlope" | "OuterSlope" => Some(Self::OuterSlope),
219            "innerSlope" | "InnerSlope" => Some(Self::InnerSlope),
220            "all" | "All" | "_" => Some(Self::All),
221            _ => None,
222        }
223    }
224}
225
226/// A single mapping in a `Roof` block: a face selector → rule name.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct RoofCase {
229    pub selector: RoofFaceSelector,
230    pub rule: String,
231}
232
233/// Rich parametric configuration for the `Roof` operation.
234///
235/// All angular values are in degrees. Lengths are in world units.
236/// Optional fields default as described; see each field doc.
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct RoofConfig {
239    pub roof_type: RoofType,
240    /// Primary pitch angle in degrees from horizontal. Must be in (0°, 90°).
241    pub pitch: f64,
242    /// Secondary pitch angle in degrees. Used by `Gambrel` (upper zone) and `Mansard`.
243    /// If `None` when required, defaults to `pitch / 2`.
244    pub secondary_pitch: Option<f64>,
245    /// Extra overhang beyond the scope footprint on each side. Default `0.0`.
246    pub overhang: f64,
247    /// Ridge offset for `Saltbox`: fraction [0, 1] of the scope depth (Z) where the
248    /// ridge is positioned from the front. Default `0.5` (symmetric / centred ridge).
249    pub ridge_offset: f64,
250    /// Thickness of the roof fascia edge in world units. Default `0.0` (flat panels).
251    pub fascia_depth: f64,
252    /// Normalised height at which the pitch break occurs for `Gambrel`, `Mansard`,
253    /// `Jerkinhead`, and `DutchGable`. `0.5` means the break is at half the eave-to-ridge
254    /// distance. `None` uses a type-specific default.
255    pub tier_height: Option<f64>,
256}
257
258impl RoofConfig {
259    /// Creates a minimal config for deterministic types (Pyramid, Shed, Gable, Hip, Flat, …).
260    pub fn new(roof_type: RoofType, pitch: f64) -> Self {
261        Self {
262            roof_type,
263            pitch,
264            secondary_pitch: None,
265            overhang: 0.0,
266            ridge_offset: 0.5,
267            fascia_depth: 0.0,
268            tier_height: None,
269        }
270    }
271
272    /// Returns the secondary pitch, defaulting to `pitch / 2` if unset.
273    pub fn secondary_pitch_or_default(&self) -> f64 {
274        self.secondary_pitch.unwrap_or(self.pitch / 2.0)
275    }
276
277    /// Returns the tier height, defaulting to `default` if unset.
278    pub fn tier_height_or(&self, default: f64) -> f64 {
279        self.tier_height.unwrap_or(default)
280    }
281}
282
283/// Selector for the `Attach` operation.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
285pub enum AttachSelector {
286    /// The projected scope that sits on (or comes out of) the surface.
287    Surface,
288    /// Matches any selector not otherwise mapped.
289    All,
290}
291
292impl AttachSelector {
293    pub fn parse(s: &str) -> Option<Self> {
294        match s {
295            "surface" | "Surface" => Some(Self::Surface),
296            "all" | "All" | "_" => Some(Self::All),
297            _ => None,
298        }
299    }
300}
301
302/// A single mapping in an `Attach` block: a selector → rule name.
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
304pub struct AttachCase {
305    pub selector: AttachSelector,
306    pub rule: String,
307}
308
309/// The atomic CGA operations that the interpreter executes.
310///
311/// Every operation transforms the current `Scope` into zero or more child scopes,
312/// each tagged with a rule name that will be recursively evaluated.
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub enum ShapeOp {
315    /// Lifts a 2-D footprint (XZ plane) into a 3-D volume by setting the Y size.
316    Extrude(f64),
317
318    /// Pyramidal taper: scales the top face toward the centroid.
319    /// `amount` ∈ `[0, 1]`: 0 = no taper, 1 = full pyramid (top collapses to a point).
320    Taper(f64),
321
322    /// Applies an additional rotation to the scope (cumulative with existing rotation).
323    Rotate(Quat),
324
325    /// Translates the scope origin in local space.
326    Translate(Vec3),
327
328    /// Scales the scope size along each axis (multiplicative).
329    Scale(Vec3),
330
331    /// Divides the scope along `axis` into ordered slots.
332    Split { axis: Axis, slots: Vec<SplitSlot> },
333
334    /// Tiles the scope along `axis` with child scopes of approximate size `tile_size`.
335    /// The tile count is `floor(total / tile_size)`; the actual tile size is then
336    /// stretched to `total / count` so the tiles fill the scope exactly with no gap.
337    Repeat {
338        axis: Axis,
339        tile_size: f64,
340        rule: String,
341    },
342
343    /// Decomposes the scope into its geometric components (faces, edges, vertices).
344    Comp(CompTarget),
345
346    /// Terminal: replace the scope with the named mesh asset.
347    /// This is the "terminal symbol" — produces a `Terminal` node in the output model.
348    I(String),
349
350    /// Sets the material identifier on the current work item.
351    /// The material is propagated to the final `Terminal`, allowing downstream
352    /// renderers to apply textures/shaders without changing the scope.
353    /// Syntax: `Mat("Brick")` or `Mat(Brick)`.
354    Mat(String),
355
356    /// Calls a named sub-rule on the current scope unchanged.
357    /// Used for grammar rule references that don't transform the scope themselves.
358    Rule(String),
359
360    /// Rotates the scope so that the specified local axis points in the given world direction.
361    ///
362    /// Applies the shortest-arc rotation from the current world direction of `local_axis`
363    /// to `target`. Useful for recovering from accumulated rotations.
364    /// Syntax: `Align(Y, Up)`, `Align(Z, Forward)`, etc.
365    /// Named targets: `Up`=(0,1,0), `Down`=(0,-1,0), `Right`=(1,0,0), `Left`=(-1,0,0),
366    /// `Forward`=(0,0,-1), `Back`=(0,0,1).
367    Align { local_axis: Axis, target: Vec3 },
368
369    /// Creates an inset (`distance < 0`) frame on a 2D face scope.
370    ///
371    /// Produces up to two kinds of child scopes:
372    /// - `Inside`: the inset rectangle.
373    /// - `Border`: four surrounding strips (bottom, top, left, right), each invoking the same rule.
374    ///
375    /// Syntax: `Offset(-0.2) { Inside: Glass | Border: Frame }`
376    Offset {
377        distance: f64,
378        cases: Vec<OffsetCase>,
379    },
380
381    /// Generates a roof structure above the current scope using rich parametric configuration.
382    ///
383    /// Operates on a volume scope. The `config` contains the roof type, primary pitch angle,
384    /// optional secondary pitch, overhang, ridge offset, fascia depth, and tier height.
385    ///
386    /// Syntax examples:
387    /// - `Roof(Gable, 30) { Slope: Tiles | GableEnd: Bricks }` — basic Gable
388    /// - `Roof(Hip, 30, 0.5) { Slope: Tiles }` — Hip with overhang
389    /// - `Roof(Gambrel, 45, 20) { LowerSlope: Shingles | UpperSlope: Tiles }` — Gambrel
390    /// - `Roof(Saltbox, 45, offset=0.3) { Slope: Tiles | GableEnd: Bricks }` — Saltbox
391    /// - `Roof(DutchGable, 45, tier=0.7) { Slope: Tiles | GableEnd: Bricks }` — Dutch Gable
392    Roof {
393        config: RoofConfig,
394        cases: Vec<RoofCase>,
395    },
396
397    /// Projects a new horizontal scope out of a sloped face for attaching dormers or details.
398    ///
399    /// `world_axis` defines the "up" direction for the attached scope (usually world Y).
400    /// The resulting scope sits on the face's surface with its Y axis aligned to `world_axis`,
401    /// inheriting the face's width and height but with depth = 0.
402    ///
403    /// Syntax: `Attach(Up) { Surface: DormerMass }`
404    Attach {
405        world_axis: Vec3,
406        cases: Vec<AttachCase>,
407    },
408}