Skip to main content

step_io/scene/
presentation.rs

1//! Presentation — the colour, transparency, layer, and visibility of a shape.
2//!
3//! These surface as accessors on the geometry handles: `.color()`,
4//! `.transparency()`, `.layer()`, and `.is_visible()`. A style the reader
5//! cannot follow to a value — a complex style, or an unknown pre-defined
6//! colour name — is recorded via [`Scene::warnings`](crate::scene::Scene)
7//! rather than skipped silently.
8
9use crate::generated::model as m;
10use crate::scene::Ctx;
11
12/// An RGB colour with components in `[0, 1]`.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct Rgb {
15    pub red: f64,
16    pub green: f64,
17    pub blue: f64,
18}
19
20/// The colour styled onto the entity `target` (a face or solid), if any.
21pub(crate) fn colour_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<Rgb> {
22    let rg = cx.ref_graph();
23    for r in rg.referrers(target) {
24        // A STYLED_ITEM references `target` only through its `item` field, so any
25        // styled item referring to `target` styles it.
26        let m::EntityKey::StyledItem(sid) = r else {
27            continue;
28        };
29        let si = cx.model.styled_item_arena.get(sid.0);
30        if let Some(rgb) = si.styles.iter().find_map(|psa| psa_colour(cx, psa)) {
31            return Some(rgb);
32        }
33    }
34    None
35}
36
37fn psa_colour(cx: Ctx<'_>, r: &m::PresentationStyleAssignmentRef) -> Option<Rgb> {
38    let styles: &[m::PresentationStyleSelectRef] = match r {
39        m::PresentationStyleAssignmentRef::PresentationStyleAssignment(i) => {
40            &cx.model.presentation_style_assignment_arena.get(i.0).styles
41        }
42        m::PresentationStyleAssignmentRef::PresentationStyleByContext(i) => {
43            &cx.model.presentation_style_by_context_arena.get(i.0).styles
44        }
45        m::PresentationStyleAssignmentRef::Complex(_) => {
46            cx.warn("styled item uses a complex presentation style assignment".to_owned());
47            return None;
48        }
49    };
50    styles.iter().find_map(|s| style_select_colour(cx, s))
51}
52
53fn style_select_colour(cx: Ctx<'_>, r: &m::PresentationStyleSelectRef) -> Option<Rgb> {
54    match r {
55        m::PresentationStyleSelectRef::SurfaceStyleUsage(i) => {
56            let ssu = cx.model.surface_style_usage_arena.get(i.0);
57            side_style_colour(cx, &ssu.style)
58        }
59        m::PresentationStyleSelectRef::FillAreaStyle(i) => fill_area_colour(cx, *i),
60        m::PresentationStyleSelectRef::Complex(_) => {
61            cx.warn("complex presentation style".to_owned());
62            None
63        }
64        // Curve / point / text / pre-defined styles carry no surface colour.
65        _ => None,
66    }
67}
68
69fn side_style_colour(cx: Ctx<'_>, r: &m::SurfaceSideStyleSelectRef) -> Option<Rgb> {
70    match r {
71        m::SurfaceSideStyleSelectRef::SurfaceSideStyle(i) => cx
72            .model
73            .surface_side_style_arena
74            .get(i.0)
75            .styles
76            .iter()
77            .find_map(|e| element_colour(cx, e)),
78        m::SurfaceSideStyleSelectRef::Complex(_) => {
79            cx.warn("complex surface side style".to_owned());
80            None
81        }
82        m::SurfaceSideStyleSelectRef::PreDefinedSurfaceSideStyle(_) => None,
83    }
84}
85
86fn element_colour(cx: Ctx<'_>, r: &m::SurfaceStyleElementSelectRef) -> Option<Rgb> {
87    match r {
88        m::SurfaceStyleElementSelectRef::SurfaceStyleFillArea(i) => fill_area_ref_colour(
89            cx,
90            &cx.model.surface_style_fill_area_arena.get(i.0).fill_area,
91        ),
92        m::SurfaceStyleElementSelectRef::SurfaceStyleRendering(i) => colour_to_rgb(
93            cx,
94            &cx.model
95                .surface_style_rendering_arena
96                .get(i.0)
97                .surface_colour,
98        ),
99        m::SurfaceStyleElementSelectRef::SurfaceStyleRenderingWithProperties(i) => colour_to_rgb(
100            cx,
101            &cx.model
102                .surface_style_rendering_with_properties_arena
103                .get(i.0)
104                .surface_colour,
105        ),
106        m::SurfaceStyleElementSelectRef::Complex(_) => {
107            cx.warn("complex surface style element".to_owned());
108            None
109        }
110        // Boundary / control-grid / parameter-line / segmentation / silhouette.
111        _ => None,
112    }
113}
114
115fn fill_area_ref_colour(cx: Ctx<'_>, r: &m::FillAreaStyleRef) -> Option<Rgb> {
116    match r {
117        m::FillAreaStyleRef::FillAreaStyle(i) => fill_area_colour(cx, *i),
118        m::FillAreaStyleRef::Complex(_) => {
119            cx.warn("complex fill area style".to_owned());
120            None
121        }
122    }
123}
124
125fn fill_area_colour(cx: Ctx<'_>, id: m::FillAreaStyleId) -> Option<Rgb> {
126    cx.model
127        .fill_area_style_arena
128        .get(id.0)
129        .fill_styles
130        .iter()
131        .find_map(|f| match f {
132            m::FillStyleSelectRef::FillAreaStyleColour(i) => colour_to_rgb(
133                cx,
134                &cx.model.fill_area_style_colour_arena.get(i.0).fill_colour,
135            ),
136            m::FillStyleSelectRef::Complex(_) => {
137                cx.warn("complex fill style".to_owned());
138                None
139            }
140            // Hatching / tiles / texture styles carry no solid colour.
141            _ => None,
142        })
143}
144
145fn colour_to_rgb(cx: Ctx<'_>, r: &m::ColourRef) -> Option<Rgb> {
146    match r {
147        m::ColourRef::ColourRgb(i) => {
148            let c = cx.model.colour_rgb_arena.get(i.0);
149            Some(Rgb {
150                red: c.red,
151                green: c.green,
152                blue: c.blue,
153            })
154        }
155        m::ColourRef::DraughtingPreDefinedColour(i) => {
156            let name = &cx.model.draughting_pre_defined_colour_arena.get(i.0).name;
157            predefined_rgb(name).or_else(|| {
158                cx.warn(format!("unknown pre-defined colour: {name}"));
159                None
160            })
161        }
162        m::ColourRef::Complex(_) => {
163            cx.warn("complex colour".to_owned());
164            None
165        }
166        // Bare COLOUR / colour specification / other pre-defined colour.
167        _ => None,
168    }
169}
170
171/// The eight standard draughting pre-defined colours.
172fn predefined_rgb(name: &str) -> Option<Rgb> {
173    let (red, green, blue) = match name.to_ascii_lowercase().as_str() {
174        "red" => (1.0, 0.0, 0.0),
175        "green" => (0.0, 1.0, 0.0),
176        "blue" => (0.0, 0.0, 1.0),
177        "yellow" => (1.0, 1.0, 0.0),
178        "cyan" => (0.0, 1.0, 1.0),
179        "magenta" => (1.0, 0.0, 1.0),
180        "black" => (0.0, 0.0, 0.0),
181        "white" => (1.0, 1.0, 1.0),
182        _ => return None,
183    };
184    Some(Rgb { red, green, blue })
185}
186
187// ---------------------------------------------------------------------------
188// Transparency
189// ---------------------------------------------------------------------------
190
191/// Reverse-walk from `target` (a face or solid) to each surface-style element
192/// applied to it — `STYLED_ITEM` → presentation-style assignment →
193/// `SURFACE_STYLE_USAGE` → `SURFACE_SIDE_STYLE` → element — running `leaf` on
194/// each and returning its first `Some`. Shared by any per-element surface-style
195/// query (currently transparency).
196fn surface_elements_of<T>(
197    cx: Ctx<'_>,
198    target: m::EntityKey,
199    leaf: impl Fn(Ctx<'_>, &m::SurfaceStyleElementSelectRef) -> Option<T>,
200) -> Option<T> {
201    let rg = cx.ref_graph();
202    for r in rg.referrers(target) {
203        let m::EntityKey::StyledItem(sid) = r else {
204            continue;
205        };
206        let si = cx.model.styled_item_arena.get(sid.0);
207        for psa in &si.styles {
208            let styles: &[m::PresentationStyleSelectRef] = match psa {
209                m::PresentationStyleAssignmentRef::PresentationStyleAssignment(i) => {
210                    &cx.model.presentation_style_assignment_arena.get(i.0).styles
211                }
212                m::PresentationStyleAssignmentRef::PresentationStyleByContext(i) => {
213                    &cx.model.presentation_style_by_context_arena.get(i.0).styles
214                }
215                m::PresentationStyleAssignmentRef::Complex(_) => {
216                    cx.warn("styled item uses a complex presentation style assignment".to_owned());
217                    continue;
218                }
219            };
220            for style in styles {
221                let m::PresentationStyleSelectRef::SurfaceStyleUsage(i) = style else {
222                    continue;
223                };
224                let side = &cx.model.surface_style_usage_arena.get(i.0).style;
225                let elements: &[m::SurfaceStyleElementSelectRef] = match side {
226                    m::SurfaceSideStyleSelectRef::SurfaceSideStyle(i) => {
227                        &cx.model.surface_side_style_arena.get(i.0).styles
228                    }
229                    m::SurfaceSideStyleSelectRef::Complex(_) => {
230                        cx.warn("complex surface side style".to_owned());
231                        continue;
232                    }
233                    m::SurfaceSideStyleSelectRef::PreDefinedSurfaceSideStyle(_) => continue,
234                };
235                if let Some(found) = elements.iter().find_map(|e| leaf(cx, e)) {
236                    return Some(found);
237                }
238            }
239        }
240    }
241    None
242}
243
244/// The transparency on a surface-style element, from a
245/// `SURFACE_STYLE_RENDERING_WITH_PROPERTIES`'s `SURFACE_STYLE_TRANSPARENT`
246/// property (the only place transparency lives).
247fn element_transparency(cx: Ctx<'_>, r: &m::SurfaceStyleElementSelectRef) -> Option<f64> {
248    let m::SurfaceStyleElementSelectRef::SurfaceStyleRenderingWithProperties(i) = r else {
249        return None;
250    };
251    cx.model
252        .surface_style_rendering_with_properties_arena
253        .get(i.0)
254        .properties
255        .iter()
256        .find_map(|p| match p {
257            m::RenderingPropertiesSelectRef::SurfaceStyleTransparent(t) => Some(
258                cx.model
259                    .surface_style_transparent_arena
260                    .get(t.0)
261                    .transparency,
262            ),
263            _ => None,
264        })
265}
266
267/// The transparency styled onto `target` (a face or solid), if any. Raw STEP
268/// value: `0.0` = opaque, `1.0` = fully transparent.
269pub(crate) fn transparency_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<f64> {
270    surface_elements_of(cx, target, element_transparency)
271}
272
273// ---------------------------------------------------------------------------
274// Layer and visibility
275// ---------------------------------------------------------------------------
276//
277// Corpus: a shape usually reaches a layer / invisibility through its STYLED_ITEM
278// (88% / 97%), not directly. So both queries consider `target`'s "presentation
279// delegates" = the target itself plus the styled items that reference it.
280
281/// The styled items that style `target`.
282fn styled_items_of(cx: Ctx<'_>, target: m::EntityKey) -> Vec<m::StyledItemId> {
283    cx.ref_graph()
284        .referrers(target)
285        .iter()
286        .filter_map(|r| match r {
287            m::EntityKey::StyledItem(sid) => Some(*sid),
288            _ => None,
289        })
290        .collect()
291}
292
293/// The layers `target` belongs to — assignments that contain it directly or
294/// contain one of its styled items.
295fn layers_of(cx: Ctx<'_>, target: m::EntityKey) -> Vec<m::PresentationLayerAssignmentId> {
296    let rg = cx.ref_graph();
297    let mut out: Vec<m::PresentationLayerAssignmentId> = rg
298        .referrers(target)
299        .iter()
300        .filter_map(|r| match r {
301            m::EntityKey::PresentationLayerAssignment(pid) => Some(*pid),
302            _ => None,
303        })
304        .collect();
305    for sid in styled_items_of(cx, target) {
306        for r in rg.referrers(m::EntityKey::StyledItem(sid)) {
307            if let m::EntityKey::PresentationLayerAssignment(pid) = r {
308                out.push(*pid);
309            }
310        }
311    }
312    out
313}
314
315/// Whether an `INVISIBILITY` lists `key` among its invisible items.
316fn referred_by_invisibility(cx: Ctx<'_>, key: m::EntityKey) -> bool {
317    cx.ref_graph()
318        .referrers(key)
319        .iter()
320        .any(|r| matches!(r, m::EntityKey::Invisibility(_)))
321}
322
323/// The name of the first layer `target` belongs to, if any.
324pub(crate) fn layer_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<&str> {
325    let pid = *layers_of(cx, target).first()?;
326    Some(&cx.model.presentation_layer_assignment_arena.get(pid.0).name)
327}
328
329/// Whether `target` is visible — i.e. no `INVISIBILITY` hides one of its styled
330/// items or one of its layers. Defaults to visible when nothing hides it.
331pub(crate) fn is_visible(cx: Ctx<'_>, target: m::EntityKey) -> bool {
332    let styled_hidden = styled_items_of(cx, target)
333        .into_iter()
334        .any(|sid| referred_by_invisibility(cx, m::EntityKey::StyledItem(sid)));
335    let layer_hidden = layers_of(cx, target)
336        .into_iter()
337        .any(|pid| referred_by_invisibility(cx, m::EntityKey::PresentationLayerAssignment(pid)));
338    !(styled_hidden || layer_hidden)
339}
340
341// ---------------------------------------------------------------------------
342// Curve style (an edge's / curve's colour, width, and line font)
343// ---------------------------------------------------------------------------
344//
345// Corpus: curve styles apply to standalone curves (LINE / TRIMMED_CURVE / CIRCLE,
346// ~60k) — reached via the `Curve` handle — not b-rep EDGE_CURVEs (498, a
347// two-file Spatial InterOp anomaly).
348
349/// Reverse-walk from `target` to each presentation-style select styling it
350/// (`STYLED_ITEM` → presentation-style assignment → style), running `leaf` on
351/// each and returning its first `Some`.
352fn each_presentation_style<'m, T>(
353    cx: Ctx<'m>,
354    target: m::EntityKey,
355    leaf: impl Fn(Ctx<'m>, &'m m::PresentationStyleSelectRef) -> Option<T>,
356) -> Option<T> {
357    let rg = cx.ref_graph();
358    for r in rg.referrers(target) {
359        let m::EntityKey::StyledItem(sid) = r else {
360            continue;
361        };
362        for psa in &cx.model.styled_item_arena.get(sid.0).styles {
363            let styles: &[m::PresentationStyleSelectRef] = match psa {
364                m::PresentationStyleAssignmentRef::PresentationStyleAssignment(i) => {
365                    &cx.model.presentation_style_assignment_arena.get(i.0).styles
366                }
367                m::PresentationStyleAssignmentRef::PresentationStyleByContext(i) => {
368                    &cx.model.presentation_style_by_context_arena.get(i.0).styles
369                }
370                m::PresentationStyleAssignmentRef::Complex(_) => {
371                    cx.warn("styled item uses a complex presentation style assignment".to_owned());
372                    continue;
373                }
374            };
375            if let Some(found) = styles.iter().find_map(|s| leaf(cx, s)) {
376                return Some(found);
377            }
378        }
379    }
380    None
381}
382
383/// The `CURVE_STYLE` styling `target`, if any.
384fn curve_style_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<&m::CurveStyle> {
385    each_presentation_style(cx, target, |cx, s| match s {
386        m::PresentationStyleSelectRef::CurveStyle(i) => Some(cx.model.curve_style_arena.get(i.0)),
387        _ => None,
388    })
389}
390
391/// The scalar value of a `SIZE_SELECT` (a curve width).
392fn size_f64(cx: Ctx<'_>, r: &m::SizeSelectRef) -> Option<f64> {
393    let value = match r {
394        m::SizeSelectRef::PositiveLengthMeasure(v) => return Some(*v),
395        m::SizeSelectRef::LengthMeasureWithUnit(i) => {
396            &cx.model
397                .length_measure_with_unit_arena
398                .get(i.0)
399                .value_component
400        }
401        m::SizeSelectRef::MeasureWithUnit(i) => {
402            &cx.model.measure_with_unit_arena.get(i.0).value_component
403        }
404        m::SizeSelectRef::MeasureRepresentationItem(i) => {
405            &cx.model
406                .measure_representation_item_arena
407                .get(i.0)
408                .value_component
409        }
410        m::SizeSelectRef::PlaneAngleMeasureWithUnit(i) => {
411            &cx.model
412                .plane_angle_measure_with_unit_arena
413                .get(i.0)
414                .value_component
415        }
416        m::SizeSelectRef::RatioMeasureWithUnit(i) => {
417            &cx.model
418                .ratio_measure_with_unit_arena
419                .get(i.0)
420                .value_component
421        }
422        m::SizeSelectRef::UncertaintyMeasureWithUnit(i) => {
423            &cx.model
424                .uncertainty_measure_with_unit_arena
425                .get(i.0)
426                .value_component
427        }
428        m::SizeSelectRef::MassMeasureWithUnit(i) => {
429            &cx.model
430                .mass_measure_with_unit_arena
431                .get(i.0)
432                .value_component
433        }
434        m::SizeSelectRef::DescriptiveMeasure(_) | m::SizeSelectRef::Complex(_) => return None,
435    };
436    Some(value.value.as_f64())
437}
438
439/// The line-font name of a curve font select (a line pattern like `continuous`).
440fn curve_font_name<'m>(cx: Ctx<'m>, r: &m::CurveFontOrScaledCurveFontSelectRef) -> Option<&'m str> {
441    match r {
442        m::CurveFontOrScaledCurveFontSelectRef::CurveStyleFont(i) => {
443            Some(&cx.model.curve_style_font_arena.get(i.0).name)
444        }
445        m::CurveFontOrScaledCurveFontSelectRef::CurveStyleFontAndScaling(i) => {
446            Some(&cx.model.curve_style_font_and_scaling_arena.get(i.0).name)
447        }
448        m::CurveFontOrScaledCurveFontSelectRef::DraughtingPreDefinedCurveFont(i) => Some(
449            &cx.model
450                .draughting_pre_defined_curve_font_arena
451                .get(i.0)
452                .name,
453        ),
454        m::CurveFontOrScaledCurveFontSelectRef::PreDefinedCurveFont(i) => {
455            Some(&cx.model.pre_defined_curve_font_arena.get(i.0).name)
456        }
457        // No name (externally defined) or unresolved (complex).
458        m::CurveFontOrScaledCurveFontSelectRef::ExternallyDefinedCurveFont(_) => None,
459        m::CurveFontOrScaledCurveFontSelectRef::Complex(_) => {
460            cx.warn("curve style uses a complex font".to_owned());
461            None
462        }
463    }
464}
465
466/// The colour of the curve style on `target`, if any.
467pub(crate) fn curve_colour_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<Rgb> {
468    let cs = curve_style_of(cx, target)?;
469    colour_to_rgb(cx, cs.curve_colour.as_ref()?)
470}
471
472/// The line width of the curve style on `target` (raw measure value), if any.
473pub(crate) fn curve_width_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<f64> {
474    let cs = curve_style_of(cx, target)?;
475    size_f64(cx, cs.curve_width.as_ref()?)
476}
477
478/// The line-font name of the curve style on `target`, if any.
479pub(crate) fn curve_font_of(cx: Ctx<'_>, target: m::EntityKey) -> Option<&str> {
480    let cs = curve_style_of(cx, target)?;
481    curve_font_name(cx, cs.curve_font.as_ref()?)
482}