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