ifc_lite_wasm/zero_copy/
symbolic.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use wasm_bindgen::prelude::*;
6
7/// A single 2D polyline for symbolic representations (Plan, Annotation, FootPrint)
8/// Points are stored as [x1, y1, x2, y2, ...] in 2D coordinates
9#[wasm_bindgen]
10pub struct SymbolicPolyline {
11    express_id: u32,
12    ifc_type: String,
13    /// 2D points: [x1, y1, x2, y2, ...]
14    points: Vec<f32>,
15    /// Whether this is a closed loop
16    is_closed: bool,
17    /// World-Y (elevation in world meters) sampled from the placement chain
18    /// or the polyline's own 3D IfcCartesianPoint Z component. Lets the JS
19    /// hook bucket annotations by elevation instead of by storey id —
20    /// important for files like 3DEXPERIENCE's IFC_Annotation.ifc whose
21    /// IfcRelAggregates leaves storeys orphaned but encodes the elevation
22    /// on each item's geometry.
23    world_y: f32,
24    /// Representation identifier: "Plan", "Annotation", "FootPrint", "Axis"
25    rep_identifier: String,
26}
27
28#[wasm_bindgen]
29impl SymbolicPolyline {
30    /// Get express ID of the parent element
31    #[wasm_bindgen(getter, js_name = expressId)]
32    pub fn express_id(&self) -> u32 {
33        self.express_id
34    }
35
36    /// Get IFC type name (e.g., "IfcDoor", "IfcWindow")
37    #[wasm_bindgen(getter, js_name = ifcType)]
38    pub fn ifc_type(&self) -> String {
39        self.ifc_type.clone()
40    }
41
42    /// Get 2D points as Float32Array [x1, y1, x2, y2, ...]
43    #[wasm_bindgen(getter)]
44    pub fn points(&self) -> js_sys::Float32Array {
45        js_sys::Float32Array::from(&self.points[..])
46    }
47
48    /// Get number of points
49    #[wasm_bindgen(getter, js_name = pointCount)]
50    pub fn point_count(&self) -> usize {
51        self.points.len() / 2
52    }
53
54    /// Check if this is a closed loop
55    #[wasm_bindgen(getter, js_name = isClosed)]
56    pub fn is_closed(&self) -> bool {
57        self.is_closed
58    }
59
60    /// Get representation identifier ("Plan", "Annotation", "FootPrint", "Axis")
61    #[wasm_bindgen(getter, js_name = repIdentifier)]
62    pub fn rep_identifier(&self) -> String {
63        self.rep_identifier.clone()
64    }
65
66    /// World-Y elevation captured from the placement chain (or first 3D
67    /// point's Z component). JS uses this as the canonical bucket key.
68    #[wasm_bindgen(getter, js_name = worldY)]
69    pub fn world_y(&self) -> f32 {
70        self.world_y
71    }
72}
73
74impl SymbolicPolyline {
75    /// Create a new symbolic polyline
76    #[allow(clippy::too_many_arguments)]
77    pub fn new(
78        express_id: u32,
79        ifc_type: String,
80        points: Vec<f32>,
81        is_closed: bool,
82        world_y: f32,
83        rep_identifier: String,
84    ) -> Self {
85        Self {
86            express_id,
87            ifc_type,
88            points,
89            is_closed,
90            world_y,
91            rep_identifier,
92        }
93    }
94}
95
96/// A 2D circle/arc for symbolic representations
97#[wasm_bindgen]
98pub struct SymbolicCircle {
99    express_id: u32,
100    ifc_type: String,
101    /// Center point [x, y]
102    center_x: f32,
103    center_y: f32,
104    /// Radius
105    radius: f32,
106    /// World-Y elevation (see SymbolicPolyline.world_y).
107    world_y: f32,
108    /// Start angle in radians (0 for full circle)
109    start_angle: f32,
110    /// End angle in radians (2*PI for full circle)
111    end_angle: f32,
112    /// Representation identifier
113    rep_identifier: String,
114}
115
116#[wasm_bindgen]
117impl SymbolicCircle {
118    #[wasm_bindgen(getter, js_name = expressId)]
119    pub fn express_id(&self) -> u32 {
120        self.express_id
121    }
122
123    #[wasm_bindgen(getter, js_name = ifcType)]
124    pub fn ifc_type(&self) -> String {
125        self.ifc_type.clone()
126    }
127
128    #[wasm_bindgen(getter, js_name = centerX)]
129    pub fn center_x(&self) -> f32 {
130        self.center_x
131    }
132
133    #[wasm_bindgen(getter, js_name = centerY)]
134    pub fn center_y(&self) -> f32 {
135        self.center_y
136    }
137
138    #[wasm_bindgen(getter)]
139    pub fn radius(&self) -> f32 {
140        self.radius
141    }
142
143    #[wasm_bindgen(getter, js_name = startAngle)]
144    pub fn start_angle(&self) -> f32 {
145        self.start_angle
146    }
147
148    #[wasm_bindgen(getter, js_name = endAngle)]
149    pub fn end_angle(&self) -> f32 {
150        self.end_angle
151    }
152
153    #[wasm_bindgen(getter, js_name = repIdentifier)]
154    pub fn rep_identifier(&self) -> String {
155        self.rep_identifier.clone()
156    }
157
158    /// Check if this is a full circle
159    #[wasm_bindgen(getter, js_name = isFullCircle)]
160    pub fn is_full_circle(&self) -> bool {
161        (self.end_angle - self.start_angle - std::f32::consts::TAU).abs() < 0.001
162    }
163
164    /// World-Y elevation captured from the placement chain.
165    #[wasm_bindgen(getter, js_name = worldY)]
166    pub fn world_y(&self) -> f32 {
167        self.world_y
168    }
169}
170
171impl SymbolicCircle {
172    #[allow(clippy::too_many_arguments)]
173    pub fn new(
174        express_id: u32,
175        ifc_type: String,
176        center_x: f32,
177        center_y: f32,
178        radius: f32,
179        world_y: f32,
180        start_angle: f32,
181        end_angle: f32,
182        rep_identifier: String,
183    ) -> Self {
184        Self {
185            express_id,
186            ifc_type,
187            center_x,
188            center_y,
189            radius,
190            world_y,
191            start_angle,
192            end_angle,
193            rep_identifier,
194        }
195    }
196
197    /// Create a full circle
198    pub fn full_circle(
199        express_id: u32,
200        ifc_type: String,
201        center_x: f32,
202        center_y: f32,
203        radius: f32,
204        world_y: f32,
205        rep_identifier: String,
206    ) -> Self {
207        Self::new(
208            express_id,
209            ifc_type,
210            center_x,
211            center_y,
212            radius,
213            world_y,
214            0.0,
215            std::f32::consts::TAU,
216            rep_identifier,
217        )
218    }
219}
220
221/// A 2D text annotation (IfcTextLiteral / IfcTextLiteralWithExtent).
222///
223/// Position is in the same 2D coordinate space as `SymbolicPolyline` (i.e. the
224/// floor-plan / annotation overlay's local frame after applying placement +
225/// RTC). The text-orientation pair `(cos, sin)` rotates the baseline from the
226/// `+x` axis. Height is the IFC font height in model units, scaled by the
227/// project's length-unit factor so the renderer can convert directly to world
228/// units. Alignment is the IFC `BoxAlignment` string verbatim
229/// (`top-left`, `center`, `bottom-right`, …) — the renderer can interpret it.
230#[wasm_bindgen]
231pub struct SymbolicText {
232    express_id: u32,
233    ifc_type: String,
234    /// Anchor point on the text baseline (model units).
235    x: f32,
236    y: f32,
237    /// Baseline orientation as a (cos, sin) pair. Defaults to (1, 0).
238    dir_x: f32,
239    dir_y: f32,
240    /// Font height in model units (already unit-scaled). Defaults to 1.0 when
241    /// IfcTextStyle isn't resolvable.
242    height: f32,
243    /// UTF-8 text content (decoded from IFC's `\X2\…\X0\` escape sequences).
244    content: String,
245    /// IFC `BoxAlignment` — empty string when absent. Renderer treats absent
246    /// as `"bottom-left"`, matching the IFC default.
247    alignment: String,
248    /// World-Y elevation captured from the placement chain (see
249    /// SymbolicPolyline.world_y for the why).
250    world_y: f32,
251    /// sRGB straight-alpha colour (0..1). Defaults to dark-grey when no
252    /// IfcStyledItem chain resolves a colour. The grid-tag emission path
253    /// uses this to render white bubble fills + black outlines + black
254    /// tags out of the existing text pipeline (free billboard +
255    /// screen-pixel scaling).
256    color_r: f32,
257    color_g: f32,
258    color_b: f32,
259    color_a: f32,
260    /// Per-instance target screen-pixel cap height. 0 = fall back to the
261    /// renderer's global default (~14 px for body text). Grid bubble fills
262    /// + outlines emit at a larger value (~30 px) so the bubble stays
263    /// proportional to the inscribed tag at every zoom level.
264    target_px: f32,
265    /// "Plan" | "Annotation" | "FootPrint" | "Axis"
266    rep_identifier: String,
267}
268
269#[wasm_bindgen]
270impl SymbolicText {
271    #[wasm_bindgen(getter, js_name = expressId)]
272    pub fn express_id(&self) -> u32 { self.express_id }
273    #[wasm_bindgen(getter, js_name = ifcType)]
274    pub fn ifc_type(&self) -> String { self.ifc_type.clone() }
275    #[wasm_bindgen(getter)]
276    pub fn x(&self) -> f32 { self.x }
277    #[wasm_bindgen(getter)]
278    pub fn y(&self) -> f32 { self.y }
279    #[wasm_bindgen(getter, js_name = dirX)]
280    pub fn dir_x(&self) -> f32 { self.dir_x }
281    #[wasm_bindgen(getter, js_name = dirY)]
282    pub fn dir_y(&self) -> f32 { self.dir_y }
283    #[wasm_bindgen(getter)]
284    pub fn height(&self) -> f32 { self.height }
285    #[wasm_bindgen(getter)]
286    pub fn content(&self) -> String { self.content.clone() }
287    #[wasm_bindgen(getter)]
288    pub fn alignment(&self) -> String { self.alignment.clone() }
289    #[wasm_bindgen(getter, js_name = worldY)]
290    pub fn world_y(&self) -> f32 { self.world_y }
291    #[wasm_bindgen(getter, js_name = colorR)]
292    pub fn color_r(&self) -> f32 { self.color_r }
293    #[wasm_bindgen(getter, js_name = colorG)]
294    pub fn color_g(&self) -> f32 { self.color_g }
295    #[wasm_bindgen(getter, js_name = colorB)]
296    pub fn color_b(&self) -> f32 { self.color_b }
297    #[wasm_bindgen(getter, js_name = colorA)]
298    pub fn color_a(&self) -> f32 { self.color_a }
299    #[wasm_bindgen(getter, js_name = targetPx)]
300    pub fn target_px(&self) -> f32 { self.target_px }
301    #[wasm_bindgen(getter, js_name = repIdentifier)]
302    pub fn rep_identifier(&self) -> String { self.rep_identifier.clone() }
303}
304
305impl SymbolicText {
306    #[allow(clippy::too_many_arguments)]
307    pub fn new(
308        express_id: u32,
309        ifc_type: String,
310        x: f32,
311        y: f32,
312        dir_x: f32,
313        dir_y: f32,
314        height: f32,
315        content: String,
316        alignment: String,
317        world_y: f32,
318        rep_identifier: String,
319    ) -> Self {
320        Self::new_styled(
321            express_id, ifc_type, x, y, dir_x, dir_y,
322            height, content, alignment, world_y,
323            [0.05, 0.05, 0.05, 1.0], // default near-black text color
324            0.0,                       // 0 → renderer global default target_px
325            rep_identifier,
326        )
327    }
328
329    /// Full constructor with per-instance colour + screen-pixel target.
330    /// Used by the grid bubble emission (white fill / black outline) and
331    /// by future IfcTextStyle resolution.
332    #[allow(clippy::too_many_arguments)]
333    pub fn new_styled(
334        express_id: u32,
335        ifc_type: String,
336        x: f32,
337        y: f32,
338        dir_x: f32,
339        dir_y: f32,
340        height: f32,
341        content: String,
342        alignment: String,
343        world_y: f32,
344        rgba: [f32; 4],
345        target_px: f32,
346        rep_identifier: String,
347    ) -> Self {
348        Self {
349            express_id,
350            ifc_type,
351            x,
352            y,
353            dir_x,
354            dir_y,
355            height,
356            content,
357            alignment,
358            world_y,
359            color_r: rgba[0],
360            color_g: rgba[1],
361            color_b: rgba[2],
362            color_a: rgba[3],
363            target_px,
364            rep_identifier,
365        }
366    }
367}
368
369/// A 2D filled region (IfcAnnotationFillArea / IfcAnnotationFillAreaOccurrence).
370///
371/// Stores one outer ring of 2D points plus an offset table indexing inner
372/// rings (holes). Both rings are stored flat in `points` so the JS side can
373/// view the buffer as one Float32Array. The optional `hatch_*` fields encode
374/// IfcFillAreaStyleHatching (line spacing, primary/secondary angles, line
375/// width) when the IfcStyledItem chain resolves to a hatching style; absent
376/// styles render as a solid fill.
377///
378/// `holes_offsets` is an inclusive-prefix array describing where each hole
379/// begins. The outer ring is implicitly at `points[0..holes_offsets[0]]`
380/// (or all points if `holes_offsets` is empty). Each `holes_offsets[i]` is a
381/// vertex index, not a byte offset.
382#[wasm_bindgen]
383pub struct SymbolicFillArea {
384    express_id: u32,
385    geometry_item_id: Option<u32>,
386    ifc_type: String,
387    /// All ring vertices: outer ring, then each hole back-to-back. Format:
388    /// [x1, y1, x2, y2, …]
389    points: Vec<f32>,
390    /// Inclusive prefix of where each hole begins (in vertex indices, not
391    /// floats). Empty array = no holes.
392    holes_offsets: Vec<u32>,
393    /// Fill color (sRGB, 0..1). Defaults to opaque black when no style.
394    fill_r: f32,
395    fill_g: f32,
396    fill_b: f32,
397    fill_a: f32,
398    /// Whether this fill has a hatching style applied.
399    has_hatching: bool,
400    /// Hatching primary line spacing in model units. Only valid when has_hatching.
401    hatch_spacing: f32,
402    /// Hatching primary angle in radians from the +x axis.
403    hatch_angle: f32,
404    /// Optional secondary angle (cross-hatching). NaN if absent.
405    hatch_angle_secondary: f32,
406    /// Hatching line width in model units (0 when unspecified).
407    hatch_line_width: f32,
408    /// World-Y elevation captured from the placement chain or the boundary
409    /// curve's IfcCartesianPoint Z components (see SymbolicPolyline.world_y).
410    world_y: f32,
411    rep_identifier: String,
412}
413
414#[wasm_bindgen]
415impl SymbolicFillArea {
416    #[wasm_bindgen(getter, js_name = geometryItemId)]
417    pub fn geometry_item_id(&self) -> Option<u32> { self.geometry_item_id }
418    #[wasm_bindgen(getter, js_name = expressId)]
419    pub fn express_id(&self) -> u32 { self.express_id }
420    #[wasm_bindgen(getter, js_name = ifcType)]
421    pub fn ifc_type(&self) -> String { self.ifc_type.clone() }
422    /// Flattened ring vertices.
423    #[wasm_bindgen(getter)]
424    pub fn points(&self) -> js_sys::Float32Array {
425        js_sys::Float32Array::from(&self.points[..])
426    }
427    #[wasm_bindgen(getter, js_name = pointCount)]
428    pub fn point_count(&self) -> usize { self.points.len() / 2 }
429    /// Vertex indices marking the start of each hole. Empty = no holes.
430    #[wasm_bindgen(getter, js_name = holesOffsets)]
431    pub fn holes_offsets(&self) -> js_sys::Uint32Array {
432        js_sys::Uint32Array::from(&self.holes_offsets[..])
433    }
434    #[wasm_bindgen(getter, js_name = holeCount)]
435    pub fn hole_count(&self) -> usize { self.holes_offsets.len() }
436    #[wasm_bindgen(getter, js_name = fillR)]
437    pub fn fill_r(&self) -> f32 { self.fill_r }
438    #[wasm_bindgen(getter, js_name = fillG)]
439    pub fn fill_g(&self) -> f32 { self.fill_g }
440    #[wasm_bindgen(getter, js_name = fillB)]
441    pub fn fill_b(&self) -> f32 { self.fill_b }
442    #[wasm_bindgen(getter, js_name = fillA)]
443    pub fn fill_a(&self) -> f32 { self.fill_a }
444    #[wasm_bindgen(getter, js_name = hasHatching)]
445    pub fn has_hatching(&self) -> bool { self.has_hatching }
446    #[wasm_bindgen(getter, js_name = hatchSpacing)]
447    pub fn hatch_spacing(&self) -> f32 { self.hatch_spacing }
448    #[wasm_bindgen(getter, js_name = hatchAngle)]
449    pub fn hatch_angle(&self) -> f32 { self.hatch_angle }
450    #[wasm_bindgen(getter, js_name = hatchAngleSecondary)]
451    pub fn hatch_angle_secondary(&self) -> f32 { self.hatch_angle_secondary }
452    #[wasm_bindgen(getter, js_name = hatchLineWidth)]
453    pub fn hatch_line_width(&self) -> f32 { self.hatch_line_width }
454    #[wasm_bindgen(getter, js_name = worldY)]
455    pub fn world_y(&self) -> f32 { self.world_y }
456    #[wasm_bindgen(getter, js_name = repIdentifier)]
457    pub fn rep_identifier(&self) -> String { self.rep_identifier.clone() }
458}
459
460impl SymbolicFillArea {
461    pub(super) fn with_geometry_item_id(mut self, id: Option<u32>) -> Self {
462        self.geometry_item_id = id;
463        self
464    }
465
466    #[allow(clippy::too_many_arguments)]
467    pub fn new(
468        express_id: u32,
469        ifc_type: String,
470        points: Vec<f32>,
471        holes_offsets: Vec<u32>,
472        fill_rgba: [f32; 4],
473        world_y: f32,
474        rep_identifier: String,
475    ) -> Self {
476        Self {
477            express_id,
478            geometry_item_id: None,
479            ifc_type,
480            points,
481            holes_offsets,
482            fill_r: fill_rgba[0],
483            fill_g: fill_rgba[1],
484            fill_b: fill_rgba[2],
485            fill_a: fill_rgba[3],
486            has_hatching: false,
487            hatch_spacing: 0.0,
488            hatch_angle: 0.0,
489            hatch_angle_secondary: f32::NAN,
490            hatch_line_width: 0.0,
491            world_y,
492            rep_identifier,
493        }
494    }
495
496    /// Builder method: attach a hatching style to an existing fill area.
497    pub fn with_hatching(
498        mut self,
499        spacing: f32,
500        angle: f32,
501        angle_secondary: Option<f32>,
502        line_width: f32,
503    ) -> Self {
504        self.has_hatching = true;
505        self.hatch_spacing = spacing;
506        self.hatch_angle = angle;
507        self.hatch_angle_secondary = angle_secondary.unwrap_or(f32::NAN);
508        self.hatch_line_width = line_width;
509        self
510    }
511}
512
513/// Collection of symbolic representations for an IFC model
514#[wasm_bindgen]
515pub struct SymbolicRepresentationCollection {
516    polylines: Vec<SymbolicPolyline>,
517    circles: Vec<SymbolicCircle>,
518    texts: Vec<SymbolicText>,
519    fills: Vec<SymbolicFillArea>,
520    /// What stopped extraction early; `None` if it completed.
521    ///
522    /// Carried across the boundary in FULL because geometry is client-side
523    /// only, so the browser is the consumer that most needs it (#2938). The
524    /// count alone cannot say whether a whole-file bound clipped the drawing or
525    /// one representation item hit a per-item bound, and cannot render
526    /// "showing {emitted} of {limit}" for the cases that have a limit.
527    pub(super) truncated: Option<ifc_lite_processing::SymbolicTruncation>,
528}
529
530#[wasm_bindgen]
531impl SymbolicRepresentationCollection {
532    /// Get all express IDs that have symbolic representations
533    #[wasm_bindgen(js_name = getExpressIds)]
534    pub fn get_express_ids(&self) -> Vec<u32> {
535        let mut ids: Vec<u32> = self
536            .polylines
537            .iter()
538            .map(|p| p.express_id)
539            .chain(self.circles.iter().map(|c| c.express_id))
540            .chain(self.texts.iter().map(|t| t.express_id))
541            .chain(self.fills.iter().map(|f| f.express_id))
542            .collect();
543        ids.sort_unstable();
544        ids.dedup();
545        ids
546    }
547
548    /// Get number of polylines
549    #[wasm_bindgen(getter, js_name = polylineCount)]
550    pub fn polyline_count(&self) -> usize {
551        self.polylines.len()
552    }
553
554    /// Get number of circles/arcs
555    #[wasm_bindgen(getter, js_name = circleCount)]
556    pub fn circle_count(&self) -> usize {
557        self.circles.len()
558    }
559
560    /// Get number of text annotations
561    #[wasm_bindgen(getter, js_name = textCount)]
562    pub fn text_count(&self) -> usize { self.texts.len() }
563
564    /// Get number of fill areas
565    #[wasm_bindgen(getter, js_name = fillCount)]
566    pub fn fill_count(&self) -> usize { self.fills.len() }
567
568    /// Get total count of all symbolic items
569    #[wasm_bindgen(getter, js_name = totalCount)]
570    pub fn total_count(&self) -> usize {
571        self.polylines.len() + self.circles.len() + self.texts.len() + self.fills.len()
572    }
573
574    /// Check if collection is empty. A TRUNCATED result never is, even with no
575    /// primitives; the reasoning and the parity test are in `symbolic_truncation.rs`.
576    #[wasm_bindgen(getter, js_name = isEmpty)]
577    pub fn is_empty(&self) -> bool {
578        self.polylines.is_empty()
579            && self.circles.is_empty()
580            && self.texts.is_empty()
581            && self.fills.is_empty()
582            && self.truncated.is_none()
583    }
584
585    /// Get polyline at index
586    #[wasm_bindgen(js_name = getPolyline)]
587    pub fn get_polyline(&self, index: usize) -> Option<SymbolicPolyline> {
588        self.polylines.get(index).map(|p| SymbolicPolyline {
589            express_id: p.express_id,
590            ifc_type: p.ifc_type.clone(),
591            points: p.points.clone(),
592            is_closed: p.is_closed,
593            world_y: p.world_y,
594            rep_identifier: p.rep_identifier.clone(),
595        })
596    }
597
598    /// Get circle at index
599    #[wasm_bindgen(js_name = getCircle)]
600    pub fn get_circle(&self, index: usize) -> Option<SymbolicCircle> {
601        self.circles.get(index).map(|c| SymbolicCircle {
602            express_id: c.express_id,
603            ifc_type: c.ifc_type.clone(),
604            center_x: c.center_x,
605            center_y: c.center_y,
606            radius: c.radius,
607            world_y: c.world_y,
608            start_angle: c.start_angle,
609            end_angle: c.end_angle,
610            rep_identifier: c.rep_identifier.clone(),
611        })
612    }
613
614    /// Get text annotation at index.
615    #[wasm_bindgen(js_name = getText)]
616    pub fn get_text(&self, index: usize) -> Option<SymbolicText> {
617        self.texts.get(index).map(|t| SymbolicText {
618            express_id: t.express_id,
619            ifc_type: t.ifc_type.clone(),
620            x: t.x,
621            y: t.y,
622            dir_x: t.dir_x,
623            dir_y: t.dir_y,
624            height: t.height,
625            content: t.content.clone(),
626            alignment: t.alignment.clone(),
627            world_y: t.world_y,
628            color_r: t.color_r,
629            color_g: t.color_g,
630            color_b: t.color_b,
631            color_a: t.color_a,
632            target_px: t.target_px,
633            rep_identifier: t.rep_identifier.clone(),
634        })
635    }
636
637    /// Get fill area at index.
638    #[wasm_bindgen(js_name = getFill)]
639    pub fn get_fill(&self, index: usize) -> Option<SymbolicFillArea> {
640        self.fills.get(index).map(|f| SymbolicFillArea {
641            express_id: f.express_id,
642            geometry_item_id: f.geometry_item_id,
643            ifc_type: f.ifc_type.clone(),
644            points: f.points.clone(),
645            holes_offsets: f.holes_offsets.clone(),
646            fill_r: f.fill_r,
647            fill_g: f.fill_g,
648            fill_b: f.fill_b,
649            fill_a: f.fill_a,
650            has_hatching: f.has_hatching,
651            hatch_spacing: f.hatch_spacing,
652            hatch_angle: f.hatch_angle,
653            hatch_angle_secondary: f.hatch_angle_secondary,
654            hatch_line_width: f.hatch_line_width,
655            world_y: f.world_y,
656            rep_identifier: f.rep_identifier.clone(),
657        })
658    }
659
660}
661
662impl SymbolicRepresentationCollection {
663    pub fn new() -> Self {
664        Self {
665            polylines: Vec::new(),
666            circles: Vec::new(),
667            texts: Vec::new(),
668            fills: Vec::new(),
669            truncated: None,
670        }
671    }
672
673    pub fn with_capacity(polyline_capacity: usize, circle_capacity: usize) -> Self {
674        Self {
675            polylines: Vec::with_capacity(polyline_capacity),
676            circles: Vec::with_capacity(circle_capacity),
677            texts: Vec::new(),
678            fills: Vec::new(),
679            truncated: None,
680        }
681    }
682
683    pub fn add_polyline(&mut self, polyline: SymbolicPolyline) {
684        self.polylines.push(polyline);
685    }
686
687    pub fn add_circle(&mut self, circle: SymbolicCircle) {
688        self.circles.push(circle);
689    }
690
691    pub fn add_text(&mut self, text: SymbolicText) {
692        self.texts.push(text);
693    }
694
695    pub fn add_fill(&mut self, fill: SymbolicFillArea) {
696        self.fills.push(fill);
697    }
698}
699
700impl Default for SymbolicRepresentationCollection {
701    fn default() -> Self {
702        Self::new()
703    }
704}