Skip to main content

kcl_lib/execution/
artifact.rs

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