Skip to main content

kcl_lib/frontend/
sketch.rs

1#![allow(async_fn_in_trait)]
2
3use kcl_api::Arc;
4use kcl_api::Circle;
5use kcl_api::Coincident;
6use kcl_api::Constraint;
7use kcl_api::ConstraintSegment;
8use kcl_api::ControlPointSpline;
9use kcl_api::Distance;
10use kcl_api::Freedom;
11use kcl_api::Line;
12use kcl_api::Number;
13use kcl_api::ObjectId;
14use kcl_api::Point;
15use kcl_api::Point2d;
16use kcl_api::Segment;
17use kcl_api::SegmentCtor;
18use kcl_api::SketchCtor;
19use serde::Deserialize;
20use serde::Serialize;
21
22use crate::ExecutorContext;
23use crate::KclErrorWithOutputs;
24use crate::frontend::api::FileId;
25use crate::frontend::api::ProjectId;
26use crate::frontend::api::SceneGraph;
27use crate::frontend::api::SceneGraphDelta;
28use crate::frontend::api::SourceDelta;
29use crate::frontend::api::Version;
30
31pub type ExecResult<T> = std::result::Result<T, KclErrorWithOutputs>;
32
33/// Information about a newly created segment for batch operations
34#[derive(Debug, Clone)]
35pub struct NewSegmentInfo {
36    pub segment_id: ObjectId,
37    pub start_point_id: ObjectId,
38    pub end_point_id: ObjectId,
39    pub center_point_id: Option<ObjectId>,
40}
41
42pub trait SketchApi {
43    /// Execute the sketch in mock mode, without changing anything. This is
44    /// useful after editing segments, and the user releases the mouse button.
45    async fn execute_mock(
46        &mut self,
47        ctx: &ExecutorContext,
48        version: Version,
49        sketch: ObjectId,
50    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
51
52    async fn new_sketch(
53        &mut self,
54        ctx: &ExecutorContext,
55        project: ProjectId,
56        file: FileId,
57        version: Version,
58        args: SketchCtor,
59    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)>;
60
61    // Enters sketch mode
62    async fn edit_sketch(
63        &mut self,
64        ctx: &ExecutorContext,
65        project: ProjectId,
66        file: FileId,
67        version: Version,
68        sketch: ObjectId,
69    ) -> ExecResult<SceneGraphDelta>;
70
71    async fn exit_sketch(
72        &mut self,
73        ctx: &ExecutorContext,
74        version: Version,
75        sketch: ObjectId,
76    ) -> ExecResult<SceneGraph>;
77
78    async fn delete_sketch(
79        &mut self,
80        ctx: &ExecutorContext,
81        version: Version,
82        sketch: ObjectId,
83    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
84
85    async fn add_segment(
86        &mut self,
87        ctx: &ExecutorContext,
88        version: Version,
89        sketch: ObjectId,
90        segment: SegmentCtor,
91        label: Option<String>,
92    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
93
94    async fn edit_segments(
95        &mut self,
96        ctx: &ExecutorContext,
97        version: Version,
98        sketch: ObjectId,
99        segments: Vec<ExistingSegmentCtor>,
100    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
101
102    async fn delete_objects(
103        &mut self,
104        ctx: &ExecutorContext,
105        version: Version,
106        sketch: ObjectId,
107        constraint_ids: Vec<ObjectId>,
108        segment_ids: Vec<ObjectId>,
109    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
110
111    async fn add_constraint(
112        &mut self,
113        ctx: &ExecutorContext,
114        version: Version,
115        sketch: ObjectId,
116        constraint: Constraint,
117    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
118
119    async fn chain_segment(
120        &mut self,
121        ctx: &ExecutorContext,
122        version: Version,
123        sketch: ObjectId,
124        previous_segment_end_point_id: ObjectId,
125        segment: SegmentCtor,
126        label: Option<String>,
127    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
128
129    async fn edit_constraint_value(
130        &mut self,
131        ctx: &ExecutorContext,
132        version: Version,
133        sketch: ObjectId,
134        constraint_id: ObjectId,
135        value_expression: String,
136    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
137
138    async fn edit_distance_constraint_label_position(
139        &mut self,
140        ctx: &ExecutorContext,
141        version: Version,
142        sketch: ObjectId,
143        constraint_id: ObjectId,
144        label_position: Point2d<Number>,
145        anchor_segment_ids: Vec<ObjectId>,
146    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
147
148    /// Batch operations for split segment: edit segments, add constraints, delete objects.
149    /// All operations are applied to a single AST and execute_after_edit is called once at the end.
150    /// new_segment_info contains the IDs from the segment(s) added in a previous step.
151    #[allow(clippy::too_many_arguments)]
152    async fn batch_split_segment_operations(
153        &mut self,
154        ctx: &ExecutorContext,
155        version: Version,
156        sketch: ObjectId,
157        edit_segments: Vec<ExistingSegmentCtor>,
158        add_constraints: Vec<Constraint>,
159        delete_constraint_ids: Vec<ObjectId>,
160        new_segment_info: NewSegmentInfo,
161    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
162
163    /// Batch operations for tail-cut trim: edit a segment, add coincident constraints,
164    /// delete constraints, and execute once.
165    #[allow(clippy::too_many_arguments)]
166    async fn batch_tail_cut_operations(
167        &mut self,
168        ctx: &ExecutorContext,
169        version: Version,
170        sketch: ObjectId,
171        edit_segments: Vec<ExistingSegmentCtor>,
172        add_constraints: Vec<Constraint>,
173        delete_constraint_ids: Vec<ObjectId>,
174        additional_edited_segment_ids: Vec<ObjectId>,
175    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
176}
177
178/// Runtime helpers for [`Point`].
179pub trait PointExt {
180    /// The freedom of this point.
181    fn freedom(&self) -> Freedom;
182}
183
184impl PointExt for Point {
185    fn freedom(&self) -> Freedom {
186        self.freedom
187    }
188}
189
190/// Runtime helpers for [`Segment`].
191pub trait SegmentExt {
192    /// What kind of geometry is this (point, line, arc, etc)
193    /// Suitable for use in user-facing messages.
194    fn human_friendly_kind_with_article(&self) -> &'static str;
195    /// Compute the overall freedom of this segment. For geometry types (Line,
196    /// Arc, Circle) this looks up and merges the freedom of their constituent
197    /// points. For points, returns the point's own freedom directly.
198    /// Returns `None` if a required point lookup failed.
199    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom>;
200}
201
202impl SegmentExt for Segment {
203    fn human_friendly_kind_with_article(&self) -> &'static str {
204        match self {
205            Self::Point(_) => "a Point",
206            Self::Line(_) => "a Line",
207            Self::Arc(_) => "an Arc",
208            Self::Circle(_) => "a Circle",
209            Self::ControlPointSpline(_) => "a Control Point Spline",
210        }
211    }
212
213    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
214        match self {
215            Self::Point(p) => Some(p.freedom()),
216            Self::Line(l) => l.freedom(&lookup),
217            Self::Arc(a) => a.freedom(&lookup),
218            Self::Circle(c) => c.freedom(&lookup),
219            Self::ControlPointSpline(s) => s.freedom(&lookup),
220        }
221    }
222}
223
224#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
225#[ts(export, export_to = "FrontendApi.ts")]
226pub struct ExistingSegmentCtor {
227    pub id: ObjectId,
228    pub ctor: SegmentCtor,
229}
230
231#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
232#[ts(export, export_to = "FrontendApi.ts")]
233#[serde(rename_all = "camelCase")]
234pub struct ConstraintLabelPositionEdit {
235    pub constraint_id: ObjectId,
236    pub label_position: Point2d<Number>,
237}
238
239/// Runtime helpers for [`Line`].
240pub trait LineExt {
241    /// Compute the overall freedom of this line by merging the freedom of its
242    /// start and end points. Returns `None` if a point lookup failed.
243    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom>;
244}
245
246impl LineExt for Line {
247    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
248        let start = lookup(self.start)?;
249        let end = lookup(self.end)?;
250        Some(start.merge(end))
251    }
252}
253
254#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
255#[ts(export, export_to = "FrontendApi.ts", rename = "ApiStartOrEnd")]
256#[serde(tag = "type")]
257pub enum StartOrEnd<T> {
258    Start(T),
259    End(T),
260}
261
262/// Runtime helpers for [`Arc`].
263pub trait ArcExt {
264    /// Compute the overall freedom of this arc by merging the freedom of its
265    /// start, end, and center points. Returns `None` if a point lookup failed.
266    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom>;
267}
268
269impl ArcExt for Arc {
270    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
271        let start = lookup(self.start)?;
272        let end = lookup(self.end)?;
273        let center = lookup(self.center)?;
274        Some(start.merge(end).merge(center))
275    }
276}
277
278/// Runtime helpers for [`Circle`].
279pub trait CircleExt {
280    /// Compute the overall freedom of this circle by merging the freedom of its
281    /// start and center points. Returns `None` if a point lookup failed.
282    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom>;
283}
284
285impl CircleExt for Circle {
286    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
287        let start = lookup(self.start)?;
288        let center = lookup(self.center)?;
289        Some(start.merge(center))
290    }
291}
292
293/// Runtime helpers for [`ControlPointSpline`].
294pub trait ControlPointSplineExt {
295    /// Compute the overall freedom of this spline by merging the freedom of its
296    /// control points. Returns `None` if any required point lookup failed.
297    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom>;
298}
299
300impl ControlPointSplineExt for ControlPointSpline {
301    fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
302        let mut controls = self.controls.iter();
303        let first = lookup(*controls.next()?)?;
304        let merged = controls.try_fold(first, |acc, id| lookup(*id).map(|freedom| acc.merge(freedom)))?;
305        Some(merged)
306    }
307}
308
309/// Runtime helpers for [`Coincident`].
310pub trait CoincidentExt {
311    fn get_segments(&self) -> Vec<ObjectId>;
312    fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_;
313    fn contains_segment(&self, segment_id: ObjectId) -> bool;
314}
315
316impl CoincidentExt for Coincident {
317    fn get_segments(&self) -> Vec<ObjectId> {
318        self.segments
319            .iter()
320            .filter_map(|segment| match segment {
321                ConstraintSegment::Segment(id) => Some(*id),
322                ConstraintSegment::Origin(_) => None,
323            })
324            .collect()
325    }
326
327    fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
328        self.segments.iter().filter_map(|segment| match segment {
329            ConstraintSegment::Segment(id) => Some(*id),
330            ConstraintSegment::Origin(_) => None,
331        })
332    }
333
334    fn contains_segment(&self, segment_id: ObjectId) -> bool {
335        self.segment_ids().any(|id| id == segment_id)
336    }
337}
338
339/// Runtime helpers for [`Distance`].
340pub trait DistanceExt {
341    fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_;
342    fn contains_segment(&self, segment_id: ObjectId) -> bool;
343}
344
345impl DistanceExt for Distance {
346    fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
347        self.segments.iter().filter_map(|segment| match segment {
348            ConstraintSegment::Segment(id) => Some(*id),
349            ConstraintSegment::Origin(_) => None,
350        })
351    }
352
353    fn contains_segment(&self, segment_id: ObjectId) -> bool {
354        self.segment_ids().any(|id| id == segment_id)
355    }
356}