Skip to main content

kcl_api/
artifact.rs

1use indexmap::IndexMap;
2use kcl_error::SourceRange;
3use parse_display::Display;
4use parse_display::FromStr;
5use schemars::JsonSchema;
6use serde::Deserialize;
7use serde::Serialize;
8use serde::ser::SerializeSeq;
9use uuid::Uuid;
10
11use crate::ArtifactId;
12use crate::NodePath;
13use crate::ObjectId;
14use crate::UnitLength;
15
16pub type DummyPathToNode = Vec<()>;
17
18fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
19where
20    S: serde::Serializer,
21{
22    let seq = serializer.serialize_seq(Some(0))?;
23    seq.end()
24}
25
26#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
27#[ts(export_to = "Artifact.ts")]
28#[serde(rename_all = "camelCase")]
29pub struct CodeRef {
30    pub range: SourceRange,
31    pub node_path: NodePath,
32    // TODO: We should implement this in Rust.
33    #[serde(default, serialize_with = "serialize_dummy_path_to_node")]
34    #[ts(type = "Array<[string | number, string]>")]
35    pub path_to_node: DummyPathToNode,
36}
37
38impl CodeRef {
39    pub fn placeholder(range: SourceRange) -> Self {
40        Self {
41            range,
42            node_path: Default::default(),
43            path_to_node: Vec::new(),
44        }
45    }
46}
47
48#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, JsonSchema, PartialEq, ts_rs::TS, Display, FromStr)]
49#[ts(export)]
50#[serde(rename_all = "camelCase")]
51pub enum PlaneName {
52    /// The XY plane.
53    #[display("XY")]
54    Xy,
55    /// The opposite side of the XY plane.
56    #[display("-XY")]
57    NegXy,
58    /// The XZ plane.
59    #[display("XZ")]
60    Xz,
61    /// The opposite side of the XZ plane.
62    #[display("-XZ")]
63    NegXz,
64    /// The YZ plane.
65    #[display("YZ")]
66    Yz,
67    /// The opposite side of the YZ plane.
68    #[display("-YZ")]
69    NegYz,
70}
71
72#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Default, ts_rs::TS)]
73#[ts(export_to = "Artifact.ts")]
74/// API-owned point data used by artifact payloads so the wire model does not
75/// depend on kcl-lib's execution geometry types.
76pub struct ArtifactPoint3d {
77    pub x: f64,
78    pub y: f64,
79    pub z: f64,
80    pub units: Option<UnitLength>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
84#[ts(export_to = "Artifact.ts")]
85#[serde(rename_all = "camelCase")]
86/// API-owned plane data used by artifacts. It mirrors kcl-lib's evaluated
87/// plane JSON shape while keeping kcl-api independent of execution internals.
88pub struct ArtifactPlaneInfo {
89    pub origin: ArtifactPoint3d,
90    pub x_axis: ArtifactPoint3d,
91    pub y_axis: ArtifactPoint3d,
92    pub z_axis: ArtifactPoint3d,
93}
94
95#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
96#[ts(export_to = "Artifact.ts")]
97#[serde(rename_all = "snake_case")]
98/// API-owned sweep method equivalent to the engine extrusion method, avoiding
99/// a kcl-api dependency on kittycad-modeling-cmds.
100pub enum ArtifactSweepMethod {
101    New,
102    Merge,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
106#[ts(export_to = "Artifact.ts")]
107#[serde(rename_all = "camelCase")]
108pub struct CompositeSolid {
109    pub id: ArtifactId,
110    /// Whether this artifact has been used in a subsequent operation
111    pub consumed: bool,
112    pub sub_type: CompositeSolidSubType,
113    /// Index of this output in the expression result, for operations that
114    /// return multiple selectable bodies from one KCL variable.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub output_index: Option<usize>,
117    /// Constituent solids of the composite solid.
118    pub solid_ids: Vec<ArtifactId>,
119    /// Tool solids used for asymmetric operations like subtract.
120    pub tool_ids: Vec<ArtifactId>,
121    pub code_ref: CodeRef,
122    /// This is the ID of the composite solid that this is part of, if any, as a
123    /// composite solid can be used as input for another composite solid.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub composite_solid_id: Option<ArtifactId>,
126    /// Pattern operations that use this composite solid as their source.
127    #[serde(default, skip_serializing_if = "Vec::is_empty")]
128    pub pattern_ids: Vec<ArtifactId>,
129}
130
131#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
132#[ts(export_to = "Artifact.ts")]
133#[serde(rename_all = "camelCase")]
134pub enum CompositeSolidSubType {
135    Intersect,
136    Subtract,
137    Split,
138    Union,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
142#[ts(export_to = "Artifact.ts")]
143#[serde(rename_all = "camelCase")]
144pub struct Plane {
145    pub id: ArtifactId,
146    pub path_ids: Vec<ArtifactId>,
147    pub code_ref: CodeRef,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
151#[ts(export_to = "Artifact.ts")]
152#[serde(rename_all = "camelCase")]
153pub struct Path {
154    pub id: ArtifactId,
155    pub sub_type: PathSubType,
156    pub plane_id: ArtifactId,
157    pub seg_ids: Vec<ArtifactId>,
158    /// Whether this artifact has been used in a subsequent operation
159    pub consumed: bool,
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    /// The sweep, if any, that this Path serves as the base path for.
162    /// corresponds to `path_id` on the Sweep.
163    pub sweep_id: Option<ArtifactId>,
164    /// The sweep, if any, that this Path serves as the trajectory for.
165    pub trajectory_sweep_id: Option<ArtifactId>,
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub solid2d_id: Option<ArtifactId>,
168    pub code_ref: CodeRef,
169    /// This is the ID of the composite solid that this is part of, if any, as
170    /// this can be used as input for another composite solid.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub composite_solid_id: Option<ArtifactId>,
173    /// For sketch paths, the ID of the sketch block this path was created
174    /// from. `None` for region paths and paths created in other ways.
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub sketch_block_id: Option<ArtifactId>,
177    /// For region paths, the ID of the sketch path this region was created
178    /// from. `None` for sketch paths.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub origin_path_id: Option<ArtifactId>,
181    /// The hole, if any, from a subtract2d() call.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub inner_path_id: Option<ArtifactId>,
184    /// The `Path` that this is a hole of, if any. The inverse link of
185    /// `inner_path_id`.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub outer_path_id: Option<ArtifactId>,
188    /// Pattern operations that use this path as their source.
189    #[serde(default, skip_serializing_if = "Vec::is_empty")]
190    pub pattern_ids: Vec<ArtifactId>,
191}
192
193#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
194#[ts(export_to = "Artifact.ts")]
195#[serde(rename_all = "camelCase")]
196pub enum PathSubType {
197    Sketch,
198    Region,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
202#[ts(export_to = "Artifact.ts")]
203#[serde(rename_all = "camelCase")]
204pub struct Segment {
205    pub id: ArtifactId,
206    pub path_id: ArtifactId,
207    /// If this artifact is a segment in a region, the segment in the original
208    /// sketch that this was derived from.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub original_seg_id: Option<ArtifactId>,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub surface_id: Option<ArtifactId>,
213    pub edge_ids: Vec<ArtifactId>,
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub edge_cut_id: Option<ArtifactId>,
216    pub code_ref: CodeRef,
217    pub common_surface_ids: Vec<ArtifactId>,
218}
219
220/// A sweep is a more generic term for extrude, revolve, loft, sweep, and blend.
221#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
222#[ts(export_to = "Artifact.ts")]
223#[serde(rename_all = "camelCase")]
224pub struct Sweep {
225    pub id: ArtifactId,
226    pub sub_type: SweepSubType,
227    pub path_id: ArtifactId,
228    pub surface_ids: Vec<ArtifactId>,
229    pub edge_ids: Vec<ArtifactId>,
230    pub code_ref: CodeRef,
231    /// ID of trajectory path for sweep, if any
232    /// Only applicable to SweepSubType::Sweep and SweepSubType::Blend, which
233    /// can use a second path-like input
234    pub trajectory_id: Option<ArtifactId>,
235    pub method: ArtifactSweepMethod,
236    /// Whether this artifact has been used in a subsequent operation
237    pub consumed: bool,
238    /// Pattern operations that use this sweep as their source.
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub pattern_ids: Vec<ArtifactId>,
241}
242
243#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
244#[ts(export_to = "Artifact.ts")]
245#[serde(rename_all = "camelCase")]
246pub enum SweepSubType {
247    Extrusion,
248    ExtrusionTwist,
249    Revolve,
250    RevolveAboutEdge,
251    Loft,
252    Blend,
253    Sweep,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
257#[ts(export_to = "Artifact.ts")]
258#[serde(rename_all = "camelCase")]
259pub struct Solid2d {
260    pub id: ArtifactId,
261    pub path_id: ArtifactId,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
265#[ts(export_to = "Artifact.ts")]
266#[serde(rename_all = "camelCase")]
267pub struct PrimitiveFace {
268    pub id: ArtifactId,
269    pub solid_id: ArtifactId,
270    pub code_ref: CodeRef,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
274#[ts(export_to = "Artifact.ts")]
275#[serde(rename_all = "camelCase")]
276pub struct PrimitiveEdge {
277    pub id: ArtifactId,
278    pub solid_id: ArtifactId,
279    pub code_ref: CodeRef,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
283#[ts(export_to = "Artifact.ts")]
284#[serde(rename_all = "camelCase")]
285pub struct PlaneOfFace {
286    pub id: ArtifactId,
287    pub face_id: ArtifactId,
288    pub code_ref: CodeRef,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
292#[ts(export_to = "Artifact.ts")]
293#[serde(rename_all = "camelCase")]
294pub struct StartSketchOnFace {
295    pub id: ArtifactId,
296    pub face_id: ArtifactId,
297    pub code_ref: CodeRef,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
301#[ts(export_to = "Artifact.ts")]
302#[serde(rename_all = "camelCase")]
303pub struct StartSketchOnPlane {
304    pub id: ArtifactId,
305    pub plane_id: ArtifactId,
306    pub code_ref: CodeRef,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
310#[ts(export_to = "Artifact.ts")]
311#[serde(rename_all = "camelCase")]
312pub struct SketchBlock {
313    pub id: ArtifactId,
314    /// The semantic standard plane name when the sketch block is on a standard plane.
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub standard_plane: Option<PlaneName>,
317    /// The concrete plane artifact ID backing the sketch block, when one is available.
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub plane_id: Option<ArtifactId>,
320    /// The evaluated plane data backing the sketch block, when the sketch is on a plane.
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub plane_info: Option<ArtifactPlaneInfo>,
323    /// The path artifact ID created from the sketch block, if there is one.
324    /// There are edge cases when a path isn't created, like when there are no
325    /// segments.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub path_id: Option<ArtifactId>,
328    pub code_ref: CodeRef,
329    /// The sketch ID (ObjectId) for the sketch scene object.
330    pub sketch_id: ObjectId,
331}
332
333#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
334#[ts(export_to = "Artifact.ts")]
335#[serde(rename_all = "camelCase")]
336pub enum SketchBlockConstraintType {
337    Angle,
338    Coincident,
339    Distance,
340    Diameter,
341    EqualRadius,
342    Fixed,
343    HorizontalDistance,
344    VerticalDistance,
345    Horizontal,
346    LinesEqualLength,
347    Midpoint,
348    Parallel,
349    Perpendicular,
350    Radius,
351    Symmetric,
352    Tangent,
353    Vertical,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
357#[ts(export_to = "Artifact.ts")]
358#[serde(rename_all = "camelCase")]
359pub struct SketchBlockConstraint {
360    pub id: ArtifactId,
361    /// The sketch ID (ObjectId) that owns this constraint.
362    pub sketch_id: ObjectId,
363    /// The constraint ID (ObjectId) for the constraint scene object.
364    pub constraint_id: ObjectId,
365    pub constraint_type: SketchBlockConstraintType,
366    pub code_ref: CodeRef,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
370#[ts(export_to = "Artifact.ts")]
371#[serde(rename_all = "camelCase")]
372pub struct Wall {
373    pub id: ArtifactId,
374    pub seg_id: ArtifactId,
375    pub edge_cut_edge_ids: Vec<ArtifactId>,
376    pub sweep_id: ArtifactId,
377    pub path_ids: Vec<ArtifactId>,
378    /// This is for the sketch-on-face plane, not for the wall itself.  Traverse
379    /// to the extrude and/or segment to get the wall's code_ref.
380    pub face_code_ref: CodeRef,
381    /// The command ID that got the data for this wall. Used for stable sorting.
382    pub cmd_id: Uuid,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
386#[ts(export_to = "Artifact.ts")]
387#[serde(rename_all = "camelCase")]
388pub struct Cap {
389    pub id: ArtifactId,
390    pub sub_type: CapSubType,
391    pub edge_cut_edge_ids: Vec<ArtifactId>,
392    pub sweep_id: ArtifactId,
393    pub path_ids: Vec<ArtifactId>,
394    /// This is for the sketch-on-face plane, not for the cap itself.  Traverse
395    /// to the extrude and/or segment to get the cap's code_ref.
396    pub face_code_ref: CodeRef,
397    /// The command ID that got the data for this cap. Used for stable sorting.
398    pub cmd_id: Uuid,
399}
400
401#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
402#[ts(export_to = "Artifact.ts")]
403#[serde(rename_all = "camelCase")]
404pub enum CapSubType {
405    Start,
406    End,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
410#[ts(export_to = "Artifact.ts")]
411#[serde(rename_all = "camelCase")]
412pub struct SweepEdge {
413    pub id: ArtifactId,
414    pub sub_type: SweepEdgeSubType,
415    pub seg_id: ArtifactId,
416    pub cmd_id: Uuid,
417    // This is only used for sorting, not for the actual artifact.
418    #[serde(skip)]
419    pub index: usize,
420    pub sweep_id: ArtifactId,
421    pub common_surface_ids: Vec<ArtifactId>,
422}
423
424#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
425#[ts(export_to = "Artifact.ts")]
426#[serde(rename_all = "camelCase")]
427pub enum SweepEdgeSubType {
428    Opposite,
429    Adjacent,
430    PreviousAdjacent,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
434#[ts(export_to = "Artifact.ts")]
435#[serde(rename_all = "camelCase")]
436pub struct EdgeCut {
437    pub id: ArtifactId,
438    pub sub_type: EdgeCutSubType,
439    pub consumed_edge_id: ArtifactId,
440    pub edge_ids: Vec<ArtifactId>,
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub surface_id: Option<ArtifactId>,
443    pub code_ref: CodeRef,
444}
445
446#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
447#[ts(export_to = "Artifact.ts")]
448#[serde(rename_all = "camelCase")]
449pub enum EdgeCutSubType {
450    Fillet,
451    Chamfer,
452    Custom,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
456#[ts(export_to = "Artifact.ts")]
457#[serde(rename_all = "camelCase")]
458pub struct EdgeCutEdge {
459    pub id: ArtifactId,
460    pub edge_cut_id: ArtifactId,
461    pub surface_id: ArtifactId,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
465#[ts(export_to = "Artifact.ts")]
466#[serde(rename_all = "camelCase")]
467pub struct Helix {
468    pub id: ArtifactId,
469    /// The axis of the helix.  Currently this is always an edge ID, but we may
470    /// add axes to the graph.
471    pub axis_id: Option<ArtifactId>,
472    pub code_ref: CodeRef,
473    /// The sweep, if any, that this Helix serves as the trajectory for.
474    pub trajectory_sweep_id: Option<ArtifactId>,
475    /// Whether this artifact has been used in a subsequent operation
476    pub consumed: bool,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
480#[ts(export_to = "Artifact.ts")]
481#[serde(rename_all = "camelCase")]
482pub struct GdtAnnotationArtifact {
483    pub id: ArtifactId,
484    pub code_ref: CodeRef,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
488#[ts(export_to = "Artifact.ts")]
489#[serde(rename_all = "camelCase")]
490pub struct Pattern {
491    pub id: ArtifactId,
492    pub sub_type: PatternSubType,
493    /// Geometry artifact that was the source of the pattern operation.
494    pub source_id: ArtifactId,
495    /// IDs of copied top-level objects created by the pattern operation.
496    pub copy_ids: Vec<ArtifactId>,
497    /// IDs of copied faces created by the pattern operation.
498    pub copy_face_ids: Vec<ArtifactId>,
499    /// IDs of copied edges created by the pattern operation.
500    pub copy_edge_ids: Vec<ArtifactId>,
501    pub code_ref: CodeRef,
502}
503
504#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
505#[ts(export_to = "Artifact.ts")]
506#[serde(rename_all = "camelCase")]
507pub enum PatternSubType {
508    Circular,
509    Linear,
510    Transform,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
514#[ts(export_to = "Artifact.ts")]
515#[serde(tag = "type", rename_all = "camelCase")]
516pub enum Artifact {
517    CompositeSolid(CompositeSolid),
518    Plane(Plane),
519    Path(Path),
520    Segment(Segment),
521    Solid2d(Solid2d),
522    PrimitiveFace(PrimitiveFace),
523    PrimitiveEdge(PrimitiveEdge),
524    PlaneOfFace(PlaneOfFace),
525    StartSketchOnFace(StartSketchOnFace),
526    StartSketchOnPlane(StartSketchOnPlane),
527    SketchBlock(SketchBlock),
528    SketchBlockConstraint(SketchBlockConstraint),
529    Sweep(Sweep),
530    Wall(Wall),
531    Cap(Cap),
532    SweepEdge(SweepEdge),
533    EdgeCut(EdgeCut),
534    EdgeCutEdge(EdgeCutEdge),
535    Helix(Helix),
536    GdtAnnotation(GdtAnnotationArtifact),
537    Pattern(Pattern),
538}
539
540impl Artifact {
541    pub fn id(&self) -> ArtifactId {
542        match self {
543            Self::CompositeSolid(a) => a.id,
544            Self::Plane(a) => a.id,
545            Self::Path(a) => a.id,
546            Self::Segment(a) => a.id,
547            Self::Solid2d(a) => a.id,
548            Self::PrimitiveFace(a) => a.id,
549            Self::PrimitiveEdge(a) => a.id,
550            Self::PlaneOfFace(a) => a.id,
551            Self::StartSketchOnFace(a) => a.id,
552            Self::StartSketchOnPlane(a) => a.id,
553            Self::SketchBlock(a) => a.id,
554            Self::SketchBlockConstraint(a) => a.id,
555            Self::Sweep(a) => a.id,
556            Self::Wall(a) => a.id,
557            Self::Cap(a) => a.id,
558            Self::SweepEdge(a) => a.id,
559            Self::EdgeCut(a) => a.id,
560            Self::EdgeCutEdge(a) => a.id,
561            Self::Helix(a) => a.id,
562            Self::GdtAnnotation(a) => a.id,
563            Self::Pattern(a) => a.id,
564        }
565    }
566
567    /// The [`CodeRef`] for the artifact itself. See also
568    /// [`Self::face_code_ref`].
569    pub fn code_ref(&self) -> Option<&CodeRef> {
570        match self {
571            Self::CompositeSolid(a) => Some(&a.code_ref),
572            Self::Plane(a) => Some(&a.code_ref),
573            Self::Path(a) => Some(&a.code_ref),
574            Self::Segment(a) => Some(&a.code_ref),
575            Self::Solid2d(_) => None,
576            Self::PrimitiveFace(a) => Some(&a.code_ref),
577            Self::PrimitiveEdge(a) => Some(&a.code_ref),
578            Self::PlaneOfFace(a) => Some(&a.code_ref),
579            Self::StartSketchOnFace(a) => Some(&a.code_ref),
580            Self::StartSketchOnPlane(a) => Some(&a.code_ref),
581            Self::SketchBlock(a) => Some(&a.code_ref),
582            Self::SketchBlockConstraint(a) => Some(&a.code_ref),
583            Self::Sweep(a) => Some(&a.code_ref),
584            Self::Wall(_) | Self::Cap(_) | Self::SweepEdge(_) => None,
585            Self::EdgeCut(a) => Some(&a.code_ref),
586            Self::EdgeCutEdge(_) => None,
587            Self::Helix(a) => Some(&a.code_ref),
588            Self::GdtAnnotation(a) => Some(&a.code_ref),
589            Self::Pattern(a) => Some(&a.code_ref),
590        }
591    }
592
593    /// The [`CodeRef`] referring to the face artifact that it's on, not the
594    /// artifact itself.
595    pub fn face_code_ref(&self) -> Option<&CodeRef> {
596        match self {
597            Self::PrimitiveFace(a) => Some(&a.code_ref),
598            Self::Wall(a) => Some(&a.face_code_ref),
599            Self::Cap(a) => Some(&a.face_code_ref),
600            _ => None,
601        }
602    }
603}
604
605#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, ts_rs::TS)]
606#[ts(export_to = "Artifact.ts")]
607#[serde(rename_all = "camelCase")]
608pub struct ArtifactGraph {
609    map: IndexMap<ArtifactId, Artifact>,
610    item_count: usize,
611}
612
613impl ArtifactGraph {
614    pub fn from_parts(map: IndexMap<ArtifactId, Artifact>, item_count: usize) -> Self {
615        Self { map, item_count }
616    }
617
618    pub fn into_parts(self) -> (IndexMap<ArtifactId, Artifact>, usize) {
619        (self.map, self.item_count)
620    }
621
622    pub fn item_count(&self) -> usize {
623        self.item_count
624    }
625
626    pub fn get(&self, id: &ArtifactId) -> Option<&Artifact> {
627        self.map.get(id)
628    }
629
630    pub fn len(&self) -> usize {
631        self.map.len()
632    }
633
634    pub fn is_empty(&self) -> bool {
635        self.map.is_empty()
636    }
637
638    pub fn iter(&self) -> impl Iterator<Item = (&ArtifactId, &Artifact)> {
639        self.map.iter()
640    }
641
642    pub fn values(&self) -> impl Iterator<Item = &Artifact> {
643        self.map.values()
644    }
645
646    pub fn clear(&mut self) {
647        self.map.clear();
648        self.item_count = 0;
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    #[test]
657    fn default_artifact_graph_json_round_trip() {
658        let graph = ArtifactGraph::default();
659        let json = serde_json::to_string(&graph).unwrap();
660
661        assert_eq!(json, r#"{"map":{},"itemCount":0}"#);
662        assert_eq!(serde_json::from_str::<ArtifactGraph>(&json).unwrap(), graph);
663    }
664
665    #[test]
666    fn artifact_sweep_method_uses_engine_json_shape() {
667        assert_eq!(serde_json::to_string(&ArtifactSweepMethod::New).unwrap(), r#""new""#);
668        assert_eq!(
669            serde_json::to_string(&ArtifactSweepMethod::Merge).unwrap(),
670            r#""merge""#
671        );
672    }
673}