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
183impl Point {
184    /// The freedom of this point.
185    pub fn freedom(&self) -> Freedom {
186        self.freedom
187    }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
191#[ts(export, export_to = "FrontendApi.ts")]
192pub enum Freedom {
193    Free,
194    Fixed,
195    Conflict,
196}
197
198impl Freedom {
199    /// Merges two Freedom values. For example, a point has a solver variable
200    /// for each dimension, x and y. If one dimension is `Free` and the other is
201    /// `Fixed`, the point overall is `Free` since it isn't fully constrained.
202    /// `Conflict` infects the most, followed by `Free`. An object must be fully
203    /// `Fixed` to be `Fixed` overall.
204    pub fn merge(self, other: Self) -> Self {
205        match (self, other) {
206            (Self::Conflict, _) | (_, Self::Conflict) => Self::Conflict,
207            (Self::Free, _) | (_, Self::Free) => Self::Free,
208            (Self::Fixed, Self::Fixed) => Self::Fixed,
209        }
210    }
211}
212
213#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
214#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSegment")]
215#[serde(tag = "type")]
216pub enum Segment {
217    Point(Point),
218    Line(Line),
219    Arc(Arc),
220    Circle(Circle),
221}
222
223impl Segment {
224    /// What kind of geometry is this (point, line, arc, etc)
225    /// Suitable for use in user-facing messages.
226    pub fn human_friendly_kind_with_article(&self) -> &'static str {
227        match self {
228            Self::Point(_) => "a Point",
229            Self::Line(_) => "a Line",
230            Self::Arc(_) => "an Arc",
231            Self::Circle(_) => "a Circle",
232        }
233    }
234
235    /// Compute the overall freedom of this segment. For geometry types (Line,
236    /// Arc, Circle) this looks up and merges the freedom of their constituent
237    /// points. For points, returns the point's own freedom directly.
238    /// Returns `None` if a required point lookup failed.
239    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
240        match self {
241            Self::Point(p) => Some(p.freedom()),
242            Self::Line(l) => l.freedom(&lookup),
243            Self::Arc(a) => a.freedom(&lookup),
244            Self::Circle(c) => c.freedom(&lookup),
245        }
246    }
247}
248
249#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
250#[ts(export, export_to = "FrontendApi.ts")]
251pub struct ExistingSegmentCtor {
252    pub id: ObjectId,
253    pub ctor: SegmentCtor,
254}
255
256#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
257#[ts(export, export_to = "FrontendApi.ts")]
258#[serde(tag = "type")]
259pub enum SegmentCtor {
260    Point(PointCtor),
261    Line(LineCtor),
262    Arc(ArcCtor),
263    Circle(CircleCtor),
264}
265
266impl SegmentCtor {
267    /// What kind of geometry is this (point, line, arc, etc)
268    /// Suitable for use in user-facing messages.
269    pub fn human_friendly_kind_with_article(&self) -> &'static str {
270        match self {
271            Self::Point(_) => "a Point constructor",
272            Self::Line(_) => "a Line constructor",
273            Self::Arc(_) => "an Arc constructor",
274            Self::Circle(_) => "a Circle constructor",
275        }
276    }
277}
278
279#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
280#[ts(export, export_to = "FrontendApi.ts")]
281pub struct PointCtor {
282    pub position: Point2d<Expr>,
283}
284
285#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
286#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint2d")]
287pub struct Point2d<U: std::fmt::Debug + Clone + ts_rs::TS> {
288    pub x: U,
289    pub y: U,
290}
291
292#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
293#[ts(export, export_to = "FrontendApi.ts", rename = "ApiLine")]
294pub struct Line {
295    pub start: ObjectId,
296    pub end: ObjectId,
297    // Invariant: Line or MidPointLine
298    pub ctor: SegmentCtor,
299    // The constructor is applicable if changing the values of the constructor will change the rendering
300    // of the segment (modulo multiple valid solutions). I.e., whether the object is constrained with
301    // respect to the constructor inputs.
302    // The frontend should only display handles for the constructor inputs if the ctor is applicable.
303    // (Or because they are the (locked) start/end of the segment).
304    pub ctor_applicable: bool,
305    pub construction: bool,
306}
307
308impl Line {
309    /// Compute the overall freedom of this line by merging the freedom of its
310    /// start and end points. Returns `None` if a point lookup failed.
311    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
312        let start = lookup(self.start)?;
313        let end = lookup(self.end)?;
314        Some(start.merge(end))
315    }
316}
317
318#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
319#[ts(export, export_to = "FrontendApi.ts")]
320pub struct LineCtor {
321    pub start: Point2d<Expr>,
322    pub end: Point2d<Expr>,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    #[ts(optional)]
325    pub construction: Option<bool>,
326}
327
328#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
329#[ts(export, export_to = "FrontendApi.ts", rename = "ApiStartOrEnd")]
330#[serde(tag = "type")]
331pub enum StartOrEnd<T> {
332    Start(T),
333    End(T),
334}
335
336#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
337#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
338pub struct Arc {
339    pub start: ObjectId,
340    pub end: ObjectId,
341    pub center: ObjectId,
342    // Invariant: Arc
343    pub ctor: SegmentCtor,
344    pub ctor_applicable: bool,
345    pub construction: bool,
346}
347
348impl Arc {
349    /// Compute the overall freedom of this arc by merging the freedom of its
350    /// start, end, and center points. Returns `None` if a point lookup failed.
351    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
352        let start = lookup(self.start)?;
353        let end = lookup(self.end)?;
354        let center = lookup(self.center)?;
355        Some(start.merge(end).merge(center))
356    }
357}
358
359#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
360#[ts(export, export_to = "FrontendApi.ts")]
361pub struct ArcCtor {
362    pub start: Point2d<Expr>,
363    pub end: Point2d<Expr>,
364    pub center: Point2d<Expr>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    #[ts(optional)]
367    pub construction: Option<bool>,
368}
369
370#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
371#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
372pub struct Circle {
373    pub start: ObjectId,
374    pub center: ObjectId,
375    // Invariant: Circle
376    pub ctor: SegmentCtor,
377    pub ctor_applicable: bool,
378    pub construction: bool,
379}
380
381impl Circle {
382    /// Compute the overall freedom of this circle by merging the freedom of its
383    /// start and center points. Returns `None` if a point lookup failed.
384    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
385        let start = lookup(self.start)?;
386        let center = lookup(self.center)?;
387        Some(start.merge(center))
388    }
389}
390
391#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
392#[ts(export, export_to = "FrontendApi.ts")]
393pub struct CircleCtor {
394    pub start: Point2d<Expr>,
395    pub center: Point2d<Expr>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    #[ts(optional)]
398    pub construction: Option<bool>,
399}
400
401#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
402#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
403#[serde(tag = "type")]
404pub enum Constraint {
405    Coincident(Coincident),
406    Distance(Distance),
407    Angle(Angle),
408    Diameter(Diameter),
409    EqualRadius(EqualRadius),
410    Fixed(Fixed),
411    HorizontalDistance(Distance),
412    VerticalDistance(Distance),
413    Horizontal(Horizontal),
414    LinesEqualLength(LinesEqualLength),
415    Midpoint(Midpoint),
416    Parallel(Parallel),
417    Perpendicular(Perpendicular),
418    Radius(Radius),
419    Tangent(Tangent),
420    Vertical(Vertical),
421}
422
423#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
424#[ts(export, export_to = "FrontendApi.ts")]
425pub struct Coincident {
426    pub segments: Vec<ConstraintSegment>,
427}
428
429impl Coincident {
430    pub fn get_segments(&self) -> Vec<ObjectId> {
431        self.segments
432            .iter()
433            .filter_map(|segment| match segment {
434                ConstraintSegment::Segment(id) => Some(*id),
435                ConstraintSegment::Origin(_) => None,
436            })
437            .collect()
438    }
439
440    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
441        self.segments.iter().filter_map(|segment| match segment {
442            ConstraintSegment::Segment(id) => Some(*id),
443            ConstraintSegment::Origin(_) => None,
444        })
445    }
446
447    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
448        self.segment_ids().any(|id| id == segment_id)
449    }
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
453#[ts(export, export_to = "FrontendApi.ts")]
454#[serde(untagged)]
455pub enum ConstraintSegment {
456    Segment(ObjectId),
457    Origin(OriginLiteral),
458}
459
460impl ConstraintSegment {
461    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
465#[ts(export, export_to = "FrontendApi.ts")]
466#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
467pub enum OriginLiteral {
468    Origin,
469}
470
471impl From<ObjectId> for ConstraintSegment {
472    fn from(value: ObjectId) -> Self {
473        Self::Segment(value)
474    }
475}
476
477#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
478#[ts(export, export_to = "FrontendApi.ts")]
479pub struct Distance {
480    pub points: Vec<ConstraintSegment>,
481    pub distance: Number,
482    pub source: ConstraintSource,
483}
484
485impl Distance {
486    pub fn point_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
487        self.points.iter().filter_map(|point| match point {
488            ConstraintSegment::Segment(id) => Some(*id),
489            ConstraintSegment::Origin(_) => None,
490        })
491    }
492
493    pub fn contains_point(&self, point_id: ObjectId) -> bool {
494        self.point_ids().any(|id| id == point_id)
495    }
496}
497
498#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
499#[ts(export, export_to = "FrontendApi.ts")]
500pub struct Angle {
501    pub lines: Vec<ObjectId>,
502    pub angle: Number,
503    pub source: ConstraintSource,
504}
505
506#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
507#[ts(export, export_to = "FrontendApi.ts")]
508pub struct ConstraintSource {
509    pub expr: String,
510    pub is_literal: bool,
511}
512
513#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
514#[ts(export, export_to = "FrontendApi.ts")]
515pub struct Radius {
516    pub arc: ObjectId,
517    pub radius: Number,
518    #[serde(default)]
519    pub source: ConstraintSource,
520}
521
522#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
523#[ts(export, export_to = "FrontendApi.ts")]
524pub struct Diameter {
525    pub arc: ObjectId,
526    pub diameter: Number,
527    #[serde(default)]
528    pub source: ConstraintSource,
529}
530
531#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
532#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
533pub struct EqualRadius {
534    pub input: Vec<ObjectId>,
535}
536
537/// Multiple fixed constraints, allowing callers to add fixed constraints on
538/// multiple points at once.
539#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
540#[ts(export, export_to = "FrontendApi.ts")]
541pub struct Fixed {
542    pub points: Vec<FixedPoint>,
543}
544
545/// A fixed constraint on a single point.
546#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
547#[ts(export, export_to = "FrontendApi.ts")]
548pub struct FixedPoint {
549    pub point: ObjectId,
550    pub position: Point2d<Number>,
551}
552
553#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
554#[ts(export, export_to = "FrontendApi.ts")]
555#[serde(untagged)]
556pub enum Horizontal {
557    Line { line: ObjectId },
558    Points { points: Vec<ConstraintSegment> },
559}
560
561#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
562#[ts(export, export_to = "FrontendApi.ts")]
563pub struct LinesEqualLength {
564    pub lines: Vec<ObjectId>,
565}
566
567#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
568#[ts(export, export_to = "FrontendApi.ts")]
569pub struct Midpoint {
570    pub point: ObjectId,
571    #[serde(alias = "line")]
572    pub segment: ObjectId,
573}
574
575#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
576#[ts(export, export_to = "FrontendApi.ts")]
577#[serde(untagged)]
578pub enum Vertical {
579    Line { line: ObjectId },
580    Points { points: Vec<ConstraintSegment> },
581}
582
583#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
584#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
585pub struct Parallel {
586    pub lines: Vec<ObjectId>,
587}
588
589#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
590#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
591pub struct Perpendicular {
592    pub lines: Vec<ObjectId>,
593}
594
595#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
596#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
597pub struct Tangent {
598    pub input: Vec<ObjectId>,
599}