Skip to main content

kcl_lib/execution/
artifact.rs

1use fnv::FnvHashMap;
2use indexmap::IndexMap;
3use kittycad_modeling_cmds::EnableSketchMode;
4use kittycad_modeling_cmds::FaceIsPlanar;
5use kittycad_modeling_cmds::ModelingCmd;
6use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
7use kittycad_modeling_cmds::shared::ExtrusionFaceCapType;
8use kittycad_modeling_cmds::websocket::BatchResponse;
9use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
10use kittycad_modeling_cmds::websocket::WebSocketResponse;
11use kittycad_modeling_cmds::{self as kcmc};
12use serde::Serialize;
13use serde::ser::SerializeSeq;
14use uuid::Uuid;
15
16use crate::KclError;
17use crate::ModuleId;
18use crate::NodePath;
19use crate::SourceRange;
20use crate::engine::PlaneName;
21use crate::errors::KclErrorDetails;
22use crate::execution::ArtifactId;
23use crate::execution::state::ModuleInfoMap;
24use crate::front::Constraint;
25use crate::front::ObjectId;
26use crate::modules::ModulePath;
27use crate::parsing::ast::types::BodyItem;
28use crate::parsing::ast::types::ImportPath;
29use crate::parsing::ast::types::ImportSelector;
30use crate::parsing::ast::types::Node;
31use crate::parsing::ast::types::Program;
32use crate::std::sketch::build_reverse_region_mapping;
33
34#[cfg(test)]
35mod mermaid_tests;
36
37macro_rules! internal_error {
38    ($range:expr, $($rest:tt)*) => {{
39        let message = format!($($rest)*);
40        debug_assert!(false, "{}", &message);
41        return Err(KclError::new_internal(KclErrorDetails::new(message, vec![$range])));
42    }};
43}
44
45/// A command that may create or update artifacts on the TS side.  Because
46/// engine commands are batched, we don't have the response yet when these are
47/// created.
48#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
49#[ts(export_to = "Artifact.ts")]
50#[serde(rename_all = "camelCase")]
51pub struct ArtifactCommand {
52    /// Identifier of the command that can be matched with its response.
53    pub cmd_id: Uuid,
54    /// The source range that's the boundary of calling the standard
55    /// library, not necessarily the true source range of the command.
56    pub range: SourceRange,
57    /// The engine command.  Each artifact command is backed by an engine
58    /// command.  In the future, we may need to send information to the TS side
59    /// without an engine command, in which case, we would make this field
60    /// optional.
61    pub command: ModelingCmd,
62}
63
64pub type DummyPathToNode = Vec<()>;
65
66fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
67where
68    S: serde::Serializer,
69{
70    // Always output an empty array, for now.
71    let seq = serializer.serialize_seq(Some(0))?;
72    seq.end()
73}
74
75#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, ts_rs::TS)]
76#[ts(export_to = "Artifact.ts")]
77#[serde(rename_all = "camelCase")]
78pub struct CodeRef {
79    pub range: SourceRange,
80    pub node_path: NodePath,
81    // TODO: We should implement this in Rust.
82    #[serde(default, serialize_with = "serialize_dummy_path_to_node")]
83    #[ts(type = "Array<[string | number, string]>")]
84    pub path_to_node: DummyPathToNode,
85}
86
87impl CodeRef {
88    pub fn placeholder(range: SourceRange) -> Self {
89        Self {
90            range,
91            node_path: Default::default(),
92            path_to_node: Vec::new(),
93        }
94    }
95}
96
97#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
98#[ts(export_to = "Artifact.ts")]
99#[serde(rename_all = "camelCase")]
100pub struct CompositeSolid {
101    pub id: ArtifactId,
102    /// Whether this artifact has been used in a subsequent operation
103    pub consumed: bool,
104    pub sub_type: CompositeSolidSubType,
105    /// Index of this output in the expression result, for operations that
106    /// return multiple selectable bodies from one KCL variable.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub output_index: Option<usize>,
109    /// Constituent solids of the composite solid.
110    pub solid_ids: Vec<ArtifactId>,
111    /// Tool solids used for asymmetric operations like subtract.
112    pub tool_ids: Vec<ArtifactId>,
113    pub code_ref: CodeRef,
114    /// This is the ID of the composite solid that this is part of, if any, as a
115    /// composite solid can be used as input for another composite solid.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub composite_solid_id: Option<ArtifactId>,
118}
119
120#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
121#[ts(export_to = "Artifact.ts")]
122#[serde(rename_all = "camelCase")]
123pub enum CompositeSolidSubType {
124    Intersect,
125    Subtract,
126    Split,
127    Union,
128}
129
130#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
131#[ts(export_to = "Artifact.ts")]
132#[serde(rename_all = "camelCase")]
133pub struct Plane {
134    pub id: ArtifactId,
135    pub path_ids: Vec<ArtifactId>,
136    pub code_ref: CodeRef,
137}
138
139#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
140#[ts(export_to = "Artifact.ts")]
141#[serde(rename_all = "camelCase")]
142pub struct Path {
143    pub id: ArtifactId,
144    pub sub_type: PathSubType,
145    pub plane_id: ArtifactId,
146    pub seg_ids: Vec<ArtifactId>,
147    /// Whether this artifact has been used in a subsequent operation
148    pub consumed: bool,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    /// The sweep, if any, that this Path serves as the base path for.
151    /// corresponds to `path_id` on the Sweep.
152    pub sweep_id: Option<ArtifactId>,
153    /// The sweep, if any, that this Path serves as the trajectory for.
154    pub trajectory_sweep_id: Option<ArtifactId>,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub solid2d_id: Option<ArtifactId>,
157    pub code_ref: CodeRef,
158    /// This is the ID of the composite solid that this is part of, if any, as
159    /// this can be used as input for another composite solid.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub composite_solid_id: Option<ArtifactId>,
162    /// For sketch paths, the ID of the sketch block this path was created
163    /// from. `None` for region paths and paths created in other ways.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub sketch_block_id: Option<ArtifactId>,
166    /// For region paths, the ID of the sketch path this region was created
167    /// from. `None` for sketch paths.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub origin_path_id: Option<ArtifactId>,
170    /// The hole, if any, from a subtract2d() call.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub inner_path_id: Option<ArtifactId>,
173    /// The `Path` that this is a hole of, if any. The inverse link of
174    /// `inner_path_id`.
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub outer_path_id: Option<ArtifactId>,
177}
178
179#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
180#[ts(export_to = "Artifact.ts")]
181#[serde(rename_all = "camelCase")]
182pub enum PathSubType {
183    Sketch,
184    Region,
185}
186
187#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
188#[ts(export_to = "Artifact.ts")]
189#[serde(rename_all = "camelCase")]
190pub struct Segment {
191    pub id: ArtifactId,
192    pub path_id: ArtifactId,
193    /// If this artifact is a segment in a region, the segment in the original
194    /// sketch that this was derived from.
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub original_seg_id: Option<ArtifactId>,
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub surface_id: Option<ArtifactId>,
199    pub edge_ids: Vec<ArtifactId>,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub edge_cut_id: Option<ArtifactId>,
202    pub code_ref: CodeRef,
203    pub common_surface_ids: Vec<ArtifactId>,
204}
205
206/// A sweep is a more generic term for extrude, revolve, loft, sweep, and blend.
207#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
208#[ts(export_to = "Artifact.ts")]
209#[serde(rename_all = "camelCase")]
210pub struct Sweep {
211    pub id: ArtifactId,
212    pub sub_type: SweepSubType,
213    pub path_id: ArtifactId,
214    pub surface_ids: Vec<ArtifactId>,
215    pub edge_ids: Vec<ArtifactId>,
216    pub code_ref: CodeRef,
217    /// ID of trajectory path for sweep, if any
218    /// Only applicable to SweepSubType::Sweep and SweepSubType::Blend, which
219    /// can use a second path-like input
220    pub trajectory_id: Option<ArtifactId>,
221    pub method: kittycad_modeling_cmds::shared::ExtrudeMethod,
222    /// Whether this artifact has been used in a subsequent operation
223    pub consumed: bool,
224}
225
226#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
227#[ts(export_to = "Artifact.ts")]
228#[serde(rename_all = "camelCase")]
229pub enum SweepSubType {
230    Extrusion,
231    ExtrusionTwist,
232    Revolve,
233    RevolveAboutEdge,
234    Loft,
235    Blend,
236    Sweep,
237}
238
239#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
240#[ts(export_to = "Artifact.ts")]
241#[serde(rename_all = "camelCase")]
242pub struct Solid2d {
243    pub id: ArtifactId,
244    pub path_id: ArtifactId,
245}
246
247#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
248#[ts(export_to = "Artifact.ts")]
249#[serde(rename_all = "camelCase")]
250pub struct PrimitiveFace {
251    pub id: ArtifactId,
252    pub solid_id: ArtifactId,
253    pub code_ref: CodeRef,
254}
255
256#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
257#[ts(export_to = "Artifact.ts")]
258#[serde(rename_all = "camelCase")]
259pub struct PrimitiveEdge {
260    pub id: ArtifactId,
261    pub solid_id: ArtifactId,
262    pub code_ref: CodeRef,
263}
264
265#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
266#[ts(export_to = "Artifact.ts")]
267#[serde(rename_all = "camelCase")]
268pub struct PlaneOfFace {
269    pub id: ArtifactId,
270    pub face_id: ArtifactId,
271    pub code_ref: CodeRef,
272}
273
274#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
275#[ts(export_to = "Artifact.ts")]
276#[serde(rename_all = "camelCase")]
277pub struct StartSketchOnFace {
278    pub id: ArtifactId,
279    pub face_id: ArtifactId,
280    pub code_ref: CodeRef,
281}
282
283#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
284#[ts(export_to = "Artifact.ts")]
285#[serde(rename_all = "camelCase")]
286pub struct StartSketchOnPlane {
287    pub id: ArtifactId,
288    pub plane_id: ArtifactId,
289    pub code_ref: CodeRef,
290}
291
292#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
293#[ts(export_to = "Artifact.ts")]
294#[serde(rename_all = "camelCase")]
295pub struct SketchBlock {
296    pub id: ArtifactId,
297    /// The semantic standard plane name when the sketch block is on a standard plane.
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub standard_plane: Option<PlaneName>,
300    /// The concrete plane artifact ID backing the sketch block, when one is available.
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub plane_id: Option<ArtifactId>,
303    /// The path artifact ID created from the sketch block, if there is one.
304    /// There are edge cases when a path isn't created, like when there are no
305    /// segments.
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub path_id: Option<ArtifactId>,
308    pub code_ref: CodeRef,
309    /// The sketch ID (ObjectId) for the sketch scene object.
310    pub sketch_id: ObjectId,
311}
312
313#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
314#[ts(export_to = "Artifact.ts")]
315#[serde(rename_all = "camelCase")]
316pub enum SketchBlockConstraintType {
317    Angle,
318    Coincident,
319    Distance,
320    Diameter,
321    EqualRadius,
322    Fixed,
323    HorizontalDistance,
324    VerticalDistance,
325    Horizontal,
326    LinesEqualLength,
327    Midpoint,
328    Parallel,
329    Perpendicular,
330    Radius,
331    Symmetric,
332    Tangent,
333    Vertical,
334}
335
336impl From<&Constraint> for SketchBlockConstraintType {
337    fn from(constraint: &Constraint) -> Self {
338        match constraint {
339            Constraint::Coincident { .. } => SketchBlockConstraintType::Coincident,
340            Constraint::Distance { .. } => SketchBlockConstraintType::Distance,
341            Constraint::Diameter { .. } => SketchBlockConstraintType::Diameter,
342            Constraint::EqualRadius { .. } => SketchBlockConstraintType::EqualRadius,
343            Constraint::Fixed { .. } => SketchBlockConstraintType::Fixed,
344            Constraint::HorizontalDistance { .. } => SketchBlockConstraintType::HorizontalDistance,
345            Constraint::VerticalDistance { .. } => SketchBlockConstraintType::VerticalDistance,
346            Constraint::Horizontal { .. } => SketchBlockConstraintType::Horizontal,
347            Constraint::LinesEqualLength { .. } => SketchBlockConstraintType::LinesEqualLength,
348            Constraint::Midpoint(..) => SketchBlockConstraintType::Midpoint,
349            Constraint::Parallel { .. } => SketchBlockConstraintType::Parallel,
350            Constraint::Perpendicular { .. } => SketchBlockConstraintType::Perpendicular,
351            Constraint::Radius { .. } => SketchBlockConstraintType::Radius,
352            Constraint::Symmetric { .. } => SketchBlockConstraintType::Symmetric,
353            Constraint::Tangent { .. } => SketchBlockConstraintType::Tangent,
354            Constraint::Vertical { .. } => SketchBlockConstraintType::Vertical,
355            Constraint::Angle(..) => SketchBlockConstraintType::Angle,
356        }
357    }
358}
359
360#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
361#[ts(export_to = "Artifact.ts")]
362#[serde(rename_all = "camelCase")]
363pub struct SketchBlockConstraint {
364    pub id: ArtifactId,
365    /// The sketch ID (ObjectId) that owns this constraint.
366    pub sketch_id: ObjectId,
367    /// The constraint ID (ObjectId) for the constraint scene object.
368    pub constraint_id: ObjectId,
369    pub constraint_type: SketchBlockConstraintType,
370    pub code_ref: CodeRef,
371}
372
373#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
374#[ts(export_to = "Artifact.ts")]
375#[serde(rename_all = "camelCase")]
376pub struct Wall {
377    pub id: ArtifactId,
378    pub seg_id: ArtifactId,
379    pub edge_cut_edge_ids: Vec<ArtifactId>,
380    pub sweep_id: ArtifactId,
381    pub path_ids: Vec<ArtifactId>,
382    /// This is for the sketch-on-face plane, not for the wall itself.  Traverse
383    /// to the extrude and/or segment to get the wall's code_ref.
384    pub face_code_ref: CodeRef,
385    /// The command ID that got the data for this wall. Used for stable sorting.
386    pub cmd_id: uuid::Uuid,
387}
388
389#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
390#[ts(export_to = "Artifact.ts")]
391#[serde(rename_all = "camelCase")]
392pub struct Cap {
393    pub id: ArtifactId,
394    pub sub_type: CapSubType,
395    pub edge_cut_edge_ids: Vec<ArtifactId>,
396    pub sweep_id: ArtifactId,
397    pub path_ids: Vec<ArtifactId>,
398    /// This is for the sketch-on-face plane, not for the cap itself.  Traverse
399    /// to the extrude and/or segment to get the cap's code_ref.
400    pub face_code_ref: CodeRef,
401    /// The command ID that got the data for this cap. Used for stable sorting.
402    pub cmd_id: uuid::Uuid,
403}
404
405#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
406#[ts(export_to = "Artifact.ts")]
407#[serde(rename_all = "camelCase")]
408pub enum CapSubType {
409    Start,
410    End,
411}
412
413#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
414#[ts(export_to = "Artifact.ts")]
415#[serde(rename_all = "camelCase")]
416pub struct SweepEdge {
417    pub id: ArtifactId,
418    pub sub_type: SweepEdgeSubType,
419    pub seg_id: ArtifactId,
420    pub cmd_id: uuid::Uuid,
421    // This is only used for sorting, not for the actual artifact.
422    #[serde(skip)]
423    pub index: usize,
424    pub sweep_id: ArtifactId,
425    pub common_surface_ids: Vec<ArtifactId>,
426}
427
428#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
429#[ts(export_to = "Artifact.ts")]
430#[serde(rename_all = "camelCase")]
431pub enum SweepEdgeSubType {
432    Opposite,
433    Adjacent,
434}
435
436#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
437#[ts(export_to = "Artifact.ts")]
438#[serde(rename_all = "camelCase")]
439pub struct EdgeCut {
440    pub id: ArtifactId,
441    pub sub_type: EdgeCutSubType,
442    pub consumed_edge_id: ArtifactId,
443    pub edge_ids: Vec<ArtifactId>,
444    #[serde(default, skip_serializing_if = "Option::is_none")]
445    pub surface_id: Option<ArtifactId>,
446    pub code_ref: CodeRef,
447}
448
449#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
450#[ts(export_to = "Artifact.ts")]
451#[serde(rename_all = "camelCase")]
452pub enum EdgeCutSubType {
453    Fillet,
454    Chamfer,
455    Custom,
456}
457
458impl From<kcmc::shared::CutType> for EdgeCutSubType {
459    fn from(cut_type: kcmc::shared::CutType) -> Self {
460        match cut_type {
461            kcmc::shared::CutType::Fillet => EdgeCutSubType::Fillet,
462            kcmc::shared::CutType::Chamfer => EdgeCutSubType::Chamfer,
463        }
464    }
465}
466
467impl From<kcmc::shared::CutTypeV2> for EdgeCutSubType {
468    fn from(cut_type: kcmc::shared::CutTypeV2) -> Self {
469        match cut_type {
470            kcmc::shared::CutTypeV2::Fillet { .. } => EdgeCutSubType::Fillet,
471            kcmc::shared::CutTypeV2::Chamfer { .. } => EdgeCutSubType::Chamfer,
472            kcmc::shared::CutTypeV2::Custom { .. } => EdgeCutSubType::Custom,
473            // Modeling API has added something we're not aware of.
474            _other => EdgeCutSubType::Custom,
475        }
476    }
477}
478
479#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
480#[ts(export_to = "Artifact.ts")]
481#[serde(rename_all = "camelCase")]
482pub struct EdgeCutEdge {
483    pub id: ArtifactId,
484    pub edge_cut_id: ArtifactId,
485    pub surface_id: ArtifactId,
486}
487
488#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
489#[ts(export_to = "Artifact.ts")]
490#[serde(rename_all = "camelCase")]
491pub struct Helix {
492    pub id: ArtifactId,
493    /// The axis of the helix.  Currently this is always an edge ID, but we may
494    /// add axes to the graph.
495    pub axis_id: Option<ArtifactId>,
496    pub code_ref: CodeRef,
497    /// The sweep, if any, that this Helix serves as the trajectory for.
498    pub trajectory_sweep_id: Option<ArtifactId>,
499    /// Whether this artifact has been used in a subsequent operation
500    pub consumed: bool,
501}
502
503#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
504#[ts(export_to = "Artifact.ts")]
505#[serde(tag = "type", rename_all = "camelCase")]
506#[expect(clippy::large_enum_variant)]
507pub enum Artifact {
508    CompositeSolid(CompositeSolid),
509    Plane(Plane),
510    Path(Path),
511    Segment(Segment),
512    Solid2d(Solid2d),
513    PrimitiveFace(PrimitiveFace),
514    PrimitiveEdge(PrimitiveEdge),
515    PlaneOfFace(PlaneOfFace),
516    StartSketchOnFace(StartSketchOnFace),
517    StartSketchOnPlane(StartSketchOnPlane),
518    SketchBlock(SketchBlock),
519    SketchBlockConstraint(SketchBlockConstraint),
520    Sweep(Sweep),
521    Wall(Wall),
522    Cap(Cap),
523    SweepEdge(SweepEdge),
524    EdgeCut(EdgeCut),
525    EdgeCutEdge(EdgeCutEdge),
526    Helix(Helix),
527}
528
529impl Artifact {
530    pub(crate) fn id(&self) -> ArtifactId {
531        match self {
532            Artifact::CompositeSolid(a) => a.id,
533            Artifact::Plane(a) => a.id,
534            Artifact::Path(a) => a.id,
535            Artifact::Segment(a) => a.id,
536            Artifact::Solid2d(a) => a.id,
537            Artifact::PrimitiveFace(a) => a.id,
538            Artifact::PrimitiveEdge(a) => a.id,
539            Artifact::StartSketchOnFace(a) => a.id,
540            Artifact::StartSketchOnPlane(a) => a.id,
541            Artifact::SketchBlock(a) => a.id,
542            Artifact::SketchBlockConstraint(a) => a.id,
543            Artifact::PlaneOfFace(a) => a.id,
544            Artifact::Sweep(a) => a.id,
545            Artifact::Wall(a) => a.id,
546            Artifact::Cap(a) => a.id,
547            Artifact::SweepEdge(a) => a.id,
548            Artifact::EdgeCut(a) => a.id,
549            Artifact::EdgeCutEdge(a) => a.id,
550            Artifact::Helix(a) => a.id,
551        }
552    }
553
554    /// The [`CodeRef`] for the artifact itself. See also
555    /// [`Self::face_code_ref`].
556    pub fn code_ref(&self) -> Option<&CodeRef> {
557        match self {
558            Artifact::CompositeSolid(a) => Some(&a.code_ref),
559            Artifact::Plane(a) => Some(&a.code_ref),
560            Artifact::Path(a) => Some(&a.code_ref),
561            Artifact::Segment(a) => Some(&a.code_ref),
562            Artifact::Solid2d(_) => None,
563            Artifact::PrimitiveFace(a) => Some(&a.code_ref),
564            Artifact::PrimitiveEdge(a) => Some(&a.code_ref),
565            Artifact::StartSketchOnFace(a) => Some(&a.code_ref),
566            Artifact::StartSketchOnPlane(a) => Some(&a.code_ref),
567            Artifact::SketchBlock(a) => Some(&a.code_ref),
568            Artifact::SketchBlockConstraint(a) => Some(&a.code_ref),
569            Artifact::PlaneOfFace(a) => Some(&a.code_ref),
570            Artifact::Sweep(a) => Some(&a.code_ref),
571            Artifact::Wall(_) => None,
572            Artifact::Cap(_) => None,
573            Artifact::SweepEdge(_) => None,
574            Artifact::EdgeCut(a) => Some(&a.code_ref),
575            Artifact::EdgeCutEdge(_) => None,
576            Artifact::Helix(a) => Some(&a.code_ref),
577        }
578    }
579
580    /// The [`CodeRef`] referring to the face artifact that it's on, not the
581    /// artifact itself.
582    pub fn face_code_ref(&self) -> Option<&CodeRef> {
583        match self {
584            Artifact::CompositeSolid(_)
585            | Artifact::Plane(_)
586            | Artifact::Path(_)
587            | Artifact::Segment(_)
588            | Artifact::Solid2d(_)
589            | Artifact::PrimitiveEdge(_)
590            | Artifact::StartSketchOnFace(_)
591            | Artifact::PlaneOfFace(_)
592            | Artifact::StartSketchOnPlane(_)
593            | Artifact::SketchBlock(_)
594            | Artifact::SketchBlockConstraint(_)
595            | Artifact::Sweep(_) => None,
596            Artifact::PrimitiveFace(a) => Some(&a.code_ref),
597            Artifact::Wall(a) => Some(&a.face_code_ref),
598            Artifact::Cap(a) => Some(&a.face_code_ref),
599            Artifact::SweepEdge(_) | Artifact::EdgeCut(_) | Artifact::EdgeCutEdge(_) | Artifact::Helix(_) => None,
600        }
601    }
602
603    /// Merge the new artifact into self.  If it can't because it's a different
604    /// type, return the new artifact which should be used as a replacement.
605    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
606        match self {
607            Artifact::CompositeSolid(a) => a.merge(new),
608            Artifact::Plane(a) => a.merge(new),
609            Artifact::Path(a) => a.merge(new),
610            Artifact::Segment(a) => a.merge(new),
611            Artifact::Solid2d(_) => Some(new),
612            Artifact::PrimitiveFace(_) => Some(new),
613            Artifact::PrimitiveEdge(_) => Some(new),
614            Artifact::StartSketchOnFace { .. } => Some(new),
615            Artifact::StartSketchOnPlane { .. } => Some(new),
616            Artifact::SketchBlock { .. } => Some(new),
617            Artifact::SketchBlockConstraint { .. } => Some(new),
618            Artifact::PlaneOfFace { .. } => Some(new),
619            Artifact::Sweep(a) => a.merge(new),
620            Artifact::Wall(a) => a.merge(new),
621            Artifact::Cap(a) => a.merge(new),
622            Artifact::SweepEdge(_) => Some(new),
623            Artifact::EdgeCut(a) => a.merge(new),
624            Artifact::EdgeCutEdge(_) => Some(new),
625            Artifact::Helix(a) => a.merge(new),
626        }
627    }
628}
629
630impl CompositeSolid {
631    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
632        let Artifact::CompositeSolid(new) = new else {
633            return Some(new);
634        };
635        merge_ids(&mut self.solid_ids, new.solid_ids);
636        merge_ids(&mut self.tool_ids, new.tool_ids);
637        merge_opt_id(&mut self.composite_solid_id, new.composite_solid_id);
638        self.output_index = new.output_index;
639        self.consumed = new.consumed;
640
641        None
642    }
643}
644
645impl Plane {
646    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
647        let Artifact::Plane(new) = new else {
648            return Some(new);
649        };
650        merge_ids(&mut self.path_ids, new.path_ids);
651
652        None
653    }
654}
655
656impl Path {
657    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
658        let Artifact::Path(new) = new else {
659            return Some(new);
660        };
661        merge_opt_id(&mut self.sweep_id, new.sweep_id);
662        merge_opt_id(&mut self.trajectory_sweep_id, new.trajectory_sweep_id);
663        merge_ids(&mut self.seg_ids, new.seg_ids);
664        merge_opt_id(&mut self.solid2d_id, new.solid2d_id);
665        merge_opt_id(&mut self.composite_solid_id, new.composite_solid_id);
666        merge_opt_id(&mut self.sketch_block_id, new.sketch_block_id);
667        merge_opt_id(&mut self.origin_path_id, new.origin_path_id);
668        merge_opt_id(&mut self.inner_path_id, new.inner_path_id);
669        merge_opt_id(&mut self.outer_path_id, new.outer_path_id);
670        self.consumed = new.consumed;
671
672        None
673    }
674}
675
676impl Segment {
677    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
678        let Artifact::Segment(new) = new else {
679            return Some(new);
680        };
681        merge_opt_id(&mut self.original_seg_id, new.original_seg_id);
682        merge_opt_id(&mut self.surface_id, new.surface_id);
683        merge_ids(&mut self.edge_ids, new.edge_ids);
684        merge_opt_id(&mut self.edge_cut_id, new.edge_cut_id);
685        merge_ids(&mut self.common_surface_ids, new.common_surface_ids);
686
687        None
688    }
689}
690
691impl Sweep {
692    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
693        let Artifact::Sweep(new) = new else {
694            return Some(new);
695        };
696        merge_ids(&mut self.surface_ids, new.surface_ids);
697        merge_ids(&mut self.edge_ids, new.edge_ids);
698        merge_opt_id(&mut self.trajectory_id, new.trajectory_id);
699        self.consumed = new.consumed;
700
701        None
702    }
703}
704
705impl Wall {
706    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
707        let Artifact::Wall(new) = new else {
708            return Some(new);
709        };
710        merge_ids(&mut self.edge_cut_edge_ids, new.edge_cut_edge_ids);
711        merge_ids(&mut self.path_ids, new.path_ids);
712
713        None
714    }
715}
716
717impl Cap {
718    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
719        let Artifact::Cap(new) = new else {
720            return Some(new);
721        };
722        merge_ids(&mut self.edge_cut_edge_ids, new.edge_cut_edge_ids);
723        merge_ids(&mut self.path_ids, new.path_ids);
724
725        None
726    }
727}
728
729impl EdgeCut {
730    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
731        let Artifact::EdgeCut(new) = new else {
732            return Some(new);
733        };
734        merge_opt_id(&mut self.surface_id, new.surface_id);
735        merge_ids(&mut self.edge_ids, new.edge_ids);
736
737        None
738    }
739}
740
741impl Helix {
742    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
743        let Artifact::Helix(new) = new else {
744            return Some(new);
745        };
746        merge_opt_id(&mut self.axis_id, new.axis_id);
747        merge_opt_id(&mut self.trajectory_sweep_id, new.trajectory_sweep_id);
748        self.consumed = new.consumed;
749
750        None
751    }
752}
753
754#[derive(Debug, Clone, Default, PartialEq, Serialize, ts_rs::TS)]
755#[ts(export_to = "Artifact.ts")]
756#[serde(rename_all = "camelCase")]
757pub struct ArtifactGraph {
758    map: IndexMap<ArtifactId, Artifact>,
759    pub(super) item_count: usize,
760}
761
762impl ArtifactGraph {
763    pub fn get(&self, id: &ArtifactId) -> Option<&Artifact> {
764        self.map.get(id)
765    }
766
767    pub fn len(&self) -> usize {
768        self.map.len()
769    }
770
771    pub fn is_empty(&self) -> bool {
772        self.map.is_empty()
773    }
774
775    #[cfg(test)]
776    pub(crate) fn iter(&self) -> impl Iterator<Item = (&ArtifactId, &Artifact)> {
777        self.map.iter()
778    }
779
780    pub fn values(&self) -> impl Iterator<Item = &Artifact> {
781        self.map.values()
782    }
783
784    pub fn clear(&mut self) {
785        self.map.clear();
786        self.item_count = 0;
787    }
788
789    /// Consume the artifact graph and return the map of artifacts.
790    fn into_map(self) -> IndexMap<ArtifactId, Artifact> {
791        self.map
792    }
793}
794
795#[derive(Debug, Clone)]
796struct ImportCodeRef {
797    node_path: NodePath,
798    range: SourceRange,
799}
800
801fn import_statement_code_refs(
802    ast: &Node<Program>,
803    module_infos: &ModuleInfoMap,
804    programs: &crate::execution::ProgramLookup,
805    cached_body_items: usize,
806) -> FnvHashMap<ModuleId, ImportCodeRef> {
807    let mut code_refs = FnvHashMap::default();
808    for body_item in &ast.body {
809        let BodyItem::ImportStatement(import_stmt) = body_item else {
810            continue;
811        };
812        if !matches!(import_stmt.selector, ImportSelector::None { .. }) {
813            continue;
814        }
815        let Some(module_id) = module_id_for_import_path(module_infos, &import_stmt.path) else {
816            continue;
817        };
818        let range = SourceRange::from(import_stmt);
819        let node_path = NodePath::from_range(programs, cached_body_items, range).unwrap_or_default();
820        code_refs.entry(module_id).or_insert(ImportCodeRef { node_path, range });
821    }
822    code_refs
823}
824
825fn module_id_for_import_path(module_infos: &ModuleInfoMap, import_path: &ImportPath) -> Option<ModuleId> {
826    let import_path = match import_path {
827        ImportPath::Kcl { filename } => filename,
828        ImportPath::Foreign { path } => path,
829        ImportPath::Std { .. } => return None,
830    };
831
832    module_infos.iter().find_map(|(module_id, module_info)| {
833        if let ModulePath::Local {
834            original_import_path: Some(original_import_path),
835            ..
836        } = &module_info.path
837            && original_import_path == import_path
838        {
839            return Some(*module_id);
840        }
841        None
842    })
843}
844
845fn code_ref_for_range(
846    programs: &crate::execution::ProgramLookup,
847    cached_body_items: usize,
848    range: SourceRange,
849    import_code_refs: &FnvHashMap<ModuleId, ImportCodeRef>,
850) -> (SourceRange, NodePath) {
851    if let Some(code_ref) = import_code_refs.get(&range.module_id()) {
852        return (code_ref.range, code_ref.node_path.clone());
853    }
854
855    (
856        range,
857        NodePath::from_range(programs, cached_body_items, range).unwrap_or_default(),
858    )
859}
860
861/// Build the artifact graph from the artifact commands and the responses.  The
862/// initial graph is the graph cached from a previous execution.  NodePaths of
863/// `exec_artifacts` are filled in from the AST.
864pub(super) fn build_artifact_graph(
865    artifact_commands: &[ArtifactCommand],
866    responses: &IndexMap<Uuid, WebSocketResponse>,
867    ast: &Node<Program>,
868    exec_artifacts: &mut IndexMap<ArtifactId, Artifact>,
869    initial_graph: ArtifactGraph,
870    programs: &crate::execution::ProgramLookup,
871    module_infos: &ModuleInfoMap,
872) -> Result<ArtifactGraph, KclError> {
873    let item_count = initial_graph.item_count;
874    let mut map = initial_graph.into_map();
875
876    let mut path_to_plane_id_map = FnvHashMap::default();
877    let mut current_plane_id = None;
878    let import_code_refs = import_statement_code_refs(ast, module_infos, programs, item_count);
879
880    // Fill in NodePaths for artifacts that were added directly to the map
881    // during execution.
882    for exec_artifact in exec_artifacts.values_mut() {
883        // Note: We only have access to the new AST. So if these artifacts
884        // somehow came from cached AST, this won't fill in anything.
885        fill_in_node_paths(exec_artifact, programs, item_count, &import_code_refs);
886    }
887
888    for artifact_command in artifact_commands {
889        if let ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) = artifact_command.command {
890            current_plane_id = Some(entity_id);
891        }
892        // If we get a start path command, we need to set the plane ID to the
893        // current plane ID.
894        // THIS IS THE ONLY THING WE CAN ASSUME IS ALWAYS SEQUENTIAL SINCE ITS PART OF THE
895        // SAME ATOMIC COMMANDS BATCHING.
896        if let ModelingCmd::StartPath(_) = artifact_command.command
897            && let Some(plane_id) = current_plane_id
898        {
899            path_to_plane_id_map.insert(artifact_command.cmd_id, plane_id);
900        }
901        if let ModelingCmd::SketchModeDisable(_) = artifact_command.command {
902            current_plane_id = None;
903        }
904
905        let flattened_responses = flatten_modeling_command_responses(responses);
906        let artifact_updates = artifacts_to_update(
907            &map,
908            artifact_command,
909            &flattened_responses,
910            &path_to_plane_id_map,
911            programs,
912            item_count,
913            exec_artifacts,
914            &import_code_refs,
915        )?;
916        for artifact in artifact_updates {
917            // Merge with existing artifacts.
918            merge_artifact_into_map(&mut map, artifact);
919        }
920    }
921
922    for exec_artifact in exec_artifacts.values() {
923        merge_artifact_into_map(&mut map, exec_artifact.clone());
924    }
925
926    Ok(ArtifactGraph {
927        map,
928        item_count: item_count + ast.body.len(),
929    })
930}
931
932/// These may have been created with placeholder `CodeRef`s because we didn't
933/// have the entire AST available. Now we fill them in.
934fn fill_in_node_paths(
935    artifact: &mut Artifact,
936    programs: &crate::execution::ProgramLookup,
937    cached_body_items: usize,
938    import_code_refs: &FnvHashMap<ModuleId, ImportCodeRef>,
939) {
940    match artifact {
941        Artifact::StartSketchOnFace(face) if face.code_ref.node_path.is_empty() => {
942            let (range, node_path) =
943                code_ref_for_range(programs, cached_body_items, face.code_ref.range, import_code_refs);
944            face.code_ref.range = range;
945            face.code_ref.node_path = node_path;
946        }
947        Artifact::StartSketchOnPlane(plane) if plane.code_ref.node_path.is_empty() => {
948            let (range, node_path) =
949                code_ref_for_range(programs, cached_body_items, plane.code_ref.range, import_code_refs);
950            plane.code_ref.range = range;
951            plane.code_ref.node_path = node_path;
952        }
953        Artifact::SketchBlock(block) if block.code_ref.node_path.is_empty() => {
954            let (range, node_path) =
955                code_ref_for_range(programs, cached_body_items, block.code_ref.range, import_code_refs);
956            block.code_ref.range = range;
957            block.code_ref.node_path = node_path;
958        }
959        Artifact::SketchBlockConstraint(constraint) if constraint.code_ref.node_path.is_empty() => {
960            constraint.code_ref.node_path =
961                NodePath::from_range(programs, cached_body_items, constraint.code_ref.range).unwrap_or_default();
962        }
963        _ => {}
964    }
965}
966
967/// Flatten the responses into a map of command IDs to modeling command
968/// responses.  The raw responses from the engine contain batches.
969fn flatten_modeling_command_responses(
970    responses: &IndexMap<Uuid, WebSocketResponse>,
971) -> FnvHashMap<Uuid, OkModelingCmdResponse> {
972    let mut map = FnvHashMap::default();
973    for (cmd_id, ws_response) in responses {
974        let WebSocketResponse::Success(response) = ws_response else {
975            // Response not successful.
976            continue;
977        };
978        match &response.resp {
979            OkWebSocketResponseData::Modeling { modeling_response } => {
980                map.insert(*cmd_id, modeling_response.clone());
981            }
982            OkWebSocketResponseData::ModelingBatch { responses } =>
983            {
984                #[expect(
985                    clippy::iter_over_hash_type,
986                    reason = "Since we're moving entries to another unordered map, it's fine that the order is undefined"
987                )]
988                for (cmd_id, batch_response) in responses {
989                    if let BatchResponse::Success {
990                        response: modeling_response,
991                    } = batch_response
992                    {
993                        map.insert(*cmd_id.as_ref(), modeling_response.clone());
994                    }
995                }
996            }
997            OkWebSocketResponseData::IceServerInfo { .. }
998            | OkWebSocketResponseData::TrickleIce { .. }
999            | OkWebSocketResponseData::SdpAnswer { .. }
1000            | OkWebSocketResponseData::Export { .. }
1001            | OkWebSocketResponseData::MetricsRequest { .. }
1002            | OkWebSocketResponseData::ModelingSessionData { .. }
1003            | OkWebSocketResponseData::Debug { .. }
1004            | OkWebSocketResponseData::Pong { .. } => {}
1005            _other => {}
1006        }
1007    }
1008
1009    map
1010}
1011
1012fn merge_artifact_into_map(map: &mut IndexMap<ArtifactId, Artifact>, new_artifact: Artifact) {
1013    fn is_primitive_artifact(artifact: &Artifact) -> bool {
1014        matches!(artifact, Artifact::PrimitiveFace(_) | Artifact::PrimitiveEdge(_))
1015    }
1016
1017    let id = new_artifact.id();
1018    let Some(old_artifact) = map.get_mut(&id) else {
1019        // No old artifact exists.  Insert the new one.
1020        map.insert(id, new_artifact);
1021        return;
1022    };
1023
1024    // Primitive lookups (faceId/edgeId) may resolve to an ID that already has
1025    // a richer artifact (for example Segment/Cap/Wall). Keep the existing node
1026    // to avoid erasing structural graph links.
1027    if is_primitive_artifact(&new_artifact) && !is_primitive_artifact(old_artifact) {
1028        return;
1029    }
1030
1031    if let Some(replacement) = old_artifact.merge(new_artifact) {
1032        *old_artifact = replacement;
1033    }
1034}
1035
1036/// Merge the new IDs into the base vector, avoiding duplicates.  This is O(nm)
1037/// runtime.  Rationale is that most of the ID collections in the artifact graph
1038/// are pretty small, but we may want to change this in the future.
1039fn merge_ids(base: &mut Vec<ArtifactId>, new: Vec<ArtifactId>) {
1040    let original_len = base.len();
1041    for id in new {
1042        // Don't bother inspecting new items that we just pushed.
1043        let original_base = &base[..original_len];
1044        if !original_base.contains(&id) {
1045            base.push(id);
1046        }
1047    }
1048}
1049
1050/// Merge optional Artifact ID
1051fn merge_opt_id(base: &mut Option<ArtifactId>, new: Option<ArtifactId>) {
1052    // Always use the new one, even if it clears it.
1053    *base = new;
1054}
1055
1056#[allow(clippy::too_many_arguments)]
1057fn artifacts_to_update(
1058    artifacts: &IndexMap<ArtifactId, Artifact>,
1059    artifact_command: &ArtifactCommand,
1060    responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
1061    path_to_plane_id_map: &FnvHashMap<Uuid, Uuid>,
1062    programs: &crate::execution::ProgramLookup,
1063    cached_body_items: usize,
1064    exec_artifacts: &IndexMap<ArtifactId, Artifact>,
1065    import_code_refs: &FnvHashMap<ModuleId, ImportCodeRef>,
1066) -> Result<Vec<Artifact>, KclError> {
1067    let uuid = artifact_command.cmd_id;
1068    let response = responses.get(&uuid);
1069
1070    // TODO: Build path-to-node from artifact_command source range.  Right now,
1071    // we're serializing an empty array, and the TS wrapper fills it in with the
1072    // correct value based on NodePath.
1073    let path_to_node = Vec::new();
1074    let range = artifact_command.range;
1075    let (code_ref_range, node_path) = code_ref_for_range(programs, cached_body_items, range, import_code_refs);
1076    let code_ref = CodeRef {
1077        range: code_ref_range,
1078        node_path,
1079        path_to_node,
1080    };
1081
1082    let id = ArtifactId::new(uuid);
1083    let cmd = &artifact_command.command;
1084
1085    match cmd {
1086        ModelingCmd::MakePlane(_) => {
1087            if range.is_synthetic() {
1088                return Ok(Vec::new());
1089            }
1090            // If we're calling `make_plane` and the code range doesn't end at
1091            // `0` it's not a default plane, but a custom one from the
1092            // offsetPlane standard library function.
1093            return Ok(vec![Artifact::Plane(Plane {
1094                id,
1095                path_ids: Vec::new(),
1096                code_ref,
1097            })]);
1098        }
1099        ModelingCmd::FaceIsPlanar(FaceIsPlanar { object_id, .. }) => {
1100            return Ok(vec![Artifact::PlaneOfFace(PlaneOfFace {
1101                id,
1102                face_id: object_id.into(),
1103                code_ref,
1104            })]);
1105        }
1106        ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) => {
1107            let existing_plane = artifacts.get(&ArtifactId::new(*entity_id));
1108            match existing_plane {
1109                Some(Artifact::Wall(wall)) => {
1110                    return Ok(vec![Artifact::Wall(Wall {
1111                        id: entity_id.into(),
1112                        seg_id: wall.seg_id,
1113                        edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
1114                        sweep_id: wall.sweep_id,
1115                        path_ids: wall.path_ids.clone(),
1116                        face_code_ref: wall.face_code_ref.clone(),
1117                        cmd_id: artifact_command.cmd_id,
1118                    })]);
1119                }
1120                Some(Artifact::Cap(cap)) => {
1121                    return Ok(vec![Artifact::Cap(Cap {
1122                        id: entity_id.into(),
1123                        sub_type: cap.sub_type,
1124                        edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
1125                        sweep_id: cap.sweep_id,
1126                        path_ids: cap.path_ids.clone(),
1127                        face_code_ref: cap.face_code_ref.clone(),
1128                        cmd_id: artifact_command.cmd_id,
1129                    })]);
1130                }
1131                Some(_) | None => {
1132                    let path_ids = match existing_plane {
1133                        Some(Artifact::Plane(Plane { path_ids, .. })) => path_ids.clone(),
1134                        _ => Vec::new(),
1135                    };
1136                    // Create an entirely new plane
1137                    return Ok(vec![Artifact::Plane(Plane {
1138                        id: entity_id.into(),
1139                        path_ids,
1140                        code_ref,
1141                    })]);
1142                }
1143            }
1144        }
1145        ModelingCmd::StartPath(_) => {
1146            let mut return_arr = Vec::new();
1147            let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
1148                KclError::new_internal(KclErrorDetails::new(
1149                    format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"),
1150                    vec![range],
1151                ))
1152            })?;
1153            let sketch_block_id = exec_artifacts
1154                .values()
1155                .find(|a| {
1156                    if let Artifact::SketchBlock(s) = a {
1157                        if let Some(path_id) = s.path_id {
1158                            path_id == id
1159                        } else {
1160                            false
1161                        }
1162                    } else {
1163                        false
1164                    }
1165                })
1166                .map(|a| a.id());
1167            return_arr.push(Artifact::Path(Path {
1168                id,
1169                sub_type: PathSubType::Sketch,
1170                plane_id: (*current_plane_id).into(),
1171                seg_ids: Vec::new(),
1172                sweep_id: None,
1173                trajectory_sweep_id: None,
1174                solid2d_id: None,
1175                code_ref,
1176                composite_solid_id: None,
1177                sketch_block_id,
1178                origin_path_id: None,
1179                inner_path_id: None,
1180                outer_path_id: None,
1181                consumed: false,
1182            }));
1183            let plane = artifacts.get(&ArtifactId::new(*current_plane_id));
1184            if let Some(Artifact::Plane(plane)) = plane {
1185                let plane_code_ref = plane.code_ref.clone();
1186                return_arr.push(Artifact::Plane(Plane {
1187                    id: (*current_plane_id).into(),
1188                    path_ids: vec![id],
1189                    code_ref: plane_code_ref,
1190                }));
1191            }
1192            if let Some(Artifact::Wall(wall)) = plane {
1193                return_arr.push(Artifact::Wall(Wall {
1194                    id: (*current_plane_id).into(),
1195                    seg_id: wall.seg_id,
1196                    edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
1197                    sweep_id: wall.sweep_id,
1198                    path_ids: vec![id],
1199                    face_code_ref: wall.face_code_ref.clone(),
1200                    cmd_id: artifact_command.cmd_id,
1201                }));
1202            }
1203            if let Some(Artifact::Cap(cap)) = plane {
1204                return_arr.push(Artifact::Cap(Cap {
1205                    id: (*current_plane_id).into(),
1206                    sub_type: cap.sub_type,
1207                    edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
1208                    sweep_id: cap.sweep_id,
1209                    path_ids: vec![id],
1210                    face_code_ref: cap.face_code_ref.clone(),
1211                    cmd_id: artifact_command.cmd_id,
1212                }));
1213            }
1214            return Ok(return_arr);
1215        }
1216        ModelingCmd::ClosePath(_) | ModelingCmd::ExtendPath(_) => {
1217            let path_id = ArtifactId::new(match cmd {
1218                ModelingCmd::ClosePath(c) => c.path_id,
1219                ModelingCmd::ExtendPath(e) => e.path.into(),
1220                _ => internal_error!(
1221                    range,
1222                    "Close or extend path command variant not handled: id={id:?}, cmd={cmd:?}"
1223                ),
1224            });
1225            let mut return_arr = Vec::new();
1226            return_arr.push(Artifact::Segment(Segment {
1227                id,
1228                path_id,
1229                original_seg_id: None,
1230                surface_id: None,
1231                edge_ids: Vec::new(),
1232                edge_cut_id: None,
1233                code_ref,
1234                common_surface_ids: Vec::new(),
1235            }));
1236            let path = artifacts.get(&path_id);
1237            if let Some(Artifact::Path(path)) = path {
1238                let mut new_path = path.clone();
1239                new_path.seg_ids = vec![id];
1240                return_arr.push(Artifact::Path(new_path));
1241            }
1242            if let Some(OkModelingCmdResponse::ClosePath(close_path)) = response {
1243                return_arr.push(Artifact::Solid2d(Solid2d {
1244                    id: close_path.face_id.into(),
1245                    path_id,
1246                }));
1247                if let Some(Artifact::Path(path)) = path {
1248                    let mut new_path = path.clone();
1249                    new_path.solid2d_id = Some(close_path.face_id.into());
1250                    return_arr.push(Artifact::Path(new_path));
1251                }
1252            }
1253            return Ok(return_arr);
1254        }
1255        ModelingCmd::CreateRegion(kcmc::CreateRegion {
1256            object_id: origin_path_id,
1257            ..
1258        })
1259        | ModelingCmd::CreateRegionFromQueryPoint(kcmc::CreateRegionFromQueryPoint {
1260            object_id: origin_path_id,
1261            ..
1262        }) => {
1263            let mut return_arr = Vec::new();
1264            let origin_path = artifacts.get(&ArtifactId::new(*origin_path_id));
1265            let Some(Artifact::Path(path)) = origin_path else {
1266                internal_error!(
1267                    range,
1268                    "Expected to find an existing path for the origin path of CreateRegion or CreateRegionFromQueryPoint command, but found none: origin_path={origin_path:?}, cmd={cmd:?}"
1269                );
1270            };
1271            // Create the path representing the region.
1272            return_arr.push(Artifact::Path(Path {
1273                id,
1274                sub_type: PathSubType::Region,
1275                plane_id: path.plane_id,
1276                seg_ids: Vec::new(),
1277                consumed: false,
1278                sweep_id: None,
1279                trajectory_sweep_id: None,
1280                solid2d_id: None,
1281                code_ref: code_ref.clone(),
1282                composite_solid_id: None,
1283                sketch_block_id: None,
1284                origin_path_id: Some(ArtifactId::new(*origin_path_id)),
1285                inner_path_id: None,
1286                outer_path_id: None,
1287            }));
1288            // If we have a response, we can also create the segments in the
1289            // region.
1290            let Some(
1291                OkModelingCmdResponse::CreateRegion(kcmc::output::CreateRegion { region_mapping, .. })
1292                | OkModelingCmdResponse::CreateRegionFromQueryPoint(kcmc::output::CreateRegionFromQueryPoint {
1293                    region_mapping,
1294                    ..
1295                }),
1296            ) = response
1297            else {
1298                return Ok(return_arr);
1299            };
1300            // Each key is a segment in the region. The value is the segment in
1301            // the original path. Build the reverse mapping.
1302            let original_segment_ids = path.seg_ids.iter().map(|p| p.0).collect::<Vec<_>>();
1303            let reverse = build_reverse_region_mapping(region_mapping, &original_segment_ids);
1304            for (original_segment_id, region_segment_ids) in reverse.iter() {
1305                for segment_id in region_segment_ids {
1306                    return_arr.push(Artifact::Segment(Segment {
1307                        id: ArtifactId::new(*segment_id),
1308                        path_id: id,
1309                        original_seg_id: Some(ArtifactId::new(*original_segment_id)),
1310                        surface_id: None,
1311                        edge_ids: Vec::new(),
1312                        edge_cut_id: None,
1313                        code_ref: code_ref.clone(),
1314                        common_surface_ids: Vec::new(),
1315                    }))
1316                }
1317            }
1318            return Ok(return_arr);
1319        }
1320        ModelingCmd::Solid3dGetFaceUuid(kcmc::Solid3dGetFaceUuid { object_id, .. }) => {
1321            let Some(OkModelingCmdResponse::Solid3dGetFaceUuid(face_uuid)) = response else {
1322                return Ok(Vec::new());
1323            };
1324
1325            return Ok(vec![Artifact::PrimitiveFace(PrimitiveFace {
1326                id: face_uuid.face_id.into(),
1327                solid_id: (*object_id).into(),
1328                code_ref,
1329            })]);
1330        }
1331        ModelingCmd::Solid3dGetEdgeUuid(kcmc::Solid3dGetEdgeUuid { object_id, .. }) => {
1332            let Some(OkModelingCmdResponse::Solid3dGetEdgeUuid(edge_uuid)) = response else {
1333                return Ok(Vec::new());
1334            };
1335
1336            return Ok(vec![Artifact::PrimitiveEdge(PrimitiveEdge {
1337                id: edge_uuid.edge_id.into(),
1338                solid_id: (*object_id).into(),
1339                code_ref,
1340            })]);
1341        }
1342        ModelingCmd::EntityMirror(kcmc::EntityMirror {
1343            ids: original_path_ids, ..
1344        })
1345        | ModelingCmd::EntityMirrorAcrossEdge(kcmc::EntityMirrorAcrossEdge {
1346            ids: original_path_ids, ..
1347        }) => {
1348            let face_edge_infos = match response {
1349                Some(OkModelingCmdResponse::EntityMirror(resp)) => &resp.entity_face_edge_ids,
1350                Some(OkModelingCmdResponse::EntityMirrorAcrossEdge(resp)) => &resp.entity_face_edge_ids,
1351                _ => internal_error!(
1352                    range,
1353                    "Mirror response variant not handled: id={id:?}, cmd={cmd:?}, response={response:?}"
1354                ),
1355            };
1356            if original_path_ids.len() != face_edge_infos.len() {
1357                internal_error!(
1358                    range,
1359                    "EntityMirror or EntityMirrorAcrossEdge response has different number face edge info than original mirrored paths: id={id:?}, cmd={cmd:?}, response={response:?}"
1360                );
1361            }
1362            let mut return_arr = Vec::new();
1363            for (face_edge_info, original_path_id) in face_edge_infos.iter().zip(original_path_ids) {
1364                let original_path_id = ArtifactId::new(*original_path_id);
1365                let path_id = ArtifactId::new(face_edge_info.object_id);
1366                // The path may be an existing path that was extended or a new
1367                // path.
1368                let mut path = if let Some(Artifact::Path(path)) = artifacts.get(&path_id) {
1369                    // Existing path.
1370                    path.clone()
1371                } else {
1372                    // It's a new path.  We need the original path to get some
1373                    // of its info.
1374                    let Some(Artifact::Path(original_path)) = artifacts.get(&original_path_id) else {
1375                        // We couldn't find the original path. This is a bug.
1376                        internal_error!(
1377                            range,
1378                            "Couldn't find original path for mirror2d: original_path_id={original_path_id:?}, cmd={cmd:?}"
1379                        );
1380                    };
1381                    Path {
1382                        id: path_id,
1383                        sub_type: original_path.sub_type,
1384                        plane_id: original_path.plane_id,
1385                        seg_ids: Vec::new(),
1386                        sweep_id: None,
1387                        trajectory_sweep_id: None,
1388                        solid2d_id: None,
1389                        code_ref: code_ref.clone(),
1390                        composite_solid_id: None,
1391                        sketch_block_id: None,
1392                        origin_path_id: original_path.origin_path_id,
1393                        inner_path_id: None,
1394                        outer_path_id: None,
1395                        consumed: false,
1396                    }
1397                };
1398
1399                face_edge_info.edges.iter().for_each(|edge_id| {
1400                    let edge_id = ArtifactId::new(*edge_id);
1401                    return_arr.push(Artifact::Segment(Segment {
1402                        id: edge_id,
1403                        path_id: path.id,
1404                        original_seg_id: None,
1405                        surface_id: None,
1406                        edge_ids: Vec::new(),
1407                        edge_cut_id: None,
1408                        code_ref: code_ref.clone(),
1409                        common_surface_ids: Vec::new(),
1410                    }));
1411                    // Add the edge ID to the path.
1412                    path.seg_ids.push(edge_id);
1413                });
1414
1415                return_arr.push(Artifact::Path(path));
1416            }
1417            return Ok(return_arr);
1418        }
1419        ModelingCmd::Extrude(kcmc::Extrude { target, .. })
1420        | ModelingCmd::TwistExtrude(kcmc::TwistExtrude { target, .. })
1421        | ModelingCmd::Revolve(kcmc::Revolve { target, .. })
1422        | ModelingCmd::RevolveAboutEdge(kcmc::RevolveAboutEdge { target, .. })
1423        | ModelingCmd::ExtrudeToReference(kcmc::ExtrudeToReference { target, .. }) => {
1424            // Determine the resulting method from the specific command, if provided
1425            let method = match cmd {
1426                ModelingCmd::Extrude(kcmc::Extrude { extrude_method, .. }) => *extrude_method,
1427                ModelingCmd::ExtrudeToReference(kcmc::ExtrudeToReference { extrude_method, .. }) => *extrude_method,
1428                // TwistExtrude and Sweep don't carry method in the command; treat as Merge
1429                ModelingCmd::TwistExtrude(_) | ModelingCmd::Sweep(_) => {
1430                    kittycad_modeling_cmds::shared::ExtrudeMethod::Merge
1431                }
1432                // Revolve variants behave like New bodies in std layer
1433                ModelingCmd::Revolve(_) | ModelingCmd::RevolveAboutEdge(_) => {
1434                    kittycad_modeling_cmds::shared::ExtrudeMethod::New
1435                }
1436                _ => kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
1437            };
1438            let sub_type = match cmd {
1439                ModelingCmd::Extrude(_) => SweepSubType::Extrusion,
1440                ModelingCmd::ExtrudeToReference(_) => SweepSubType::Extrusion,
1441                ModelingCmd::TwistExtrude(_) => SweepSubType::ExtrusionTwist,
1442                ModelingCmd::Revolve(_) => SweepSubType::Revolve,
1443                ModelingCmd::RevolveAboutEdge(_) => SweepSubType::RevolveAboutEdge,
1444                _ => internal_error!(range, "Sweep-like command variant not handled: id={id:?}, cmd={cmd:?}",),
1445            };
1446            let mut return_arr = Vec::new();
1447            let target = ArtifactId::from(target);
1448            return_arr.push(Artifact::Sweep(Sweep {
1449                id,
1450                sub_type,
1451                path_id: target,
1452                surface_ids: Vec::new(),
1453                edge_ids: Vec::new(),
1454                code_ref,
1455                trajectory_id: None,
1456                method,
1457                consumed: false,
1458            }));
1459            let path = artifacts.get(&target);
1460            if let Some(Artifact::Path(path)) = path {
1461                let mut new_path = path.clone();
1462                new_path.sweep_id = Some(id);
1463                new_path.consumed = true;
1464                return_arr.push(Artifact::Path(new_path));
1465                if let Some(inner_path_id) = path.inner_path_id
1466                    && let Some(inner_path_artifact) = artifacts.get(&inner_path_id)
1467                    && let Artifact::Path(mut inner_path_artifact) = inner_path_artifact.clone()
1468                {
1469                    inner_path_artifact.sweep_id = Some(id);
1470                    inner_path_artifact.consumed = true;
1471                    return_arr.push(Artifact::Path(inner_path_artifact))
1472                }
1473            }
1474            return Ok(return_arr);
1475        }
1476        ModelingCmd::Sweep(kcmc::Sweep { target, trajectory, .. }) => {
1477            // Determine the resulting method from the specific command, if provided
1478            let method = kittycad_modeling_cmds::shared::ExtrudeMethod::Merge;
1479            let sub_type = SweepSubType::Sweep;
1480            let mut return_arr = Vec::new();
1481            let target = ArtifactId::from(target);
1482            let trajectory = ArtifactId::from(trajectory);
1483            return_arr.push(Artifact::Sweep(Sweep {
1484                id,
1485                sub_type,
1486                path_id: target,
1487                surface_ids: Vec::new(),
1488                edge_ids: Vec::new(),
1489                code_ref,
1490                trajectory_id: Some(trajectory),
1491                method,
1492                consumed: false,
1493            }));
1494            let path = artifacts.get(&target);
1495            if let Some(Artifact::Path(path)) = path {
1496                let mut new_path = path.clone();
1497                new_path.sweep_id = Some(id);
1498                new_path.consumed = true;
1499                return_arr.push(Artifact::Path(new_path));
1500                if let Some(inner_path_id) = path.inner_path_id
1501                    && let Some(inner_path_artifact) = artifacts.get(&inner_path_id)
1502                    && let Artifact::Path(mut inner_path_artifact) = inner_path_artifact.clone()
1503                {
1504                    inner_path_artifact.sweep_id = Some(id);
1505                    inner_path_artifact.consumed = true;
1506                    return_arr.push(Artifact::Path(inner_path_artifact))
1507                }
1508            }
1509            if let Some(trajectory_artifact) = artifacts.get(&trajectory) {
1510                match trajectory_artifact {
1511                    Artifact::Path(path) => {
1512                        let mut new_path = path.clone();
1513                        new_path.trajectory_sweep_id = Some(id);
1514                        new_path.consumed = true;
1515                        return_arr.push(Artifact::Path(new_path));
1516                    }
1517                    Artifact::Helix(helix) => {
1518                        let mut new_helix = helix.clone();
1519                        new_helix.trajectory_sweep_id = Some(id);
1520                        new_helix.consumed = true;
1521                        return_arr.push(Artifact::Helix(new_helix));
1522                    }
1523                    _ => {}
1524                }
1525            };
1526            return Ok(return_arr);
1527        }
1528        ModelingCmd::SurfaceBlend(surface_blend_cmd) => {
1529            let surface_id_to_path_id = |surface_id: ArtifactId| -> Option<ArtifactId> {
1530                match artifacts.get(&surface_id) {
1531                    Some(Artifact::Path(path)) => Some(path.id),
1532                    Some(Artifact::Segment(segment)) => Some(segment.path_id),
1533                    Some(Artifact::Sweep(sweep)) => Some(sweep.path_id),
1534                    Some(Artifact::Wall(wall)) => artifacts.get(&wall.sweep_id).and_then(|artifact| match artifact {
1535                        Artifact::Sweep(sweep) => Some(sweep.path_id),
1536                        _ => None,
1537                    }),
1538                    Some(Artifact::Cap(cap)) => artifacts.get(&cap.sweep_id).and_then(|artifact| match artifact {
1539                        Artifact::Sweep(sweep) => Some(sweep.path_id),
1540                        _ => None,
1541                    }),
1542                    _ => None,
1543                }
1544            };
1545            let Some(first_surface_ref) = surface_blend_cmd.surfaces.first() else {
1546                internal_error!(range, "SurfaceBlend command has no surfaces: id={id:?}, cmd={cmd:?}");
1547            };
1548            let first_surface_id = ArtifactId::new(first_surface_ref.object_id);
1549            let path_id = surface_id_to_path_id(first_surface_id).unwrap_or(first_surface_id);
1550            let trajectory_id = surface_blend_cmd
1551                .surfaces
1552                .get(1)
1553                .map(|surface| ArtifactId::new(surface.object_id))
1554                .and_then(surface_id_to_path_id);
1555            let return_arr = vec![Artifact::Sweep(Sweep {
1556                id,
1557                sub_type: SweepSubType::Blend,
1558                path_id,
1559                surface_ids: Vec::new(),
1560                edge_ids: Vec::new(),
1561                code_ref,
1562                trajectory_id,
1563                method: kittycad_modeling_cmds::shared::ExtrudeMethod::New,
1564                consumed: false,
1565            })];
1566            return Ok(return_arr);
1567        }
1568        ModelingCmd::Loft(loft_cmd) => {
1569            let Some(OkModelingCmdResponse::Loft(_)) = response else {
1570                return Ok(Vec::new());
1571            };
1572            let mut return_arr = Vec::new();
1573            return_arr.push(Artifact::Sweep(Sweep {
1574                id,
1575                sub_type: SweepSubType::Loft,
1576                // TODO: Using the first one.  Make sure to revisit this
1577                // choice, don't think it matters for now.
1578                path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
1579                    KclError::new_internal(KclErrorDetails::new(
1580                        format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
1581                        vec![range],
1582                    ))
1583                })?),
1584                surface_ids: Vec::new(),
1585                edge_ids: Vec::new(),
1586                code_ref,
1587                trajectory_id: None,
1588                method: kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
1589                consumed: false,
1590            }));
1591            for section_id in &loft_cmd.section_ids {
1592                let path = artifacts.get(&ArtifactId::new(*section_id));
1593                if let Some(Artifact::Path(path)) = path {
1594                    let mut new_path = path.clone();
1595                    new_path.consumed = true;
1596                    new_path.sweep_id = Some(id);
1597                    return_arr.push(Artifact::Path(new_path));
1598                }
1599            }
1600            return Ok(return_arr);
1601        }
1602        ModelingCmd::Solid3dGetExtrusionFaceInfo(_) => {
1603            let Some(OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(face_info)) = response else {
1604                return Ok(Vec::new());
1605            };
1606            let mut return_arr = Vec::new();
1607            let mut last_path = None;
1608            for face in &face_info.faces {
1609                if face.cap != ExtrusionFaceCapType::None {
1610                    continue;
1611                }
1612                let Some(curve_id) = face.curve_id.map(ArtifactId::new) else {
1613                    continue;
1614                };
1615                let Some(face_id) = face.face_id.map(ArtifactId::new) else {
1616                    continue;
1617                };
1618                let Some(Artifact::Segment(seg)) = artifacts.get(&curve_id) else {
1619                    continue;
1620                };
1621                let Some(Artifact::Path(path)) = artifacts.get(&seg.path_id) else {
1622                    continue;
1623                };
1624                last_path = Some(path);
1625                let Some(path_sweep_id) = path.sweep_id else {
1626                    // If the path doesn't have a sweep ID, check if it's a
1627                    // hole.
1628                    if path.outer_path_id.is_some() {
1629                        continue; // hole not handled
1630                    }
1631                    return Err(KclError::new_internal(KclErrorDetails::new(
1632                        format!(
1633                            "Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none:\n{id:#?}\n{path:#?}"
1634                        ),
1635                        vec![range],
1636                    )));
1637                };
1638                let extra_artifact = exec_artifacts.values().find(|a| {
1639                    if let Artifact::StartSketchOnFace(s) = a {
1640                        s.face_id == face_id
1641                    } else if let Artifact::StartSketchOnPlane(s) = a {
1642                        s.plane_id == face_id
1643                    } else {
1644                        false
1645                    }
1646                });
1647                let sketch_on_face_code_ref = extra_artifact
1648                    .and_then(|a| match a {
1649                        Artifact::StartSketchOnFace(s) => Some(s.code_ref.clone()),
1650                        Artifact::StartSketchOnPlane(s) => Some(s.code_ref.clone()),
1651                        _ => None,
1652                    })
1653                    // TODO: If we didn't find it, it's probably a bug.
1654                    .unwrap_or_default();
1655
1656                return_arr.push(Artifact::Wall(Wall {
1657                    id: face_id,
1658                    seg_id: curve_id,
1659                    edge_cut_edge_ids: Vec::new(),
1660                    sweep_id: path_sweep_id,
1661                    path_ids: Vec::new(),
1662                    face_code_ref: sketch_on_face_code_ref,
1663                    cmd_id: artifact_command.cmd_id,
1664                }));
1665                let mut new_seg = seg.clone();
1666                new_seg.surface_id = Some(face_id);
1667                return_arr.push(Artifact::Segment(new_seg));
1668                if let Some(Artifact::Sweep(sweep)) = path.sweep_id.and_then(|id| artifacts.get(&id)) {
1669                    let mut new_sweep = sweep.clone();
1670                    new_sweep.surface_ids = vec![face_id];
1671                    return_arr.push(Artifact::Sweep(new_sweep));
1672                }
1673            }
1674            if let Some(path) = last_path {
1675                for face in &face_info.faces {
1676                    let sub_type = match face.cap {
1677                        ExtrusionFaceCapType::Top => CapSubType::End,
1678                        ExtrusionFaceCapType::Bottom => CapSubType::Start,
1679                        ExtrusionFaceCapType::None | ExtrusionFaceCapType::Both => continue,
1680                        _other => {
1681                            // Modeling API has added something we're not aware of.
1682                            continue;
1683                        }
1684                    };
1685                    let Some(face_id) = face.face_id.map(ArtifactId::new) else {
1686                        continue;
1687                    };
1688                    let Some(path_sweep_id) = path.sweep_id else {
1689                        // If the path doesn't have a sweep ID, check if it's a
1690                        // hole.
1691                        if path.outer_path_id.is_some() {
1692                            continue; // hole not handled
1693                        }
1694                        return Err(KclError::new_internal(KclErrorDetails::new(
1695                            format!(
1696                                "Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none:\n{id:#?}\n{path:#?}"
1697                            ),
1698                            vec![range],
1699                        )));
1700                    };
1701                    let extra_artifact = exec_artifacts.values().find(|a| {
1702                        if let Artifact::StartSketchOnFace(s) = a {
1703                            s.face_id == face_id
1704                        } else if let Artifact::StartSketchOnPlane(s) = a {
1705                            s.plane_id == face_id
1706                        } else {
1707                            false
1708                        }
1709                    });
1710                    let sketch_on_face_code_ref = extra_artifact
1711                        .and_then(|a| match a {
1712                            Artifact::StartSketchOnFace(s) => Some(s.code_ref.clone()),
1713                            Artifact::StartSketchOnPlane(s) => Some(s.code_ref.clone()),
1714                            _ => None,
1715                        })
1716                        // TODO: If we didn't find it, it's probably a bug.
1717                        .unwrap_or_default();
1718                    return_arr.push(Artifact::Cap(Cap {
1719                        id: face_id,
1720                        sub_type,
1721                        edge_cut_edge_ids: Vec::new(),
1722                        sweep_id: path_sweep_id,
1723                        path_ids: Vec::new(),
1724                        face_code_ref: sketch_on_face_code_ref,
1725                        cmd_id: artifact_command.cmd_id,
1726                    }));
1727                    let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {
1728                        continue;
1729                    };
1730                    let mut new_sweep = sweep.clone();
1731                    new_sweep.surface_ids = vec![face_id];
1732                    return_arr.push(Artifact::Sweep(new_sweep));
1733                }
1734            }
1735            return Ok(return_arr);
1736        }
1737        ModelingCmd::Solid3dGetAdjacencyInfo(kcmc::Solid3dGetAdjacencyInfo { .. }) => {
1738            let Some(OkModelingCmdResponse::Solid3dGetAdjacencyInfo(info)) = response else {
1739                return Ok(Vec::new());
1740            };
1741
1742            let mut return_arr = Vec::new();
1743            for (index, edge) in info.edges.iter().enumerate() {
1744                let Some(original_info) = &edge.original_info else {
1745                    continue;
1746                };
1747                let edge_id = ArtifactId::new(original_info.edge_id);
1748                let Some(artifact) = artifacts.get(&edge_id) else {
1749                    continue;
1750                };
1751                match artifact {
1752                    Artifact::Segment(segment) => {
1753                        let mut new_segment = segment.clone();
1754                        new_segment.common_surface_ids =
1755                            original_info.faces.iter().map(|face| ArtifactId::new(*face)).collect();
1756                        return_arr.push(Artifact::Segment(new_segment));
1757                    }
1758                    Artifact::SweepEdge(sweep_edge) => {
1759                        let mut new_sweep_edge = sweep_edge.clone();
1760                        new_sweep_edge.common_surface_ids =
1761                            original_info.faces.iter().map(|face| ArtifactId::new(*face)).collect();
1762                        return_arr.push(Artifact::SweepEdge(new_sweep_edge));
1763                    }
1764                    _ => {}
1765                };
1766
1767                let Some(Artifact::Segment(segment)) = artifacts.get(&edge_id) else {
1768                    continue;
1769                };
1770                let Some(surface_id) = segment.surface_id else {
1771                    continue;
1772                };
1773                let Some(Artifact::Wall(wall)) = artifacts.get(&surface_id) else {
1774                    continue;
1775                };
1776                let Some(Artifact::Sweep(sweep)) = artifacts.get(&wall.sweep_id) else {
1777                    continue;
1778                };
1779                let Some(Artifact::Path(_)) = artifacts.get(&sweep.path_id) else {
1780                    continue;
1781                };
1782
1783                if let Some(opposite_info) = &edge.opposite_info {
1784                    return_arr.push(Artifact::SweepEdge(SweepEdge {
1785                        id: opposite_info.edge_id.into(),
1786                        sub_type: SweepEdgeSubType::Opposite,
1787                        seg_id: edge_id,
1788                        cmd_id: artifact_command.cmd_id,
1789                        index,
1790                        sweep_id: sweep.id,
1791                        common_surface_ids: opposite_info.faces.iter().map(|face| ArtifactId::new(*face)).collect(),
1792                    }));
1793                    let mut new_segment = segment.clone();
1794                    new_segment.edge_ids = vec![opposite_info.edge_id.into()];
1795                    return_arr.push(Artifact::Segment(new_segment));
1796                    let mut new_sweep = sweep.clone();
1797                    new_sweep.edge_ids = vec![opposite_info.edge_id.into()];
1798                    return_arr.push(Artifact::Sweep(new_sweep));
1799                    let mut new_wall = wall.clone();
1800                    new_wall.edge_cut_edge_ids = vec![opposite_info.edge_id.into()];
1801                    return_arr.push(Artifact::Wall(new_wall));
1802                }
1803                if let Some(adjacent_info) = &edge.adjacent_info {
1804                    return_arr.push(Artifact::SweepEdge(SweepEdge {
1805                        id: adjacent_info.edge_id.into(),
1806                        sub_type: SweepEdgeSubType::Adjacent,
1807                        seg_id: edge_id,
1808                        cmd_id: artifact_command.cmd_id,
1809                        index,
1810                        sweep_id: sweep.id,
1811                        common_surface_ids: adjacent_info.faces.iter().map(|face| ArtifactId::new(*face)).collect(),
1812                    }));
1813                    let mut new_segment = segment.clone();
1814                    new_segment.edge_ids = vec![adjacent_info.edge_id.into()];
1815                    return_arr.push(Artifact::Segment(new_segment));
1816                    let mut new_sweep = sweep.clone();
1817                    new_sweep.edge_ids = vec![adjacent_info.edge_id.into()];
1818                    return_arr.push(Artifact::Sweep(new_sweep));
1819                    let mut new_wall = wall.clone();
1820                    new_wall.edge_cut_edge_ids = vec![adjacent_info.edge_id.into()];
1821                    return_arr.push(Artifact::Wall(new_wall));
1822                }
1823            }
1824            return Ok(return_arr);
1825        }
1826        ModelingCmd::Solid3dMultiJoin(cmd) => {
1827            let mut return_arr = Vec::new();
1828            return_arr.push(Artifact::CompositeSolid(CompositeSolid {
1829                id,
1830                consumed: false,
1831                sub_type: CompositeSolidSubType::Union,
1832                output_index: None,
1833                solid_ids: cmd.object_ids.iter().map(|id| id.into()).collect(),
1834                tool_ids: vec![],
1835                code_ref,
1836                composite_solid_id: None,
1837            }));
1838
1839            let solid_ids = cmd.object_ids.iter().copied().map(ArtifactId::new).collect::<Vec<_>>();
1840
1841            for input_id in &solid_ids {
1842                if let Some(artifact) = artifacts.get(input_id)
1843                    && let Artifact::CompositeSolid(comp) = artifact
1844                {
1845                    let mut new_comp = comp.clone();
1846                    new_comp.composite_solid_id = Some(id);
1847                    new_comp.consumed = true;
1848                    return_arr.push(Artifact::CompositeSolid(new_comp));
1849                }
1850            }
1851            return Ok(return_arr);
1852        }
1853        ModelingCmd::Solid3dFilletEdge(cmd) => {
1854            let mut return_arr = Vec::new();
1855            let edge_id = if let Some(edge_id) = cmd.edge_id {
1856                ArtifactId::new(edge_id)
1857            } else {
1858                let Some(edge_id) = cmd.edge_ids.first() else {
1859                    internal_error!(
1860                        range,
1861                        "Solid3dFilletEdge command has no edge ID: id={id:?}, cmd={cmd:?}"
1862                    );
1863                };
1864                edge_id.into()
1865            };
1866            return_arr.push(Artifact::EdgeCut(EdgeCut {
1867                id,
1868                sub_type: cmd.cut_type.into(),
1869                consumed_edge_id: edge_id,
1870                edge_ids: Vec::new(),
1871                surface_id: None,
1872                code_ref,
1873            }));
1874            let consumed_edge = artifacts.get(&edge_id);
1875            if let Some(Artifact::Segment(consumed_edge)) = consumed_edge {
1876                let mut new_segment = consumed_edge.clone();
1877                new_segment.edge_cut_id = Some(id);
1878                return_arr.push(Artifact::Segment(new_segment));
1879            } else {
1880                // TODO: Handle other types like SweepEdge.
1881            }
1882            return Ok(return_arr);
1883        }
1884        ModelingCmd::Solid3dCutEdges(cmd) => {
1885            let mut return_arr = Vec::new();
1886            let edge_id = if let Some(edge_id) = cmd.edge_ids.first() {
1887                edge_id.into()
1888            } else {
1889                internal_error!(range, "Solid3dCutEdges command has no edge ID: id={id:?}, cmd={cmd:?}");
1890            };
1891            return_arr.push(Artifact::EdgeCut(EdgeCut {
1892                id,
1893                sub_type: cmd.cut_type.into(),
1894                consumed_edge_id: edge_id,
1895                edge_ids: Vec::new(),
1896                surface_id: None,
1897                code_ref,
1898            }));
1899            let consumed_edge = artifacts.get(&edge_id);
1900            if let Some(Artifact::Segment(consumed_edge)) = consumed_edge {
1901                let mut new_segment = consumed_edge.clone();
1902                new_segment.edge_cut_id = Some(id);
1903                return_arr.push(Artifact::Segment(new_segment));
1904            } else {
1905                // TODO: Handle other types like SweepEdge.
1906            }
1907            return Ok(return_arr);
1908        }
1909        ModelingCmd::EntityMakeHelix(cmd) => {
1910            let cylinder_id = ArtifactId::new(cmd.cylinder_id);
1911            let return_arr = vec![Artifact::Helix(Helix {
1912                id,
1913                axis_id: Some(cylinder_id),
1914                code_ref,
1915                trajectory_sweep_id: None,
1916                consumed: false,
1917            })];
1918            return Ok(return_arr);
1919        }
1920        ModelingCmd::EntityMakeHelixFromParams(_) => {
1921            let return_arr = vec![Artifact::Helix(Helix {
1922                id,
1923                axis_id: None,
1924                code_ref,
1925                trajectory_sweep_id: None,
1926                consumed: false,
1927            })];
1928            return Ok(return_arr);
1929        }
1930        ModelingCmd::EntityMakeHelixFromEdge(helix) => {
1931            let return_arr = vec![Artifact::Helix(Helix {
1932                id,
1933                axis_id: helix.edge_id.map(ArtifactId::new),
1934                code_ref,
1935                trajectory_sweep_id: None,
1936                consumed: false,
1937            })];
1938            // We could add the reverse graph edge connecting from the edge to
1939            // the helix here, but it's not useful right now.
1940            return Ok(return_arr);
1941        }
1942        ModelingCmd::Solid2dAddHole(solid2d_add_hole) => {
1943            let mut return_arr = Vec::new();
1944            // Add the hole to the outer.
1945            let outer_path = artifacts.get(&ArtifactId::new(solid2d_add_hole.object_id));
1946            if let Some(Artifact::Path(path)) = outer_path {
1947                let mut new_path = path.clone();
1948                new_path.inner_path_id = Some(ArtifactId::new(solid2d_add_hole.hole_id));
1949                return_arr.push(Artifact::Path(new_path));
1950            }
1951            // Add the outer to the hole.
1952            let inner_solid2d = artifacts.get(&ArtifactId::new(solid2d_add_hole.hole_id));
1953            if let Some(Artifact::Path(path)) = inner_solid2d {
1954                let mut new_path = path.clone();
1955                new_path.consumed = true;
1956                new_path.outer_path_id = Some(ArtifactId::new(solid2d_add_hole.object_id));
1957                return_arr.push(Artifact::Path(new_path));
1958            }
1959            return Ok(return_arr);
1960        }
1961        ModelingCmd::BooleanIntersection(_) | ModelingCmd::BooleanSubtract(_) | ModelingCmd::BooleanUnion(_) => {
1962            let (sub_type, solid_ids, tool_ids) = match cmd {
1963                ModelingCmd::BooleanIntersection(intersection) => {
1964                    let solid_ids = intersection
1965                        .solid_ids
1966                        .iter()
1967                        .copied()
1968                        .map(ArtifactId::new)
1969                        .collect::<Vec<_>>();
1970                    (CompositeSolidSubType::Intersect, solid_ids, Vec::new())
1971                }
1972                ModelingCmd::BooleanSubtract(subtract) => {
1973                    let solid_ids = subtract
1974                        .target_ids
1975                        .iter()
1976                        .copied()
1977                        .map(ArtifactId::new)
1978                        .collect::<Vec<_>>();
1979                    let tool_ids = subtract
1980                        .tool_ids
1981                        .iter()
1982                        .copied()
1983                        .map(ArtifactId::new)
1984                        .collect::<Vec<_>>();
1985                    (CompositeSolidSubType::Subtract, solid_ids, tool_ids)
1986                }
1987                ModelingCmd::BooleanUnion(union) => {
1988                    let solid_ids = union.solid_ids.iter().copied().map(ArtifactId::new).collect::<Vec<_>>();
1989                    (CompositeSolidSubType::Union, solid_ids, Vec::new())
1990                }
1991                _ => internal_error!(
1992                    range,
1993                    "Boolean or composite command variant not handled: id={id:?}, cmd={cmd:?}"
1994                ),
1995            };
1996
1997            let mut new_solid_ids = vec![id];
1998
1999            // Make sure we don't ever create a duplicate ID since merge_ids
2000            // can't handle it.
2001            let not_cmd_id = move |solid_id: &ArtifactId| *solid_id != id;
2002
2003            match response {
2004                Some(OkModelingCmdResponse::BooleanIntersection(intersection)) => intersection
2005                    .extra_solid_ids
2006                    .iter()
2007                    .copied()
2008                    .map(ArtifactId::new)
2009                    .filter(not_cmd_id)
2010                    .for_each(|id| new_solid_ids.push(id)),
2011                Some(OkModelingCmdResponse::BooleanSubtract(subtract)) => subtract
2012                    .extra_solid_ids
2013                    .iter()
2014                    .copied()
2015                    .map(ArtifactId::new)
2016                    .filter(not_cmd_id)
2017                    .for_each(|id| new_solid_ids.push(id)),
2018                Some(OkModelingCmdResponse::BooleanUnion(union)) => union
2019                    .extra_solid_ids
2020                    .iter()
2021                    .copied()
2022                    .map(ArtifactId::new)
2023                    .filter(not_cmd_id)
2024                    .for_each(|id| new_solid_ids.push(id)),
2025                _ => {}
2026            }
2027
2028            let mut return_arr = Vec::new();
2029
2030            // Create the new composite solids and update their linked artifacts
2031            for solid_id in &new_solid_ids {
2032                // Create the composite solid
2033                return_arr.push(Artifact::CompositeSolid(CompositeSolid {
2034                    id: *solid_id,
2035                    consumed: false,
2036                    sub_type,
2037                    output_index: None,
2038                    solid_ids: solid_ids.clone(),
2039                    tool_ids: tool_ids.clone(),
2040                    code_ref: code_ref.clone(),
2041                    composite_solid_id: None,
2042                }));
2043
2044                // Update the artifacts that were used as input for this composite solid
2045                for input_id in &solid_ids {
2046                    if let Some(artifact) = artifacts.get(input_id) {
2047                        match artifact {
2048                            Artifact::CompositeSolid(comp) => {
2049                                let mut new_comp = comp.clone();
2050                                new_comp.composite_solid_id = Some(*solid_id);
2051                                new_comp.consumed = true;
2052                                return_arr.push(Artifact::CompositeSolid(new_comp));
2053                            }
2054                            Artifact::Path(path) => {
2055                                let mut new_path = path.clone();
2056                                new_path.composite_solid_id = Some(*solid_id);
2057
2058                                // We want to mark any sweeps of the path used in this operation
2059                                // as consumed. The path itself is already consumed by sweeping
2060                                if let Some(sweep_id) = new_path.sweep_id
2061                                    && let Some(Artifact::Sweep(sweep)) = artifacts.get(&sweep_id)
2062                                {
2063                                    let mut new_sweep = sweep.clone();
2064                                    new_sweep.consumed = true;
2065                                    return_arr.push(Artifact::Sweep(new_sweep));
2066                                }
2067
2068                                return_arr.push(Artifact::Path(new_path));
2069                            }
2070                            _ => {}
2071                        }
2072                    }
2073                }
2074
2075                // Update the tool artifacts if this is a subtract operation
2076                for tool_id in &tool_ids {
2077                    if let Some(artifact) = artifacts.get(tool_id) {
2078                        match artifact {
2079                            Artifact::CompositeSolid(comp) => {
2080                                let mut new_comp = comp.clone();
2081                                new_comp.composite_solid_id = Some(*solid_id);
2082                                new_comp.consumed = true;
2083                                return_arr.push(Artifact::CompositeSolid(new_comp));
2084                            }
2085                            Artifact::Path(path) => {
2086                                let mut new_path = path.clone();
2087                                new_path.composite_solid_id = Some(*solid_id);
2088
2089                                // We want to mark any sweeps of the path used in this operation
2090                                // as consumed. The path itself is already consumed by sweeping
2091                                if let Some(sweep_id) = new_path.sweep_id
2092                                    && let Some(Artifact::Sweep(sweep)) = artifacts.get(&sweep_id)
2093                                {
2094                                    let mut new_sweep = sweep.clone();
2095                                    new_sweep.consumed = true;
2096                                    return_arr.push(Artifact::Sweep(new_sweep));
2097                                }
2098
2099                                return_arr.push(Artifact::Path(new_path));
2100                            }
2101                            _ => {}
2102                        }
2103                    }
2104                }
2105            }
2106
2107            return Ok(return_arr);
2108        }
2109        ModelingCmd::BooleanImprint(imprint) => {
2110            let solid_ids = imprint
2111                .body_ids
2112                .iter()
2113                .copied()
2114                .map(ArtifactId::new)
2115                .collect::<Vec<_>>();
2116            let tool_ids = imprint
2117                .tool_ids
2118                .as_ref()
2119                .map(|ids| ids.iter().copied().map(ArtifactId::new).collect::<Vec<_>>())
2120                .unwrap_or_default();
2121
2122            let mut new_solid_ids = vec![id];
2123            let not_cmd_id = move |solid_id: &ArtifactId| *solid_id != id;
2124            if let Some(OkModelingCmdResponse::BooleanImprint(imprint)) = response {
2125                imprint
2126                    .extra_solid_ids
2127                    .iter()
2128                    .copied()
2129                    .map(ArtifactId::new)
2130                    .filter(not_cmd_id)
2131                    .for_each(|id| new_solid_ids.push(id));
2132            }
2133
2134            let mut return_arr = Vec::new();
2135
2136            for (output_index, solid_id) in new_solid_ids.iter().enumerate() {
2137                return_arr.push(Artifact::CompositeSolid(CompositeSolid {
2138                    id: *solid_id,
2139                    consumed: false,
2140                    sub_type: CompositeSolidSubType::Split,
2141                    output_index: Some(output_index),
2142                    solid_ids: solid_ids.clone(),
2143                    tool_ids: tool_ids.clone(),
2144                    code_ref: code_ref.clone(),
2145                    composite_solid_id: None,
2146                }));
2147
2148                for input_id in solid_ids.iter().chain(tool_ids.iter()) {
2149                    if let Some(artifact) = artifacts.get(input_id) {
2150                        match artifact {
2151                            Artifact::CompositeSolid(comp) => {
2152                                let mut new_comp = comp.clone();
2153                                new_comp.composite_solid_id = Some(*solid_id);
2154                                new_comp.consumed = true;
2155                                return_arr.push(Artifact::CompositeSolid(new_comp));
2156                            }
2157                            Artifact::Path(path) => {
2158                                let mut new_path = path.clone();
2159                                new_path.composite_solid_id = Some(*solid_id);
2160
2161                                if let Some(sweep_id) = new_path.sweep_id
2162                                    && let Some(Artifact::Sweep(sweep)) = artifacts.get(&sweep_id)
2163                                {
2164                                    let mut new_sweep = sweep.clone();
2165                                    new_sweep.consumed = true;
2166                                    return_arr.push(Artifact::Sweep(new_sweep));
2167                                }
2168
2169                                return_arr.push(Artifact::Path(new_path));
2170                            }
2171                            _ => {}
2172                        }
2173                    }
2174                }
2175            }
2176
2177            return Ok(return_arr);
2178        }
2179        _ => {}
2180    }
2181
2182    Ok(Vec::new())
2183}