kcl_lib/execution/
artifact.rs

1use fnv::FnvHashMap;
2use indexmap::IndexMap;
3use kittycad_modeling_cmds::{
4    self as kcmc,
5    id::ModelingCmdId,
6    ok_response::OkModelingCmdResponse,
7    shared::ExtrusionFaceCapType,
8    websocket::{BatchResponse, OkWebSocketResponseData, WebSocketResponse},
9    EnableSketchMode, ModelingCmd, SketchModeDisable,
10};
11use schemars::JsonSchema;
12use serde::{ser::SerializeSeq, Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::{
16    errors::KclErrorDetails,
17    parsing::ast::types::{Node, Program},
18    KclError, SourceRange,
19};
20
21#[cfg(test)]
22mod mermaid_tests;
23
24/// A command that may create or update artifacts on the TS side.  Because
25/// engine commands are batched, we don't have the response yet when these are
26/// created.
27#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
28#[ts(export_to = "Artifact.ts")]
29#[serde(rename_all = "camelCase")]
30pub struct ArtifactCommand {
31    /// Identifier of the command that can be matched with its response.
32    pub cmd_id: Uuid,
33    pub range: SourceRange,
34    /// The engine command.  Each artifact command is backed by an engine
35    /// command.  In the future, we may need to send information to the TS side
36    /// without an engine command, in which case, we would make this field
37    /// optional.
38    pub command: ModelingCmd,
39}
40
41#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS, JsonSchema)]
42#[ts(export_to = "Artifact.ts")]
43pub struct ArtifactId(Uuid);
44
45impl ArtifactId {
46    pub fn new(uuid: Uuid) -> Self {
47        Self(uuid)
48    }
49}
50
51impl From<Uuid> for ArtifactId {
52    fn from(uuid: Uuid) -> Self {
53        Self::new(uuid)
54    }
55}
56
57impl From<&Uuid> for ArtifactId {
58    fn from(uuid: &Uuid) -> Self {
59        Self::new(*uuid)
60    }
61}
62
63impl From<ArtifactId> for Uuid {
64    fn from(id: ArtifactId) -> Self {
65        id.0
66    }
67}
68
69impl From<&ArtifactId> for Uuid {
70    fn from(id: &ArtifactId) -> Self {
71        id.0
72    }
73}
74
75impl From<ModelingCmdId> for ArtifactId {
76    fn from(id: ModelingCmdId) -> Self {
77        Self::new(*id.as_ref())
78    }
79}
80
81impl From<&ModelingCmdId> for ArtifactId {
82    fn from(id: &ModelingCmdId) -> Self {
83        Self::new(*id.as_ref())
84    }
85}
86
87pub type DummyPathToNode = Vec<()>;
88
89fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
90where
91    S: serde::Serializer,
92{
93    // Always output an empty array, for now.
94    let seq = serializer.serialize_seq(Some(0))?;
95    seq.end()
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS)]
99#[ts(export_to = "Artifact.ts")]
100#[serde(rename_all = "camelCase")]
101pub struct CodeRef {
102    pub range: SourceRange,
103    // TODO: We should implement this in Rust.
104    #[serde(default, serialize_with = "serialize_dummy_path_to_node")]
105    #[ts(type = "Array<[string | number, string]>")]
106    pub path_to_node: DummyPathToNode,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
110#[ts(export_to = "Artifact.ts")]
111#[serde(rename_all = "camelCase")]
112pub struct Plane {
113    pub id: ArtifactId,
114    pub path_ids: Vec<ArtifactId>,
115    pub code_ref: CodeRef,
116}
117
118#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
119#[ts(export_to = "Artifact.ts")]
120#[serde(rename_all = "camelCase")]
121pub struct Path {
122    pub id: ArtifactId,
123    pub plane_id: ArtifactId,
124    pub seg_ids: Vec<ArtifactId>,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub sweep_id: Option<ArtifactId>,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub solid2d_id: Option<ArtifactId>,
129    pub code_ref: CodeRef,
130}
131
132#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
133#[ts(export_to = "Artifact.ts")]
134#[serde(rename_all = "camelCase")]
135pub struct Segment {
136    pub id: ArtifactId,
137    pub path_id: ArtifactId,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub surface_id: Option<ArtifactId>,
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pub edge_ids: Vec<ArtifactId>,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub edge_cut_id: Option<ArtifactId>,
144    pub code_ref: CodeRef,
145}
146
147/// A sweep is a more generic term for extrude, revolve, loft, and sweep.
148#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
149#[ts(export_to = "Artifact.ts")]
150#[serde(rename_all = "camelCase")]
151pub struct Sweep {
152    pub id: ArtifactId,
153    pub sub_type: SweepSubType,
154    pub path_id: ArtifactId,
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub surface_ids: Vec<ArtifactId>,
157    #[serde(default, skip_serializing_if = "Vec::is_empty")]
158    pub edge_ids: Vec<ArtifactId>,
159    pub code_ref: CodeRef,
160}
161
162#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
163#[ts(export_to = "Artifact.ts")]
164#[serde(rename_all = "camelCase")]
165pub enum SweepSubType {
166    Extrusion,
167    Revolve,
168    RevolveAboutEdge,
169    Loft,
170    Sweep,
171}
172
173#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
174#[ts(export_to = "Artifact.ts")]
175#[serde(rename_all = "camelCase")]
176pub struct Solid2d {
177    pub id: ArtifactId,
178    pub path_id: ArtifactId,
179}
180
181#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
182#[ts(export_to = "Artifact.ts")]
183#[serde(rename_all = "camelCase")]
184pub struct Wall {
185    pub id: ArtifactId,
186    pub seg_id: ArtifactId,
187    #[serde(default, skip_serializing_if = "Vec::is_empty")]
188    pub edge_cut_edge_ids: Vec<ArtifactId>,
189    pub sweep_id: ArtifactId,
190    #[serde(default, skip_serializing_if = "Vec::is_empty")]
191    pub path_ids: Vec<ArtifactId>,
192    /// This is for the sketch-on-face plane, not for the wall itself.  Traverse
193    /// to the extrude and/or segment to get the wall's code_ref.
194    pub face_code_ref: CodeRef,
195}
196
197#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
198#[ts(export_to = "Artifact.ts")]
199#[serde(rename_all = "camelCase")]
200pub struct Cap {
201    pub id: ArtifactId,
202    pub sub_type: CapSubType,
203    #[serde(default, skip_serializing_if = "Vec::is_empty")]
204    pub edge_cut_edge_ids: Vec<ArtifactId>,
205    pub sweep_id: ArtifactId,
206    #[serde(default, skip_serializing_if = "Vec::is_empty")]
207    pub path_ids: Vec<ArtifactId>,
208    /// This is for the sketch-on-face plane, not for the cap itself.  Traverse
209    /// to the extrude and/or segment to get the cap's code_ref.
210    pub face_code_ref: CodeRef,
211}
212
213#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
214#[ts(export_to = "Artifact.ts")]
215#[serde(rename_all = "camelCase")]
216pub enum CapSubType {
217    Start,
218    End,
219}
220
221#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
222#[ts(export_to = "Artifact.ts")]
223#[serde(rename_all = "camelCase")]
224pub struct SweepEdge {
225    pub id: ArtifactId,
226    pub sub_type: SweepEdgeSubType,
227    pub seg_id: ArtifactId,
228    pub sweep_id: ArtifactId,
229}
230
231#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
232#[ts(export_to = "Artifact.ts")]
233#[serde(rename_all = "camelCase")]
234pub enum SweepEdgeSubType {
235    Opposite,
236    Adjacent,
237}
238
239#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
240#[ts(export_to = "Artifact.ts")]
241#[serde(rename_all = "camelCase")]
242pub struct EdgeCut {
243    pub id: ArtifactId,
244    pub sub_type: EdgeCutSubType,
245    pub consumed_edge_id: ArtifactId,
246    #[serde(default, skip_serializing_if = "Vec::is_empty")]
247    pub edge_ids: Vec<ArtifactId>,
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub surface_id: Option<ArtifactId>,
250    pub code_ref: CodeRef,
251}
252
253#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
254#[ts(export_to = "Artifact.ts")]
255#[serde(rename_all = "camelCase")]
256pub enum EdgeCutSubType {
257    Fillet,
258    Chamfer,
259}
260
261impl From<kcmc::shared::CutType> for EdgeCutSubType {
262    fn from(cut_type: kcmc::shared::CutType) -> Self {
263        match cut_type {
264            kcmc::shared::CutType::Fillet => EdgeCutSubType::Fillet,
265            kcmc::shared::CutType::Chamfer => EdgeCutSubType::Chamfer,
266        }
267    }
268}
269
270#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
271#[ts(export_to = "Artifact.ts")]
272#[serde(rename_all = "camelCase")]
273pub struct EdgeCutEdge {
274    pub id: ArtifactId,
275    pub edge_cut_id: ArtifactId,
276    pub surface_id: ArtifactId,
277}
278
279#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
280#[ts(export_to = "Artifact.ts")]
281#[serde(rename_all = "camelCase")]
282pub struct Helix {
283    pub id: ArtifactId,
284    /// The axis of the helix.  Currently this is always an edge ID, but we may
285    /// add axes to the graph.
286    pub axis_id: Option<ArtifactId>,
287    pub code_ref: CodeRef,
288}
289
290#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
291#[ts(export_to = "Artifact.ts")]
292#[serde(tag = "type", rename_all = "camelCase")]
293pub enum Artifact {
294    Plane(Plane),
295    Path(Path),
296    Segment(Segment),
297    Solid2d(Solid2d),
298    #[serde(rename_all = "camelCase")]
299    StartSketchOnFace {
300        id: ArtifactId,
301        face_id: Uuid,
302        source_range: SourceRange,
303    },
304    #[serde(rename_all = "camelCase")]
305    StartSketchOnPlane {
306        id: ArtifactId,
307        plane_id: Uuid,
308        source_range: SourceRange,
309    },
310    Sweep(Sweep),
311    Wall(Wall),
312    Cap(Cap),
313    SweepEdge(SweepEdge),
314    EdgeCut(EdgeCut),
315    EdgeCutEdge(EdgeCutEdge),
316    Helix(Helix),
317}
318
319impl Artifact {
320    pub(crate) fn id(&self) -> ArtifactId {
321        match self {
322            Artifact::Plane(a) => a.id,
323            Artifact::Path(a) => a.id,
324            Artifact::Segment(a) => a.id,
325            Artifact::Solid2d(a) => a.id,
326            Artifact::StartSketchOnFace { id, .. } => *id,
327            Artifact::StartSketchOnPlane { id, .. } => *id,
328            Artifact::Sweep(a) => a.id,
329            Artifact::Wall(a) => a.id,
330            Artifact::Cap(a) => a.id,
331            Artifact::SweepEdge(a) => a.id,
332            Artifact::EdgeCut(a) => a.id,
333            Artifact::EdgeCutEdge(a) => a.id,
334            Artifact::Helix(a) => a.id,
335        }
336    }
337
338    #[expect(dead_code)]
339    pub(crate) fn code_ref(&self) -> Option<&CodeRef> {
340        match self {
341            Artifact::Plane(a) => Some(&a.code_ref),
342            Artifact::Path(a) => Some(&a.code_ref),
343            Artifact::Segment(a) => Some(&a.code_ref),
344            Artifact::Solid2d(_) => None,
345            // TODO: We should add code refs for these.
346            Artifact::StartSketchOnFace { .. } => None,
347            Artifact::StartSketchOnPlane { .. } => None,
348            Artifact::Sweep(a) => Some(&a.code_ref),
349            Artifact::Wall(_) => None,
350            Artifact::Cap(_) => None,
351            Artifact::SweepEdge(_) => None,
352            Artifact::EdgeCut(a) => Some(&a.code_ref),
353            Artifact::EdgeCutEdge(_) => None,
354            Artifact::Helix(a) => Some(&a.code_ref),
355        }
356    }
357
358    /// Merge the new artifact into self.  If it can't because it's a different
359    /// type, return the new artifact which should be used as a replacement.
360    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
361        match self {
362            Artifact::Plane(a) => a.merge(new),
363            Artifact::Path(a) => a.merge(new),
364            Artifact::Segment(a) => a.merge(new),
365            Artifact::Solid2d(_) => Some(new),
366            Artifact::StartSketchOnFace { .. } => Some(new),
367            Artifact::StartSketchOnPlane { .. } => Some(new),
368            Artifact::Sweep(a) => a.merge(new),
369            Artifact::Wall(a) => a.merge(new),
370            Artifact::Cap(a) => a.merge(new),
371            Artifact::SweepEdge(_) => Some(new),
372            Artifact::EdgeCut(a) => a.merge(new),
373            Artifact::EdgeCutEdge(_) => Some(new),
374            Artifact::Helix(_) => Some(new),
375        }
376    }
377}
378
379impl Plane {
380    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
381        let Artifact::Plane(new) = new else {
382            return Some(new);
383        };
384        merge_ids(&mut self.path_ids, new.path_ids);
385
386        None
387    }
388}
389
390impl Path {
391    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
392        let Artifact::Path(new) = new else {
393            return Some(new);
394        };
395        merge_opt_id(&mut self.sweep_id, new.sweep_id);
396        merge_ids(&mut self.seg_ids, new.seg_ids);
397        merge_opt_id(&mut self.solid2d_id, new.solid2d_id);
398
399        None
400    }
401}
402
403impl Segment {
404    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
405        let Artifact::Segment(new) = new else {
406            return Some(new);
407        };
408        merge_opt_id(&mut self.surface_id, new.surface_id);
409        merge_ids(&mut self.edge_ids, new.edge_ids);
410        merge_opt_id(&mut self.edge_cut_id, new.edge_cut_id);
411
412        None
413    }
414}
415
416impl Sweep {
417    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
418        let Artifact::Sweep(new) = new else {
419            return Some(new);
420        };
421        merge_ids(&mut self.surface_ids, new.surface_ids);
422        merge_ids(&mut self.edge_ids, new.edge_ids);
423
424        None
425    }
426}
427
428impl Wall {
429    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
430        let Artifact::Wall(new) = new else {
431            return Some(new);
432        };
433        merge_ids(&mut self.edge_cut_edge_ids, new.edge_cut_edge_ids);
434        merge_ids(&mut self.path_ids, new.path_ids);
435
436        None
437    }
438}
439
440impl Cap {
441    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
442        let Artifact::Cap(new) = new else {
443            return Some(new);
444        };
445        merge_ids(&mut self.edge_cut_edge_ids, new.edge_cut_edge_ids);
446        merge_ids(&mut self.path_ids, new.path_ids);
447
448        None
449    }
450}
451
452impl EdgeCut {
453    fn merge(&mut self, new: Artifact) -> Option<Artifact> {
454        let Artifact::EdgeCut(new) = new else {
455            return Some(new);
456        };
457        merge_opt_id(&mut self.surface_id, new.surface_id);
458        merge_ids(&mut self.edge_ids, new.edge_ids);
459
460        None
461    }
462}
463
464#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
465#[ts(export_to = "Artifact.ts")]
466#[serde(rename_all = "camelCase")]
467pub struct ArtifactGraph {
468    map: IndexMap<ArtifactId, Artifact>,
469}
470
471impl ArtifactGraph {
472    pub fn len(&self) -> usize {
473        self.map.len()
474    }
475}
476
477pub(super) fn build_artifact_graph(
478    artifact_commands: &[ArtifactCommand],
479    responses: &IndexMap<Uuid, WebSocketResponse>,
480    ast: &Node<Program>,
481    exec_artifacts: &IndexMap<ArtifactId, Artifact>,
482) -> Result<ArtifactGraph, KclError> {
483    let mut map = IndexMap::new();
484
485    let mut current_plane_id = None;
486
487    for artifact_command in artifact_commands {
488        if let ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) = artifact_command.command {
489            current_plane_id = Some(entity_id);
490        }
491        if let ModelingCmd::SketchModeDisable(SketchModeDisable { .. }) = artifact_command.command {
492            current_plane_id = None;
493        }
494
495        let flattened_responses = flatten_modeling_command_responses(responses);
496        let artifact_updates = artifacts_to_update(
497            &map,
498            artifact_command,
499            &flattened_responses,
500            current_plane_id,
501            ast,
502            exec_artifacts,
503        )?;
504        for artifact in artifact_updates {
505            // Merge with existing artifacts.
506            merge_artifact_into_map(&mut map, artifact);
507        }
508    }
509
510    Ok(ArtifactGraph { map })
511}
512
513/// Flatten the responses into a map of command IDs to modeling command
514/// responses.  The raw responses from the engine contain batches.
515fn flatten_modeling_command_responses(
516    responses: &IndexMap<Uuid, WebSocketResponse>,
517) -> FnvHashMap<Uuid, OkModelingCmdResponse> {
518    let mut map = FnvHashMap::default();
519    for (cmd_id, ws_response) in responses {
520        let WebSocketResponse::Success(response) = ws_response else {
521            // Response not successful.
522            continue;
523        };
524        match &response.resp {
525            OkWebSocketResponseData::Modeling { modeling_response } => {
526                map.insert(*cmd_id, modeling_response.clone());
527            }
528            OkWebSocketResponseData::ModelingBatch { responses } =>
529            {
530                #[expect(
531                    clippy::iter_over_hash_type,
532                    reason = "Since we're moving entries to another unordered map, it's fine that the order is undefined"
533                )]
534                for (cmd_id, batch_response) in responses {
535                    if let BatchResponse::Success {
536                        response: modeling_response,
537                    } = batch_response
538                    {
539                        map.insert(*cmd_id.as_ref(), modeling_response.clone());
540                    }
541                }
542            }
543            OkWebSocketResponseData::IceServerInfo { .. }
544            | OkWebSocketResponseData::TrickleIce { .. }
545            | OkWebSocketResponseData::SdpAnswer { .. }
546            | OkWebSocketResponseData::Export { .. }
547            | OkWebSocketResponseData::MetricsRequest { .. }
548            | OkWebSocketResponseData::ModelingSessionData { .. }
549            | OkWebSocketResponseData::Pong { .. } => {}
550        }
551    }
552
553    map
554}
555
556fn merge_artifact_into_map(map: &mut IndexMap<ArtifactId, Artifact>, new_artifact: Artifact) {
557    let id = new_artifact.id();
558    let Some(old_artifact) = map.get_mut(&id) else {
559        // No old artifact exists.  Insert the new one.
560        map.insert(id, new_artifact);
561        return;
562    };
563
564    if let Some(replacement) = old_artifact.merge(new_artifact) {
565        *old_artifact = replacement;
566    }
567}
568
569/// Merge the new IDs into the base vector, avoiding duplicates.  This is O(nm)
570/// runtime.  Rationale is that most of the ID collections in the artifact graph
571/// are pretty small, but we may want to change this in the future.
572fn merge_ids(base: &mut Vec<ArtifactId>, new: Vec<ArtifactId>) {
573    let original_len = base.len();
574    for id in new {
575        // Don't bother inspecting new items that we just pushed.
576        let original_base = &base[..original_len];
577        if !original_base.contains(&id) {
578            base.push(id);
579        }
580    }
581}
582
583fn merge_opt_id(base: &mut Option<ArtifactId>, new: Option<ArtifactId>) {
584    // Always use the new one, even if it clears it.
585    *base = new;
586}
587
588fn artifacts_to_update(
589    artifacts: &IndexMap<ArtifactId, Artifact>,
590    artifact_command: &ArtifactCommand,
591    responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
592    current_plane_id: Option<Uuid>,
593    _ast: &Node<Program>,
594    exec_artifacts: &IndexMap<ArtifactId, Artifact>,
595) -> Result<Vec<Artifact>, KclError> {
596    // TODO: Build path-to-node from artifact_command source range.  Right now,
597    // we're serializing an empty array, and the TS wrapper fills it in with the
598    // correct value.
599    let path_to_node = Vec::new();
600
601    let range = artifact_command.range;
602    let uuid = artifact_command.cmd_id;
603    let id = ArtifactId::new(uuid);
604
605    let Some(response) = responses.get(&uuid) else {
606        // Response not found or not successful.
607        return Ok(Vec::new());
608    };
609
610    let cmd = &artifact_command.command;
611
612    match cmd {
613        ModelingCmd::MakePlane(_) => {
614            if range.is_synthetic() {
615                return Ok(Vec::new());
616            }
617            // If we're calling `make_plane` and the code range doesn't end at
618            // `0` it's not a default plane, but a custom one from the
619            // offsetPlane standard library function.
620            return Ok(vec![Artifact::Plane(Plane {
621                id,
622                path_ids: Vec::new(),
623                code_ref: CodeRef { range, path_to_node },
624            })]);
625        }
626        ModelingCmd::EnableSketchMode(_) => {
627            let current_plane_id = current_plane_id.ok_or_else(|| {
628                KclError::Internal(KclErrorDetails {
629                    message: format!(
630                        "Expected a current plane ID when processing EnableSketchMode command, but we have none: {id:?}"
631                    ),
632                    source_ranges: vec![range],
633                })
634            })?;
635            let existing_plane = artifacts.get(&ArtifactId::new(current_plane_id));
636            match existing_plane {
637                Some(Artifact::Wall(wall)) => {
638                    return Ok(vec![Artifact::Wall(Wall {
639                        id: current_plane_id.into(),
640                        seg_id: wall.seg_id,
641                        edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
642                        sweep_id: wall.sweep_id,
643                        path_ids: wall.path_ids.clone(),
644                        face_code_ref: wall.face_code_ref.clone(),
645                    })]);
646                }
647                Some(Artifact::Cap(cap)) => {
648                    return Ok(vec![Artifact::Cap(Cap {
649                        id: current_plane_id.into(),
650                        sub_type: cap.sub_type,
651                        edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
652                        sweep_id: cap.sweep_id,
653                        path_ids: cap.path_ids.clone(),
654                        face_code_ref: cap.face_code_ref.clone(),
655                    })]);
656                }
657                Some(_) | None => {
658                    let path_ids = match existing_plane {
659                        Some(Artifact::Plane(Plane { path_ids, .. })) => path_ids.clone(),
660                        _ => Vec::new(),
661                    };
662                    return Ok(vec![Artifact::Plane(Plane {
663                        id: current_plane_id.into(),
664                        path_ids,
665                        code_ref: CodeRef { range, path_to_node },
666                    })]);
667                }
668            }
669        }
670        ModelingCmd::StartPath(_) => {
671            let mut return_arr = Vec::new();
672            let current_plane_id = current_plane_id.ok_or_else(|| {
673                KclError::Internal(KclErrorDetails {
674                    message: format!(
675                        "Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
676                    ),
677                    source_ranges: vec![range],
678                })
679            })?;
680            return_arr.push(Artifact::Path(Path {
681                id,
682                plane_id: current_plane_id.into(),
683                seg_ids: Vec::new(),
684                sweep_id: None,
685                solid2d_id: None,
686                code_ref: CodeRef { range, path_to_node },
687            }));
688            let plane = artifacts.get(&ArtifactId::new(current_plane_id));
689            if let Some(Artifact::Plane(plane)) = plane {
690                let code_ref = plane.code_ref.clone();
691                return_arr.push(Artifact::Plane(Plane {
692                    id: current_plane_id.into(),
693                    path_ids: vec![id],
694                    code_ref,
695                }));
696            }
697            if let Some(Artifact::Wall(wall)) = plane {
698                return_arr.push(Artifact::Wall(Wall {
699                    id: current_plane_id.into(),
700                    seg_id: wall.seg_id,
701                    edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
702                    sweep_id: wall.sweep_id,
703                    path_ids: vec![id],
704                    face_code_ref: wall.face_code_ref.clone(),
705                }));
706            }
707            if let Some(Artifact::Cap(cap)) = plane {
708                return_arr.push(Artifact::Cap(Cap {
709                    id: current_plane_id.into(),
710                    sub_type: cap.sub_type,
711                    edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
712                    sweep_id: cap.sweep_id,
713                    path_ids: vec![id],
714                    face_code_ref: cap.face_code_ref.clone(),
715                }));
716            }
717            return Ok(return_arr);
718        }
719        ModelingCmd::ClosePath(_) | ModelingCmd::ExtendPath(_) => {
720            let path_id = ArtifactId::new(match cmd {
721                ModelingCmd::ClosePath(c) => c.path_id,
722                ModelingCmd::ExtendPath(e) => e.path.into(),
723                _ => unreachable!(),
724            });
725            let mut return_arr = Vec::new();
726            return_arr.push(Artifact::Segment(Segment {
727                id,
728                path_id,
729                surface_id: None,
730                edge_ids: Vec::new(),
731                edge_cut_id: None,
732                code_ref: CodeRef { range, path_to_node },
733            }));
734            let path = artifacts.get(&path_id);
735            if let Some(Artifact::Path(path)) = path {
736                let mut new_path = path.clone();
737                new_path.seg_ids = vec![id];
738                return_arr.push(Artifact::Path(new_path));
739            }
740            if let OkModelingCmdResponse::ClosePath(close_path) = response {
741                return_arr.push(Artifact::Solid2d(Solid2d {
742                    id: close_path.face_id.into(),
743                    path_id,
744                }));
745                if let Some(Artifact::Path(path)) = path {
746                    let mut new_path = path.clone();
747                    new_path.solid2d_id = Some(close_path.face_id.into());
748                    return_arr.push(Artifact::Path(new_path));
749                }
750            }
751            return Ok(return_arr);
752        }
753        ModelingCmd::Extrude(kcmc::Extrude { target, .. })
754        | ModelingCmd::Revolve(kcmc::Revolve { target, .. })
755        | ModelingCmd::RevolveAboutEdge(kcmc::RevolveAboutEdge { target, .. })
756        | ModelingCmd::Sweep(kcmc::Sweep { target, .. }) => {
757            let sub_type = match cmd {
758                ModelingCmd::Extrude(_) => SweepSubType::Extrusion,
759                ModelingCmd::Revolve(_) => SweepSubType::Revolve,
760                ModelingCmd::RevolveAboutEdge(_) => SweepSubType::RevolveAboutEdge,
761                ModelingCmd::Sweep(_) => SweepSubType::Sweep,
762                _ => unreachable!(),
763            };
764            let mut return_arr = Vec::new();
765            let target = ArtifactId::from(target);
766            return_arr.push(Artifact::Sweep(Sweep {
767                id,
768                sub_type,
769                path_id: target,
770                surface_ids: Vec::new(),
771                edge_ids: Vec::new(),
772                code_ref: CodeRef { range, path_to_node },
773            }));
774            let path = artifacts.get(&target);
775            if let Some(Artifact::Path(path)) = path {
776                let mut new_path = path.clone();
777                new_path.sweep_id = Some(id);
778                return_arr.push(Artifact::Path(new_path));
779            }
780            return Ok(return_arr);
781        }
782        ModelingCmd::Loft(loft_cmd) => {
783            let OkModelingCmdResponse::Loft(_) = response else {
784                return Ok(Vec::new());
785            };
786            let mut return_arr = Vec::new();
787            return_arr.push(Artifact::Sweep(Sweep {
788                id,
789                sub_type: SweepSubType::Loft,
790                // TODO: Using the first one.  Make sure to revisit this
791                // choice, don't think it matters for now.
792                path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
793                    KclError::Internal(KclErrorDetails {
794                        message: format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
795                        source_ranges: vec![range],
796                    })
797                })?),
798                surface_ids: Vec::new(),
799                edge_ids: Vec::new(),
800                code_ref: CodeRef { range, path_to_node },
801            }));
802            for section_id in &loft_cmd.section_ids {
803                let path = artifacts.get(&ArtifactId::new(*section_id));
804                if let Some(Artifact::Path(path)) = path {
805                    let mut new_path = path.clone();
806                    new_path.sweep_id = Some(id);
807                    return_arr.push(Artifact::Path(new_path));
808                }
809            }
810            return Ok(return_arr);
811        }
812        ModelingCmd::Solid3dGetExtrusionFaceInfo(_) => {
813            let OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(face_info) = response else {
814                return Ok(Vec::new());
815            };
816            let mut return_arr = Vec::new();
817            let mut last_path = None;
818            for face in &face_info.faces {
819                if face.cap != ExtrusionFaceCapType::None {
820                    continue;
821                }
822                let Some(curve_id) = face.curve_id.map(ArtifactId::new) else {
823                    continue;
824                };
825                let Some(face_id) = face.face_id.map(ArtifactId::new) else {
826                    continue;
827                };
828                let Some(Artifact::Segment(seg)) = artifacts.get(&curve_id) else {
829                    continue;
830                };
831                let Some(Artifact::Path(path)) = artifacts.get(&seg.path_id) else {
832                    continue;
833                };
834                last_path = Some(path);
835                let path_sweep_id = path.sweep_id.ok_or_else(|| {
836                    KclError::Internal(KclErrorDetails {
837                        message:format!(
838                            "Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
839                        ),
840                        source_ranges: vec![range],
841                    })
842                })?;
843                let extra_artifact = exec_artifacts.values().find(|a| {
844                    if let Artifact::StartSketchOnFace { face_id: id, .. } = a {
845                        *id == face_id.0
846                    } else {
847                        false
848                    }
849                });
850                let sketch_on_face_source_range = extra_artifact
851                    .and_then(|a| match a {
852                        Artifact::StartSketchOnFace { source_range, .. } => Some(*source_range),
853                        // TODO: If we didn't find it, it's probably a bug.
854                        _ => None,
855                    })
856                    .unwrap_or_default();
857
858                return_arr.push(Artifact::Wall(Wall {
859                    id: face_id,
860                    seg_id: curve_id,
861                    edge_cut_edge_ids: Vec::new(),
862                    sweep_id: path_sweep_id,
863                    path_ids: Vec::new(),
864                    face_code_ref: CodeRef {
865                        range: sketch_on_face_source_range,
866                        path_to_node: Vec::new(),
867                    },
868                }));
869                let mut new_seg = seg.clone();
870                new_seg.surface_id = Some(face_id);
871                return_arr.push(Artifact::Segment(new_seg));
872                if let Some(Artifact::Sweep(sweep)) = path.sweep_id.and_then(|id| artifacts.get(&id)) {
873                    let mut new_sweep = sweep.clone();
874                    new_sweep.surface_ids = vec![face_id];
875                    return_arr.push(Artifact::Sweep(new_sweep));
876                }
877            }
878            if let Some(path) = last_path {
879                for face in &face_info.faces {
880                    let sub_type = match face.cap {
881                        ExtrusionFaceCapType::Top => CapSubType::End,
882                        ExtrusionFaceCapType::Bottom => CapSubType::Start,
883                        ExtrusionFaceCapType::None | ExtrusionFaceCapType::Both => continue,
884                    };
885                    let Some(face_id) = face.face_id.map(ArtifactId::new) else {
886                        continue;
887                    };
888                    let path_sweep_id = path.sweep_id.ok_or_else(|| {
889                        KclError::Internal(KclErrorDetails {
890                            message:format!(
891                                "Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
892                            ),
893                            source_ranges: vec![range],
894                        })
895                    })?;
896                    let extra_artifact = exec_artifacts.values().find(|a| {
897                        if let Artifact::StartSketchOnFace { face_id: id, .. } = a {
898                            *id == face_id.0
899                        } else {
900                            false
901                        }
902                    });
903                    let sketch_on_face_source_range = extra_artifact
904                        .and_then(|a| match a {
905                            Artifact::StartSketchOnFace { source_range, .. } => Some(*source_range),
906                            _ => None,
907                        })
908                        .unwrap_or_default();
909                    return_arr.push(Artifact::Cap(Cap {
910                        id: face_id,
911                        sub_type,
912                        edge_cut_edge_ids: Vec::new(),
913                        sweep_id: path_sweep_id,
914                        path_ids: Vec::new(),
915                        face_code_ref: CodeRef {
916                            range: sketch_on_face_source_range,
917                            path_to_node: Vec::new(),
918                        },
919                    }));
920                    let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {
921                        continue;
922                    };
923                    let mut new_sweep = sweep.clone();
924                    new_sweep.surface_ids = vec![face_id];
925                    return_arr.push(Artifact::Sweep(new_sweep));
926                }
927            }
928            return Ok(return_arr);
929        }
930        ModelingCmd::Solid3dGetNextAdjacentEdge(kcmc::Solid3dGetNextAdjacentEdge { face_id, edge_id, .. })
931        | ModelingCmd::Solid3dGetOppositeEdge(kcmc::Solid3dGetOppositeEdge { face_id, edge_id, .. }) => {
932            let sub_type = match cmd {
933                ModelingCmd::Solid3dGetNextAdjacentEdge(_) => SweepEdgeSubType::Adjacent,
934                ModelingCmd::Solid3dGetOppositeEdge(_) => SweepEdgeSubType::Opposite,
935                _ => unreachable!(),
936            };
937            let face_id = ArtifactId::new(*face_id);
938            let edge_id = ArtifactId::new(*edge_id);
939            let Some(Artifact::Wall(wall)) = artifacts.get(&face_id) else {
940                return Ok(Vec::new());
941            };
942            let Some(Artifact::Sweep(sweep)) = artifacts.get(&wall.sweep_id) else {
943                return Ok(Vec::new());
944            };
945            let Some(Artifact::Path(_)) = artifacts.get(&sweep.path_id) else {
946                return Ok(Vec::new());
947            };
948            let Some(Artifact::Segment(segment)) = artifacts.get(&edge_id) else {
949                return Ok(Vec::new());
950            };
951            let response_edge_id = match response {
952                OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(r) => {
953                    let Some(edge_id) = r.edge else {
954                        return Err(KclError::Internal(KclErrorDetails {
955                            message:format!(
956                                "Expected Solid3dGetNextAdjacentEdge response to have an edge ID, but found none: id={id:?}, {response:?}"
957                            ),
958                            source_ranges: vec![range],
959                        }));
960                    };
961                    edge_id.into()
962                }
963                OkModelingCmdResponse::Solid3dGetOppositeEdge(r) => r.edge.into(),
964                _ => {
965                    return Err(KclError::Internal(KclErrorDetails {
966                        message:format!(
967                            "Expected Solid3dGetNextAdjacentEdge or Solid3dGetOppositeEdge response, but got: id={id:?}, {response:?}"
968                        ),
969                        source_ranges: vec![range],
970                    }));
971                }
972            };
973
974            let mut return_arr = Vec::new();
975            return_arr.push(Artifact::SweepEdge(SweepEdge {
976                id: response_edge_id,
977                sub_type,
978                seg_id: edge_id,
979                sweep_id: sweep.id,
980            }));
981            let mut new_segment = segment.clone();
982            new_segment.edge_ids = vec![response_edge_id];
983            return_arr.push(Artifact::Segment(new_segment));
984            let mut new_sweep = sweep.clone();
985            new_sweep.edge_ids = vec![response_edge_id];
986            return_arr.push(Artifact::Sweep(new_sweep));
987            return Ok(return_arr);
988        }
989        ModelingCmd::Solid3dFilletEdge(cmd) => {
990            let mut return_arr = Vec::new();
991            return_arr.push(Artifact::EdgeCut(EdgeCut {
992                id,
993                sub_type: cmd.cut_type.into(),
994                consumed_edge_id: cmd.edge_id.into(),
995                edge_ids: Vec::new(),
996                surface_id: None,
997                code_ref: CodeRef { range, path_to_node },
998            }));
999            let consumed_edge = artifacts.get(&ArtifactId::new(cmd.edge_id));
1000            if let Some(Artifact::Segment(consumed_edge)) = consumed_edge {
1001                let mut new_segment = consumed_edge.clone();
1002                new_segment.edge_cut_id = Some(id);
1003                return_arr.push(Artifact::Segment(new_segment));
1004            } else {
1005                // TODO: Handle other types like SweepEdge.
1006            }
1007            return Ok(return_arr);
1008        }
1009        ModelingCmd::EntityMakeHelixFromParams(_) => {
1010            let return_arr = vec![Artifact::Helix(Helix {
1011                id,
1012                axis_id: None,
1013                code_ref: CodeRef { range, path_to_node },
1014            })];
1015            return Ok(return_arr);
1016        }
1017        ModelingCmd::EntityMakeHelixFromEdge(helix) => {
1018            let edge_id = ArtifactId::new(helix.edge_id);
1019            let return_arr = vec![Artifact::Helix(Helix {
1020                id,
1021                axis_id: Some(edge_id),
1022                code_ref: CodeRef { range, path_to_node },
1023            })];
1024            // We could add the reverse graph edge connecting from the edge to
1025            // the helix here, but it's not useful right now.
1026            return Ok(return_arr);
1027        }
1028        _ => {}
1029    }
1030
1031    Ok(Vec::new())
1032}