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