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