Skip to main content

kcl_lib/frontend/
sketch.rs

1#![allow(async_fn_in_trait)]
2
3use serde::Deserialize;
4use serde::Serialize;
5
6use crate::ExecutorContext;
7use crate::KclErrorWithOutputs;
8use crate::front::Plane;
9use crate::frontend::api::Expr;
10use crate::frontend::api::FileId;
11use crate::frontend::api::Number;
12use crate::frontend::api::ObjectId;
13use crate::frontend::api::ProjectId;
14use crate::frontend::api::SceneGraph;
15use crate::frontend::api::SceneGraphDelta;
16use crate::frontend::api::SourceDelta;
17use crate::frontend::api::Version;
18
19pub type ExecResult<T> = std::result::Result<T, KclErrorWithOutputs>;
20
21/// Information about a newly created segment for batch operations
22#[derive(Debug, Clone)]
23pub struct NewSegmentInfo {
24    pub segment_id: ObjectId,
25    pub start_point_id: ObjectId,
26    pub end_point_id: ObjectId,
27    pub center_point_id: Option<ObjectId>,
28}
29
30pub trait SketchApi {
31    /// Execute the sketch in mock mode, without changing anything. This is
32    /// useful after editing segments, and the user releases the mouse button.
33    async fn execute_mock(
34        &mut self,
35        ctx: &ExecutorContext,
36        version: Version,
37        sketch: ObjectId,
38    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
39
40    async fn new_sketch(
41        &mut self,
42        ctx: &ExecutorContext,
43        project: ProjectId,
44        file: FileId,
45        version: Version,
46        args: SketchCtor,
47    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)>;
48
49    // Enters sketch mode
50    async fn edit_sketch(
51        &mut self,
52        ctx: &ExecutorContext,
53        project: ProjectId,
54        file: FileId,
55        version: Version,
56        sketch: ObjectId,
57    ) -> ExecResult<SceneGraphDelta>;
58
59    async fn exit_sketch(
60        &mut self,
61        ctx: &ExecutorContext,
62        version: Version,
63        sketch: ObjectId,
64    ) -> ExecResult<SceneGraph>;
65
66    async fn delete_sketch(
67        &mut self,
68        ctx: &ExecutorContext,
69        version: Version,
70        sketch: ObjectId,
71    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
72
73    async fn add_segment(
74        &mut self,
75        ctx: &ExecutorContext,
76        version: Version,
77        sketch: ObjectId,
78        segment: SegmentCtor,
79        label: Option<String>,
80    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
81
82    async fn edit_segments(
83        &mut self,
84        ctx: &ExecutorContext,
85        version: Version,
86        sketch: ObjectId,
87        segments: Vec<ExistingSegmentCtor>,
88    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
89
90    async fn delete_objects(
91        &mut self,
92        ctx: &ExecutorContext,
93        version: Version,
94        sketch: ObjectId,
95        constraint_ids: Vec<ObjectId>,
96        segment_ids: Vec<ObjectId>,
97    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
98
99    async fn add_constraint(
100        &mut self,
101        ctx: &ExecutorContext,
102        version: Version,
103        sketch: ObjectId,
104        constraint: Constraint,
105    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
106
107    async fn chain_segment(
108        &mut self,
109        ctx: &ExecutorContext,
110        version: Version,
111        sketch: ObjectId,
112        previous_segment_end_point_id: ObjectId,
113        segment: SegmentCtor,
114        label: Option<String>,
115    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
116
117    async fn edit_constraint(
118        &mut self,
119        ctx: &ExecutorContext,
120        version: Version,
121        sketch: ObjectId,
122        constraint_id: ObjectId,
123        value_expression: String,
124    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
125
126    /// Batch operations for split segment: edit segments, add constraints, delete objects.
127    /// All operations are applied to a single AST and execute_after_edit is called once at the end.
128    /// new_segment_info contains the IDs from the segment(s) added in a previous step.
129    #[allow(clippy::too_many_arguments)]
130    async fn batch_split_segment_operations(
131        &mut self,
132        ctx: &ExecutorContext,
133        version: Version,
134        sketch: ObjectId,
135        edit_segments: Vec<ExistingSegmentCtor>,
136        add_constraints: Vec<Constraint>,
137        delete_constraint_ids: Vec<ObjectId>,
138        new_segment_info: NewSegmentInfo,
139    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
140
141    /// Batch operations for tail-cut trim: edit a segment, add coincident constraints,
142    /// delete constraints, and execute once.
143    async fn batch_tail_cut_operations(
144        &mut self,
145        ctx: &ExecutorContext,
146        version: Version,
147        sketch: ObjectId,
148        edit_segments: Vec<ExistingSegmentCtor>,
149        add_constraints: Vec<Constraint>,
150        delete_constraint_ids: Vec<ObjectId>,
151    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
152}
153
154#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
155#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSketch")]
156pub struct Sketch {
157    pub args: SketchCtor,
158    pub plane: ObjectId,
159    pub segments: Vec<ObjectId>,
160    pub constraints: Vec<ObjectId>,
161}
162
163/// Arguments for creating a new sketch. This is similar to the constructor of
164/// other kinds of objects in that it is the inputs to the sketch, not the
165/// outputs.
166#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
167#[ts(export, export_to = "FrontendApi.ts")]
168pub struct SketchCtor {
169    /// The sketch surface.
170    pub on: Plane,
171}
172
173#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
174#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint")]
175pub struct Point {
176    pub position: Point2d<Number>,
177    pub ctor: Option<PointCtor>,
178    pub owner: Option<ObjectId>,
179    pub freedom: Freedom,
180    pub constraints: Vec<ObjectId>,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
184#[ts(export, export_to = "FrontendApi.ts")]
185pub enum Freedom {
186    Free,
187    Fixed,
188    Conflict,
189}
190
191impl Freedom {
192    /// Merges two Freedom values. For example, a point has a solver variable
193    /// for each dimension, x and y. If one dimension is `Free` and the other is
194    /// `Fixed`, the point overall is `Free` since it isn't fully constrained.
195    /// `Conflict` infects the most, followed by `Free`. An object must be fully
196    /// `Fixed` to be `Fixed` overall.
197    pub fn merge(self, other: Self) -> Self {
198        match (self, other) {
199            (Self::Conflict, _) | (_, Self::Conflict) => Self::Conflict,
200            (Self::Free, _) | (_, Self::Free) => Self::Free,
201            (Self::Fixed, Self::Fixed) => Self::Fixed,
202        }
203    }
204}
205
206#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
207#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSegment")]
208#[serde(tag = "type")]
209pub enum Segment {
210    Point(Point),
211    Line(Line),
212    Arc(Arc),
213    Circle(Circle),
214}
215
216#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
217#[ts(export, export_to = "FrontendApi.ts")]
218pub struct ExistingSegmentCtor {
219    pub id: ObjectId,
220    pub ctor: SegmentCtor,
221}
222
223#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
224#[ts(export, export_to = "FrontendApi.ts")]
225#[serde(tag = "type")]
226pub enum SegmentCtor {
227    Point(PointCtor),
228    Line(LineCtor),
229    Arc(ArcCtor),
230    Circle(CircleCtor),
231}
232
233#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
234#[ts(export, export_to = "FrontendApi.ts")]
235pub struct PointCtor {
236    pub position: Point2d<Expr>,
237}
238
239#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
240#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint2d")]
241pub struct Point2d<U: std::fmt::Debug + Clone + ts_rs::TS> {
242    pub x: U,
243    pub y: U,
244}
245
246#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
247#[ts(export, export_to = "FrontendApi.ts", rename = "ApiLine")]
248pub struct Line {
249    pub start: ObjectId,
250    pub end: ObjectId,
251    // Invariant: Line or MidPointLine
252    pub ctor: SegmentCtor,
253    // The constructor is applicable if changing the values of the constructor will change the rendering
254    // of the segment (modulo multiple valid solutions). I.e., whether the object is constrained with
255    // respect to the constructor inputs.
256    // The frontend should only display handles for the constructor inputs if the ctor is applicable.
257    // (Or because they are the (locked) start/end of the segment).
258    pub ctor_applicable: bool,
259    pub construction: bool,
260}
261
262#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
263#[ts(export, export_to = "FrontendApi.ts")]
264pub struct LineCtor {
265    pub start: Point2d<Expr>,
266    pub end: Point2d<Expr>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    #[ts(optional)]
269    pub construction: Option<bool>,
270}
271
272#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
273#[ts(export, export_to = "FrontendApi.ts", rename = "ApiStartOrEnd")]
274#[serde(tag = "type")]
275pub enum StartOrEnd<T> {
276    Start(T),
277    End(T),
278}
279
280#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
281#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
282pub struct Arc {
283    pub start: ObjectId,
284    pub end: ObjectId,
285    pub center: ObjectId,
286    // Invariant: Arc
287    pub ctor: SegmentCtor,
288    pub ctor_applicable: bool,
289    pub construction: bool,
290}
291
292#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
293#[ts(export, export_to = "FrontendApi.ts")]
294pub struct ArcCtor {
295    pub start: Point2d<Expr>,
296    pub end: Point2d<Expr>,
297    pub center: Point2d<Expr>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    #[ts(optional)]
300    pub construction: Option<bool>,
301}
302
303#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
304#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
305pub struct Circle {
306    pub start: ObjectId,
307    pub center: ObjectId,
308    // Invariant: Circle
309    pub ctor: SegmentCtor,
310    pub ctor_applicable: bool,
311    pub construction: bool,
312}
313
314#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
315#[ts(export, export_to = "FrontendApi.ts")]
316pub struct CircleCtor {
317    pub start: Point2d<Expr>,
318    pub center: Point2d<Expr>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    #[ts(optional)]
321    pub construction: Option<bool>,
322}
323
324#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
325#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
326#[serde(tag = "type")]
327pub enum Constraint {
328    Coincident(Coincident),
329    Distance(Distance),
330    Angle(Angle),
331    Diameter(Diameter),
332    Fixed(Fixed),
333    HorizontalDistance(Distance),
334    VerticalDistance(Distance),
335    Horizontal(Horizontal),
336    LinesEqualLength(LinesEqualLength),
337    Parallel(Parallel),
338    Perpendicular(Perpendicular),
339    Radius(Radius),
340    Tangent(Tangent),
341    Vertical(Vertical),
342}
343
344#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
345#[ts(export, export_to = "FrontendApi.ts")]
346pub struct Coincident {
347    pub segments: Vec<ConstraintSegment>,
348}
349
350impl Coincident {
351    pub fn get_segments(&self) -> Vec<ObjectId> {
352        self.segments
353            .iter()
354            .filter_map(|segment| match segment {
355                ConstraintSegment::Segment(id) => Some(*id),
356                ConstraintSegment::Origin(_) => None,
357            })
358            .collect()
359    }
360
361    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
362        self.segments.iter().filter_map(|segment| match segment {
363            ConstraintSegment::Segment(id) => Some(*id),
364            ConstraintSegment::Origin(_) => None,
365        })
366    }
367
368    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
369        self.segment_ids().any(|id| id == segment_id)
370    }
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
374#[ts(export, export_to = "FrontendApi.ts")]
375#[serde(untagged)]
376pub enum ConstraintSegment {
377    Segment(ObjectId),
378    Origin(OriginLiteral),
379}
380
381impl ConstraintSegment {
382    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
386#[ts(export, export_to = "FrontendApi.ts")]
387#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
388pub enum OriginLiteral {
389    Origin,
390}
391
392impl From<ObjectId> for ConstraintSegment {
393    fn from(value: ObjectId) -> Self {
394        Self::Segment(value)
395    }
396}
397
398#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
399#[ts(export, export_to = "FrontendApi.ts")]
400pub struct Distance {
401    pub points: Vec<ConstraintSegment>,
402    pub distance: Number,
403    pub source: ConstraintSource,
404}
405
406impl Distance {
407    pub fn point_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
408        self.points.iter().filter_map(|point| match point {
409            ConstraintSegment::Segment(id) => Some(*id),
410            ConstraintSegment::Origin(_) => None,
411        })
412    }
413
414    pub fn contains_point(&self, point_id: ObjectId) -> bool {
415        self.point_ids().any(|id| id == point_id)
416    }
417}
418
419#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
420#[ts(export, export_to = "FrontendApi.ts")]
421pub struct Angle {
422    pub lines: Vec<ObjectId>,
423    pub angle: Number,
424    pub source: ConstraintSource,
425}
426
427#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
428#[ts(export, export_to = "FrontendApi.ts")]
429pub struct ConstraintSource {
430    pub expr: String,
431    pub is_literal: bool,
432}
433
434#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
435#[ts(export, export_to = "FrontendApi.ts")]
436pub struct Radius {
437    pub arc: ObjectId,
438    pub radius: Number,
439    #[serde(default)]
440    pub source: ConstraintSource,
441}
442
443#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
444#[ts(export, export_to = "FrontendApi.ts")]
445pub struct Diameter {
446    pub arc: ObjectId,
447    pub diameter: Number,
448    #[serde(default)]
449    pub source: ConstraintSource,
450}
451
452/// Multiple fixed constraints, allowing callers to add fixed constraints on
453/// multiple points at once.
454#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
455#[ts(export, export_to = "FrontendApi.ts")]
456pub struct Fixed {
457    pub points: Vec<FixedPoint>,
458}
459
460/// A fixed constraint on a single point.
461#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
462#[ts(export, export_to = "FrontendApi.ts")]
463pub struct FixedPoint {
464    pub point: ObjectId,
465    pub position: Point2d<Number>,
466}
467
468#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
469#[ts(export, export_to = "FrontendApi.ts")]
470pub struct Horizontal {
471    pub line: ObjectId,
472}
473
474#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
475#[ts(export, export_to = "FrontendApi.ts")]
476pub struct LinesEqualLength {
477    pub lines: Vec<ObjectId>,
478}
479
480#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
481#[ts(export, export_to = "FrontendApi.ts")]
482pub struct Vertical {
483    pub line: ObjectId,
484}
485
486#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
487#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
488pub struct Parallel {
489    pub lines: Vec<ObjectId>,
490}
491
492#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
493#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
494pub struct Perpendicular {
495    pub lines: Vec<ObjectId>,
496}
497
498#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
499#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
500pub struct Tangent {
501    pub input: Vec<ObjectId>,
502}