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,
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 path_to_plane_id_map = FnvHashMap::default();
502    let mut current_plane_id = None;
503
504    for artifact_command in artifact_commands {
505        if let ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) = artifact_command.command {
506            current_plane_id = Some(entity_id);
507        }
508        // If we get a start path command, we need to set the plane ID to the
509        // current plane ID.
510        // THIS IS THE ONLY THING WE CAN ASSUME IS ALWAYS SEQUENTIAL SINCE ITS PART OF THE
511        // SAME ATOMIC COMMANDS BATCHING.
512        if let ModelingCmd::StartPath(_) = artifact_command.command {
513            if let Some(plane_id) = current_plane_id {
514                path_to_plane_id_map.insert(artifact_command.cmd_id, plane_id);
515            }
516        }
517        if let ModelingCmd::SketchModeDisable(_) = artifact_command.command {
518            current_plane_id = None;
519        }
520
521        let flattened_responses = flatten_modeling_command_responses(responses);
522        let artifact_updates = artifacts_to_update(
523            &map,
524            artifact_command,
525            &flattened_responses,
526            &path_to_plane_id_map,
527            ast,
528            exec_artifacts,
529        )?;
530        for artifact in artifact_updates {
531            // Merge with existing artifacts.
532            merge_artifact_into_map(&mut map, artifact);
533        }
534    }
535
536    for exec_artifact in exec_artifacts.values() {
537        merge_artifact_into_map(&mut map, exec_artifact.clone());
538    }
539
540    Ok(ArtifactGraph { map })
541}
542
543/// Flatten the responses into a map of command IDs to modeling command
544/// responses.  The raw responses from the engine contain batches.
545fn flatten_modeling_command_responses(
546    responses: &IndexMap<Uuid, WebSocketResponse>,
547) -> FnvHashMap<Uuid, OkModelingCmdResponse> {
548    let mut map = FnvHashMap::default();
549    for (cmd_id, ws_response) in responses {
550        let WebSocketResponse::Success(response) = ws_response else {
551            // Response not successful.
552            continue;
553        };
554        match &response.resp {
555            OkWebSocketResponseData::Modeling { modeling_response } => {
556                map.insert(*cmd_id, modeling_response.clone());
557            }
558            OkWebSocketResponseData::ModelingBatch { responses } =>
559            {
560                #[expect(
561                    clippy::iter_over_hash_type,
562                    reason = "Since we're moving entries to another unordered map, it's fine that the order is undefined"
563                )]
564                for (cmd_id, batch_response) in responses {
565                    if let BatchResponse::Success {
566                        response: modeling_response,
567                    } = batch_response
568                    {
569                        map.insert(*cmd_id.as_ref(), modeling_response.clone());
570                    }
571                }
572            }
573            OkWebSocketResponseData::IceServerInfo { .. }
574            | OkWebSocketResponseData::TrickleIce { .. }
575            | OkWebSocketResponseData::SdpAnswer { .. }
576            | OkWebSocketResponseData::Export { .. }
577            | OkWebSocketResponseData::MetricsRequest { .. }
578            | OkWebSocketResponseData::ModelingSessionData { .. }
579            | OkWebSocketResponseData::Pong { .. } => {}
580        }
581    }
582
583    map
584}
585
586fn merge_artifact_into_map(map: &mut IndexMap<ArtifactId, Artifact>, new_artifact: Artifact) {
587    let id = new_artifact.id();
588    let Some(old_artifact) = map.get_mut(&id) else {
589        // No old artifact exists.  Insert the new one.
590        map.insert(id, new_artifact);
591        return;
592    };
593
594    if let Some(replacement) = old_artifact.merge(new_artifact) {
595        *old_artifact = replacement;
596    }
597}
598
599/// Merge the new IDs into the base vector, avoiding duplicates.  This is O(nm)
600/// runtime.  Rationale is that most of the ID collections in the artifact graph
601/// are pretty small, but we may want to change this in the future.
602fn merge_ids(base: &mut Vec<ArtifactId>, new: Vec<ArtifactId>) {
603    let original_len = base.len();
604    for id in new {
605        // Don't bother inspecting new items that we just pushed.
606        let original_base = &base[..original_len];
607        if !original_base.contains(&id) {
608            base.push(id);
609        }
610    }
611}
612
613fn merge_opt_id(base: &mut Option<ArtifactId>, new: Option<ArtifactId>) {
614    // Always use the new one, even if it clears it.
615    *base = new;
616}
617
618fn artifacts_to_update(
619    artifacts: &IndexMap<ArtifactId, Artifact>,
620    artifact_command: &ArtifactCommand,
621    responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
622    path_to_plane_id_map: &FnvHashMap<Uuid, Uuid>,
623    _ast: &Node<Program>,
624    exec_artifacts: &IndexMap<ArtifactId, Artifact>,
625) -> Result<Vec<Artifact>, KclError> {
626    // TODO: Build path-to-node from artifact_command source range.  Right now,
627    // we're serializing an empty array, and the TS wrapper fills it in with the
628    // correct value.
629    let path_to_node = Vec::new();
630
631    let range = artifact_command.range;
632    let uuid = artifact_command.cmd_id;
633    let id = ArtifactId::new(uuid);
634
635    let Some(response) = responses.get(&uuid) else {
636        // Response not found or not successful.
637        return Ok(Vec::new());
638    };
639
640    let cmd = &artifact_command.command;
641
642    match cmd {
643        ModelingCmd::MakePlane(_) => {
644            if range.is_synthetic() {
645                return Ok(Vec::new());
646            }
647            // If we're calling `make_plane` and the code range doesn't end at
648            // `0` it's not a default plane, but a custom one from the
649            // offsetPlane standard library function.
650            return Ok(vec![Artifact::Plane(Plane {
651                id,
652                path_ids: Vec::new(),
653                code_ref: CodeRef { range, path_to_node },
654            })]);
655        }
656        ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) => {
657            let existing_plane = artifacts.get(&ArtifactId::new(*entity_id));
658            match existing_plane {
659                Some(Artifact::Wall(wall)) => {
660                    return Ok(vec![Artifact::Wall(Wall {
661                        id: entity_id.into(),
662                        seg_id: wall.seg_id,
663                        edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
664                        sweep_id: wall.sweep_id,
665                        path_ids: wall.path_ids.clone(),
666                        face_code_ref: wall.face_code_ref.clone(),
667                    })]);
668                }
669                Some(Artifact::Cap(cap)) => {
670                    return Ok(vec![Artifact::Cap(Cap {
671                        id: entity_id.into(),
672                        sub_type: cap.sub_type,
673                        edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
674                        sweep_id: cap.sweep_id,
675                        path_ids: cap.path_ids.clone(),
676                        face_code_ref: cap.face_code_ref.clone(),
677                    })]);
678                }
679                Some(_) | None => {
680                    let path_ids = match existing_plane {
681                        Some(Artifact::Plane(Plane { path_ids, .. })) => path_ids.clone(),
682                        _ => Vec::new(),
683                    };
684                    return Ok(vec![Artifact::Plane(Plane {
685                        id: entity_id.into(),
686                        path_ids,
687                        code_ref: CodeRef { range, path_to_node },
688                    })]);
689                }
690            }
691        }
692        ModelingCmd::StartPath(_) => {
693            let mut return_arr = Vec::new();
694            let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
695                KclError::Internal(KclErrorDetails {
696                    message: format!(
697                        "Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
698                    ),
699                    source_ranges: vec![range],
700                })
701            })?;
702            return_arr.push(Artifact::Path(Path {
703                id,
704                plane_id: (*current_plane_id).into(),
705                seg_ids: Vec::new(),
706                sweep_id: None,
707                solid2d_id: None,
708                code_ref: CodeRef { range, path_to_node },
709            }));
710            let plane = artifacts.get(&ArtifactId::new(*current_plane_id));
711            if let Some(Artifact::Plane(plane)) = plane {
712                let code_ref = plane.code_ref.clone();
713                return_arr.push(Artifact::Plane(Plane {
714                    id: (*current_plane_id).into(),
715                    path_ids: vec![id],
716                    code_ref,
717                }));
718            }
719            if let Some(Artifact::Wall(wall)) = plane {
720                return_arr.push(Artifact::Wall(Wall {
721                    id: (*current_plane_id).into(),
722                    seg_id: wall.seg_id,
723                    edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
724                    sweep_id: wall.sweep_id,
725                    path_ids: vec![id],
726                    face_code_ref: wall.face_code_ref.clone(),
727                }));
728            }
729            if let Some(Artifact::Cap(cap)) = plane {
730                return_arr.push(Artifact::Cap(Cap {
731                    id: (*current_plane_id).into(),
732                    sub_type: cap.sub_type,
733                    edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
734                    sweep_id: cap.sweep_id,
735                    path_ids: vec![id],
736                    face_code_ref: cap.face_code_ref.clone(),
737                }));
738            }
739            return Ok(return_arr);
740        }
741        ModelingCmd::ClosePath(_) | ModelingCmd::ExtendPath(_) => {
742            let path_id = ArtifactId::new(match cmd {
743                ModelingCmd::ClosePath(c) => c.path_id,
744                ModelingCmd::ExtendPath(e) => e.path.into(),
745                _ => unreachable!(),
746            });
747            let mut return_arr = Vec::new();
748            return_arr.push(Artifact::Segment(Segment {
749                id,
750                path_id,
751                surface_id: None,
752                edge_ids: Vec::new(),
753                edge_cut_id: None,
754                code_ref: CodeRef { range, path_to_node },
755            }));
756            let path = artifacts.get(&path_id);
757            if let Some(Artifact::Path(path)) = path {
758                let mut new_path = path.clone();
759                new_path.seg_ids = vec![id];
760                return_arr.push(Artifact::Path(new_path));
761            }
762            if let OkModelingCmdResponse::ClosePath(close_path) = response {
763                return_arr.push(Artifact::Solid2d(Solid2d {
764                    id: close_path.face_id.into(),
765                    path_id,
766                }));
767                if let Some(Artifact::Path(path)) = path {
768                    let mut new_path = path.clone();
769                    new_path.solid2d_id = Some(close_path.face_id.into());
770                    return_arr.push(Artifact::Path(new_path));
771                }
772            }
773            return Ok(return_arr);
774        }
775        ModelingCmd::Extrude(kcmc::Extrude { target, .. })
776        | ModelingCmd::Revolve(kcmc::Revolve { target, .. })
777        | ModelingCmd::RevolveAboutEdge(kcmc::RevolveAboutEdge { target, .. })
778        | ModelingCmd::Sweep(kcmc::Sweep { target, .. }) => {
779            let sub_type = match cmd {
780                ModelingCmd::Extrude(_) => SweepSubType::Extrusion,
781                ModelingCmd::Revolve(_) => SweepSubType::Revolve,
782                ModelingCmd::RevolveAboutEdge(_) => SweepSubType::RevolveAboutEdge,
783                ModelingCmd::Sweep(_) => SweepSubType::Sweep,
784                _ => unreachable!(),
785            };
786            let mut return_arr = Vec::new();
787            let target = ArtifactId::from(target);
788            return_arr.push(Artifact::Sweep(Sweep {
789                id,
790                sub_type,
791                path_id: target,
792                surface_ids: Vec::new(),
793                edge_ids: Vec::new(),
794                code_ref: CodeRef { range, path_to_node },
795            }));
796            let path = artifacts.get(&target);
797            if let Some(Artifact::Path(path)) = path {
798                let mut new_path = path.clone();
799                new_path.sweep_id = Some(id);
800                return_arr.push(Artifact::Path(new_path));
801            }
802            return Ok(return_arr);
803        }
804        ModelingCmd::Loft(loft_cmd) => {
805            let OkModelingCmdResponse::Loft(_) = response else {
806                return Ok(Vec::new());
807            };
808            let mut return_arr = Vec::new();
809            return_arr.push(Artifact::Sweep(Sweep {
810                id,
811                sub_type: SweepSubType::Loft,
812                // TODO: Using the first one.  Make sure to revisit this
813                // choice, don't think it matters for now.
814                path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
815                    KclError::Internal(KclErrorDetails {
816                        message: format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
817                        source_ranges: vec![range],
818                    })
819                })?),
820                surface_ids: Vec::new(),
821                edge_ids: Vec::new(),
822                code_ref: CodeRef { range, path_to_node },
823            }));
824            for section_id in &loft_cmd.section_ids {
825                let path = artifacts.get(&ArtifactId::new(*section_id));
826                if let Some(Artifact::Path(path)) = path {
827                    let mut new_path = path.clone();
828                    new_path.sweep_id = Some(id);
829                    return_arr.push(Artifact::Path(new_path));
830                }
831            }
832            return Ok(return_arr);
833        }
834        ModelingCmd::Solid3dGetExtrusionFaceInfo(_) => {
835            let OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(face_info) = response else {
836                return Ok(Vec::new());
837            };
838            let mut return_arr = Vec::new();
839            let mut last_path = None;
840            for face in &face_info.faces {
841                if face.cap != ExtrusionFaceCapType::None {
842                    continue;
843                }
844                let Some(curve_id) = face.curve_id.map(ArtifactId::new) else {
845                    continue;
846                };
847                let Some(face_id) = face.face_id.map(ArtifactId::new) else {
848                    continue;
849                };
850                let Some(Artifact::Segment(seg)) = artifacts.get(&curve_id) else {
851                    continue;
852                };
853                let Some(Artifact::Path(path)) = artifacts.get(&seg.path_id) else {
854                    continue;
855                };
856                last_path = Some(path);
857                let path_sweep_id = path.sweep_id.ok_or_else(|| {
858                    KclError::Internal(KclErrorDetails {
859                        message:format!(
860                            "Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
861                        ),
862                        source_ranges: vec![range],
863                    })
864                })?;
865                let extra_artifact = exec_artifacts.values().find(|a| {
866                    if let Artifact::StartSketchOnFace(s) = a {
867                        s.face_id == face_id
868                    } else {
869                        false
870                    }
871                });
872                let sketch_on_face_source_range = extra_artifact
873                    .and_then(|a| match a {
874                        Artifact::StartSketchOnFace(s) => Some(s.code_ref.range),
875                        // TODO: If we didn't find it, it's probably a bug.
876                        _ => None,
877                    })
878                    .unwrap_or_default();
879
880                return_arr.push(Artifact::Wall(Wall {
881                    id: face_id,
882                    seg_id: curve_id,
883                    edge_cut_edge_ids: Vec::new(),
884                    sweep_id: path_sweep_id,
885                    path_ids: Vec::new(),
886                    face_code_ref: CodeRef {
887                        range: sketch_on_face_source_range,
888                        path_to_node: Vec::new(),
889                    },
890                }));
891                let mut new_seg = seg.clone();
892                new_seg.surface_id = Some(face_id);
893                return_arr.push(Artifact::Segment(new_seg));
894                if let Some(Artifact::Sweep(sweep)) = path.sweep_id.and_then(|id| artifacts.get(&id)) {
895                    let mut new_sweep = sweep.clone();
896                    new_sweep.surface_ids = vec![face_id];
897                    return_arr.push(Artifact::Sweep(new_sweep));
898                }
899            }
900            if let Some(path) = last_path {
901                for face in &face_info.faces {
902                    let sub_type = match face.cap {
903                        ExtrusionFaceCapType::Top => CapSubType::End,
904                        ExtrusionFaceCapType::Bottom => CapSubType::Start,
905                        ExtrusionFaceCapType::None | ExtrusionFaceCapType::Both => continue,
906                    };
907                    let Some(face_id) = face.face_id.map(ArtifactId::new) else {
908                        continue;
909                    };
910                    let path_sweep_id = path.sweep_id.ok_or_else(|| {
911                        KclError::Internal(KclErrorDetails {
912                            message:format!(
913                                "Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
914                            ),
915                            source_ranges: vec![range],
916                        })
917                    })?;
918                    let extra_artifact = exec_artifacts.values().find(|a| {
919                        if let Artifact::StartSketchOnFace(s) = a {
920                            s.face_id == face_id
921                        } else {
922                            false
923                        }
924                    });
925                    let sketch_on_face_source_range = extra_artifact
926                        .and_then(|a| match a {
927                            Artifact::StartSketchOnFace(s) => Some(s.code_ref.range),
928                            _ => None,
929                        })
930                        .unwrap_or_default();
931                    return_arr.push(Artifact::Cap(Cap {
932                        id: face_id,
933                        sub_type,
934                        edge_cut_edge_ids: Vec::new(),
935                        sweep_id: path_sweep_id,
936                        path_ids: Vec::new(),
937                        face_code_ref: CodeRef {
938                            range: sketch_on_face_source_range,
939                            path_to_node: Vec::new(),
940                        },
941                    }));
942                    let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {
943                        continue;
944                    };
945                    let mut new_sweep = sweep.clone();
946                    new_sweep.surface_ids = vec![face_id];
947                    return_arr.push(Artifact::Sweep(new_sweep));
948                }
949            }
950            return Ok(return_arr);
951        }
952        ModelingCmd::Solid3dGetNextAdjacentEdge(kcmc::Solid3dGetNextAdjacentEdge { face_id, edge_id, .. })
953        | ModelingCmd::Solid3dGetOppositeEdge(kcmc::Solid3dGetOppositeEdge { face_id, edge_id, .. }) => {
954            let sub_type = match cmd {
955                ModelingCmd::Solid3dGetNextAdjacentEdge(_) => SweepEdgeSubType::Adjacent,
956                ModelingCmd::Solid3dGetOppositeEdge(_) => SweepEdgeSubType::Opposite,
957                _ => unreachable!(),
958            };
959            let face_id = ArtifactId::new(*face_id);
960            let edge_id = ArtifactId::new(*edge_id);
961            let Some(Artifact::Wall(wall)) = artifacts.get(&face_id) else {
962                return Ok(Vec::new());
963            };
964            let Some(Artifact::Sweep(sweep)) = artifacts.get(&wall.sweep_id) else {
965                return Ok(Vec::new());
966            };
967            let Some(Artifact::Path(_)) = artifacts.get(&sweep.path_id) else {
968                return Ok(Vec::new());
969            };
970            let Some(Artifact::Segment(segment)) = artifacts.get(&edge_id) else {
971                return Ok(Vec::new());
972            };
973            let response_edge_id = match response {
974                OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(r) => {
975                    let Some(edge_id) = r.edge else {
976                        return Err(KclError::Internal(KclErrorDetails {
977                            message:format!(
978                                "Expected Solid3dGetNextAdjacentEdge response to have an edge ID, but found none: id={id:?}, {response:?}"
979                            ),
980                            source_ranges: vec![range],
981                        }));
982                    };
983                    edge_id.into()
984                }
985                OkModelingCmdResponse::Solid3dGetOppositeEdge(r) => r.edge.into(),
986                _ => {
987                    return Err(KclError::Internal(KclErrorDetails {
988                        message:format!(
989                            "Expected Solid3dGetNextAdjacentEdge or Solid3dGetOppositeEdge response, but got: id={id:?}, {response:?}"
990                        ),
991                        source_ranges: vec![range],
992                    }));
993                }
994            };
995
996            let mut return_arr = Vec::new();
997            return_arr.push(Artifact::SweepEdge(SweepEdge {
998                id: response_edge_id,
999                sub_type,
1000                seg_id: edge_id,
1001                sweep_id: sweep.id,
1002            }));
1003            let mut new_segment = segment.clone();
1004            new_segment.edge_ids = vec![response_edge_id];
1005            return_arr.push(Artifact::Segment(new_segment));
1006            let mut new_sweep = sweep.clone();
1007            new_sweep.edge_ids = vec![response_edge_id];
1008            return_arr.push(Artifact::Sweep(new_sweep));
1009            return Ok(return_arr);
1010        }
1011        ModelingCmd::Solid3dFilletEdge(cmd) => {
1012            let mut return_arr = Vec::new();
1013            return_arr.push(Artifact::EdgeCut(EdgeCut {
1014                id,
1015                sub_type: cmd.cut_type.into(),
1016                consumed_edge_id: cmd.edge_id.into(),
1017                edge_ids: Vec::new(),
1018                surface_id: None,
1019                code_ref: CodeRef { range, path_to_node },
1020            }));
1021            let consumed_edge = artifacts.get(&ArtifactId::new(cmd.edge_id));
1022            if let Some(Artifact::Segment(consumed_edge)) = consumed_edge {
1023                let mut new_segment = consumed_edge.clone();
1024                new_segment.edge_cut_id = Some(id);
1025                return_arr.push(Artifact::Segment(new_segment));
1026            } else {
1027                // TODO: Handle other types like SweepEdge.
1028            }
1029            return Ok(return_arr);
1030        }
1031        ModelingCmd::EntityMakeHelixFromParams(_) => {
1032            let return_arr = vec![Artifact::Helix(Helix {
1033                id,
1034                axis_id: None,
1035                code_ref: CodeRef { range, path_to_node },
1036            })];
1037            return Ok(return_arr);
1038        }
1039        ModelingCmd::EntityMakeHelixFromEdge(helix) => {
1040            let edge_id = ArtifactId::new(helix.edge_id);
1041            let return_arr = vec![Artifact::Helix(Helix {
1042                id,
1043                axis_id: Some(edge_id),
1044                code_ref: CodeRef { range, path_to_node },
1045            })];
1046            // We could add the reverse graph edge connecting from the edge to
1047            // the helix here, but it's not useful right now.
1048            return Ok(return_arr);
1049        }
1050        _ => {}
1051    }
1052
1053    Ok(Vec::new())
1054}