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    /// The original segment this segment was cloned from, if any. For clones
208    /// of clones, this continues to point to the originating segment.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub source_segment_id: Option<ArtifactId>,
211    /// If this artifact is a segment in a region, the segment in the original
212    /// sketch that this was derived from.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub original_seg_id: Option<ArtifactId>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub surface_id: Option<ArtifactId>,
217    pub edge_ids: Vec<ArtifactId>,
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub edge_cut_id: Option<ArtifactId>,
220    pub code_ref: CodeRef,
221    pub common_surface_ids: Vec<ArtifactId>,
222}
223
224/// A sweep is a more generic term for extrude, revolve, loft, sweep, and blend.
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
226#[ts(export_to = "Artifact.ts")]
227#[serde(rename_all = "camelCase")]
228pub struct Sweep {
229    pub id: ArtifactId,
230    pub sub_type: SweepSubType,
231    pub path_id: ArtifactId,
232    pub surface_ids: Vec<ArtifactId>,
233    pub edge_ids: Vec<ArtifactId>,
234    pub code_ref: CodeRef,
235    /// The original sweep this body was cloned from, if any. For clones of
236    /// clones, this continues to point to the originating sweep.
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub source_sweep_id: Option<ArtifactId>,
239    /// ID of trajectory path for sweep, if any
240    /// Only applicable to SweepSubType::Sweep and SweepSubType::Blend, which
241    /// can use a second path-like input
242    pub trajectory_id: Option<ArtifactId>,
243    pub method: ArtifactSweepMethod,
244    /// Whether this artifact has been used in a subsequent operation
245    pub consumed: bool,
246    /// Pattern operations that use this sweep as their source.
247    #[serde(default, skip_serializing_if = "Vec::is_empty")]
248    pub pattern_ids: Vec<ArtifactId>,
249}
250
251#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
252#[ts(export_to = "Artifact.ts")]
253#[serde(rename_all = "camelCase")]
254pub enum SweepSubType {
255    Extrusion,
256    ExtrusionTwist,
257    Revolve,
258    RevolveAboutEdge,
259    Loft,
260    Blend,
261    Sweep,
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 Solid2d {
268    pub id: ArtifactId,
269    pub path_id: ArtifactId,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
273#[ts(export_to = "Artifact.ts")]
274#[serde(rename_all = "camelCase")]
275pub struct PrimitiveFace {
276    pub id: ArtifactId,
277    pub solid_id: ArtifactId,
278    pub code_ref: CodeRef,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
282#[ts(export_to = "Artifact.ts")]
283#[serde(rename_all = "camelCase")]
284pub struct PrimitiveEdge {
285    pub id: ArtifactId,
286    pub solid_id: ArtifactId,
287    pub code_ref: CodeRef,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
291#[ts(export_to = "Artifact.ts")]
292#[serde(rename_all = "camelCase")]
293pub struct PlaneOfFace {
294    pub id: ArtifactId,
295    pub face_id: ArtifactId,
296    pub code_ref: CodeRef,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
300#[ts(export_to = "Artifact.ts")]
301#[serde(rename_all = "camelCase")]
302pub struct StartSketchOnFace {
303    pub id: ArtifactId,
304    pub face_id: ArtifactId,
305    pub code_ref: CodeRef,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
309#[ts(export_to = "Artifact.ts")]
310#[serde(rename_all = "camelCase")]
311pub struct StartSketchOnPlane {
312    pub id: ArtifactId,
313    pub plane_id: ArtifactId,
314    pub code_ref: CodeRef,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
318#[ts(export_to = "Artifact.ts")]
319#[serde(rename_all = "camelCase")]
320pub struct SketchBlock {
321    pub id: ArtifactId,
322    /// The semantic standard plane name when the sketch block is on a standard plane.
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub standard_plane: Option<PlaneName>,
325    /// The concrete plane artifact ID backing the sketch block, when one is available.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub plane_id: Option<ArtifactId>,
328    /// The evaluated plane data backing the sketch block, when the sketch is on a plane.
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub plane_info: Option<ArtifactPlaneInfo>,
331    /// The path artifact ID created from the sketch block, if there is one.
332    /// There are edge cases when a path isn't created, like when there are no
333    /// segments.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub path_id: Option<ArtifactId>,
336    pub code_ref: CodeRef,
337    /// The sketch ID (ObjectId) for the sketch scene object.
338    pub sketch_id: ObjectId,
339}
340
341#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
342#[ts(export_to = "Artifact.ts")]
343#[serde(rename_all = "camelCase")]
344pub enum SketchBlockConstraintType {
345    Angle,
346    Coincident,
347    Distance,
348    Diameter,
349    EqualRadius,
350    Fixed,
351    HorizontalDistance,
352    VerticalDistance,
353    Horizontal,
354    LinesEqualLength,
355    Midpoint,
356    Parallel,
357    Perpendicular,
358    Radius,
359    Symmetric,
360    Tangent,
361    Vertical,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
365#[ts(export_to = "Artifact.ts")]
366#[serde(rename_all = "camelCase")]
367pub struct SketchBlockConstraint {
368    pub id: ArtifactId,
369    /// The sketch ID (ObjectId) that owns this constraint.
370    pub sketch_id: ObjectId,
371    /// The constraint ID (ObjectId) for the constraint scene object.
372    pub constraint_id: ObjectId,
373    pub constraint_type: SketchBlockConstraintType,
374    pub code_ref: CodeRef,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
378#[ts(export_to = "Artifact.ts")]
379#[serde(rename_all = "camelCase")]
380pub struct Wall {
381    pub id: ArtifactId,
382    pub seg_id: ArtifactId,
383    pub edge_cut_edge_ids: Vec<ArtifactId>,
384    pub sweep_id: ArtifactId,
385    pub path_ids: Vec<ArtifactId>,
386    /// This is for the sketch-on-face plane, not for the wall itself.  Traverse
387    /// to the extrude and/or segment to get the wall's code_ref.
388    pub face_code_ref: CodeRef,
389    /// The command ID that got the data for this wall. Used for stable sorting.
390    pub cmd_id: Uuid,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
394#[ts(export_to = "Artifact.ts")]
395#[serde(rename_all = "camelCase")]
396pub struct Cap {
397    pub id: ArtifactId,
398    pub sub_type: CapSubType,
399    pub edge_cut_edge_ids: Vec<ArtifactId>,
400    pub sweep_id: ArtifactId,
401    pub path_ids: Vec<ArtifactId>,
402    /// This is for the sketch-on-face plane, not for the cap itself.  Traverse
403    /// to the extrude and/or segment to get the cap's code_ref.
404    pub face_code_ref: CodeRef,
405    /// The command ID that got the data for this cap. Used for stable sorting.
406    pub cmd_id: Uuid,
407}
408
409#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
410#[ts(export_to = "Artifact.ts")]
411#[serde(rename_all = "camelCase")]
412pub enum CapSubType {
413    Start,
414    End,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
418#[ts(export_to = "Artifact.ts")]
419#[serde(rename_all = "camelCase")]
420pub struct SweepEdge {
421    pub id: ArtifactId,
422    pub sub_type: SweepEdgeSubType,
423    pub seg_id: ArtifactId,
424    pub cmd_id: Uuid,
425    // This is only used for sorting, not for the actual artifact.
426    #[serde(skip)]
427    pub index: usize,
428    pub sweep_id: ArtifactId,
429    pub common_surface_ids: Vec<ArtifactId>,
430}
431
432#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
433#[ts(export_to = "Artifact.ts")]
434#[serde(rename_all = "camelCase")]
435pub enum SweepEdgeSubType {
436    Opposite,
437    Adjacent,
438    PreviousAdjacent,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
442#[ts(export_to = "Artifact.ts")]
443#[serde(rename_all = "camelCase")]
444pub struct EdgeCut {
445    pub id: ArtifactId,
446    pub sub_type: EdgeCutSubType,
447    pub consumed_edge_id: ArtifactId,
448    pub edge_ids: Vec<ArtifactId>,
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub surface_id: Option<ArtifactId>,
451    pub code_ref: CodeRef,
452}
453
454#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
455#[ts(export_to = "Artifact.ts")]
456#[serde(rename_all = "camelCase")]
457pub enum EdgeCutSubType {
458    Fillet,
459    Chamfer,
460    Custom,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
464#[ts(export_to = "Artifact.ts")]
465#[serde(rename_all = "camelCase")]
466pub struct EdgeCutEdge {
467    pub id: ArtifactId,
468    pub edge_cut_id: ArtifactId,
469    pub surface_id: ArtifactId,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
473#[ts(export_to = "Artifact.ts")]
474#[serde(rename_all = "camelCase")]
475pub struct Helix {
476    pub id: ArtifactId,
477    /// The axis of the helix.  Currently this is always an edge ID, but we may
478    /// add axes to the graph.
479    pub axis_id: Option<ArtifactId>,
480    pub code_ref: CodeRef,
481    /// The sweep, if any, that this Helix serves as the trajectory for.
482    pub trajectory_sweep_id: Option<ArtifactId>,
483    /// Whether this artifact has been used in a subsequent operation
484    pub consumed: bool,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
488#[ts(export_to = "Artifact.ts")]
489#[serde(rename_all = "camelCase")]
490pub struct GdtAnnotationArtifact {
491    pub id: ArtifactId,
492    pub code_ref: CodeRef,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
496#[ts(export_to = "Artifact.ts")]
497#[serde(rename_all = "camelCase")]
498pub struct Pattern {
499    pub id: ArtifactId,
500    pub sub_type: PatternSubType,
501    /// Geometry artifact that was the source of the pattern operation.
502    pub source_id: ArtifactId,
503    /// IDs of copied top-level objects created by the pattern operation.
504    pub copy_ids: Vec<ArtifactId>,
505    /// IDs of copied faces created by the pattern operation.
506    pub copy_face_ids: Vec<ArtifactId>,
507    /// IDs of copied edges created by the pattern operation.
508    pub copy_edge_ids: Vec<ArtifactId>,
509    pub code_ref: CodeRef,
510}
511
512#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ts_rs::TS)]
513#[ts(export_to = "Artifact.ts")]
514#[serde(rename_all = "camelCase")]
515pub enum PatternSubType {
516    Circular,
517    Linear,
518    Transform,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, ts_rs::TS)]
522#[ts(export_to = "Artifact.ts")]
523#[serde(tag = "type", rename_all = "camelCase")]
524pub enum Artifact {
525    CompositeSolid(CompositeSolid),
526    Plane(Plane),
527    Path(Path),
528    Segment(Segment),
529    Solid2d(Solid2d),
530    PrimitiveFace(PrimitiveFace),
531    PrimitiveEdge(PrimitiveEdge),
532    PlaneOfFace(PlaneOfFace),
533    StartSketchOnFace(StartSketchOnFace),
534    StartSketchOnPlane(StartSketchOnPlane),
535    SketchBlock(SketchBlock),
536    SketchBlockConstraint(SketchBlockConstraint),
537    Sweep(Sweep),
538    Wall(Wall),
539    Cap(Cap),
540    SweepEdge(SweepEdge),
541    EdgeCut(EdgeCut),
542    EdgeCutEdge(EdgeCutEdge),
543    Helix(Helix),
544    GdtAnnotation(GdtAnnotationArtifact),
545    Pattern(Pattern),
546}
547
548impl Artifact {
549    pub fn id(&self) -> ArtifactId {
550        match self {
551            Self::CompositeSolid(a) => a.id,
552            Self::Plane(a) => a.id,
553            Self::Path(a) => a.id,
554            Self::Segment(a) => a.id,
555            Self::Solid2d(a) => a.id,
556            Self::PrimitiveFace(a) => a.id,
557            Self::PrimitiveEdge(a) => a.id,
558            Self::PlaneOfFace(a) => a.id,
559            Self::StartSketchOnFace(a) => a.id,
560            Self::StartSketchOnPlane(a) => a.id,
561            Self::SketchBlock(a) => a.id,
562            Self::SketchBlockConstraint(a) => a.id,
563            Self::Sweep(a) => a.id,
564            Self::Wall(a) => a.id,
565            Self::Cap(a) => a.id,
566            Self::SweepEdge(a) => a.id,
567            Self::EdgeCut(a) => a.id,
568            Self::EdgeCutEdge(a) => a.id,
569            Self::Helix(a) => a.id,
570            Self::GdtAnnotation(a) => a.id,
571            Self::Pattern(a) => a.id,
572        }
573    }
574
575    /// The [`CodeRef`] for the artifact itself. See also
576    /// [`Self::face_code_ref`].
577    pub fn code_ref(&self) -> Option<&CodeRef> {
578        match self {
579            Self::CompositeSolid(a) => Some(&a.code_ref),
580            Self::Plane(a) => Some(&a.code_ref),
581            Self::Path(a) => Some(&a.code_ref),
582            Self::Segment(a) => Some(&a.code_ref),
583            Self::Solid2d(_) => None,
584            Self::PrimitiveFace(a) => Some(&a.code_ref),
585            Self::PrimitiveEdge(a) => Some(&a.code_ref),
586            Self::PlaneOfFace(a) => Some(&a.code_ref),
587            Self::StartSketchOnFace(a) => Some(&a.code_ref),
588            Self::StartSketchOnPlane(a) => Some(&a.code_ref),
589            Self::SketchBlock(a) => Some(&a.code_ref),
590            Self::SketchBlockConstraint(a) => Some(&a.code_ref),
591            Self::Sweep(a) => Some(&a.code_ref),
592            Self::Wall(_) | Self::Cap(_) | Self::SweepEdge(_) => None,
593            Self::EdgeCut(a) => Some(&a.code_ref),
594            Self::EdgeCutEdge(_) => None,
595            Self::Helix(a) => Some(&a.code_ref),
596            Self::GdtAnnotation(a) => Some(&a.code_ref),
597            Self::Pattern(a) => Some(&a.code_ref),
598        }
599    }
600
601    /// The [`CodeRef`] referring to the face artifact that it's on, not the
602    /// artifact itself.
603    pub fn face_code_ref(&self) -> Option<&CodeRef> {
604        match self {
605            Self::PrimitiveFace(a) => Some(&a.code_ref),
606            Self::Wall(a) => Some(&a.face_code_ref),
607            Self::Cap(a) => Some(&a.face_code_ref),
608            _ => None,
609        }
610    }
611}
612
613#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, ts_rs::TS)]
614#[ts(export_to = "Artifact.ts")]
615#[serde(rename_all = "camelCase")]
616pub struct ArtifactGraph {
617    map: IndexMap<ArtifactId, Artifact>,
618    item_count: usize,
619}
620
621impl ArtifactGraph {
622    pub fn from_parts(map: IndexMap<ArtifactId, Artifact>, item_count: usize) -> Self {
623        Self { map, item_count }
624    }
625
626    pub fn into_parts(self) -> (IndexMap<ArtifactId, Artifact>, usize) {
627        (self.map, self.item_count)
628    }
629
630    pub fn item_count(&self) -> usize {
631        self.item_count
632    }
633
634    pub fn get(&self, id: &ArtifactId) -> Option<&Artifact> {
635        self.map.get(id)
636    }
637
638    pub fn len(&self) -> usize {
639        self.map.len()
640    }
641
642    pub fn is_empty(&self) -> bool {
643        self.map.is_empty()
644    }
645
646    pub fn iter(&self) -> impl Iterator<Item = (&ArtifactId, &Artifact)> {
647        self.map.iter()
648    }
649
650    pub fn values(&self) -> impl Iterator<Item = &Artifact> {
651        self.map.values()
652    }
653
654    pub fn clear(&mut self) {
655        self.map.clear();
656        self.item_count = 0;
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn default_artifact_graph_json_round_trip() {
666        let graph = ArtifactGraph::default();
667        let json = serde_json::to_string(&graph).unwrap();
668
669        assert_eq!(json, r#"{"map":{},"itemCount":0}"#);
670        assert_eq!(serde_json::from_str::<ArtifactGraph>(&json).unwrap(), graph);
671    }
672
673    #[test]
674    fn artifact_sweep_method_uses_engine_json_shape() {
675        assert_eq!(serde_json::to_string(&ArtifactSweepMethod::New).unwrap(), r#""new""#);
676        assert_eq!(
677            serde_json::to_string(&ArtifactSweepMethod::Merge).unwrap(),
678            r#""merge""#
679        );
680    }
681}