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    Parallel(Parallel),
416    Perpendicular(Perpendicular),
417    Radius(Radius),
418    Tangent(Tangent),
419    Vertical(Vertical),
420}
421
422#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
423#[ts(export, export_to = "FrontendApi.ts")]
424pub struct Coincident {
425    pub segments: Vec<ConstraintSegment>,
426}
427
428impl Coincident {
429    pub fn get_segments(&self) -> Vec<ObjectId> {
430        self.segments
431            .iter()
432            .filter_map(|segment| match segment {
433                ConstraintSegment::Segment(id) => Some(*id),
434                ConstraintSegment::Origin(_) => None,
435            })
436            .collect()
437    }
438
439    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
440        self.segments.iter().filter_map(|segment| match segment {
441            ConstraintSegment::Segment(id) => Some(*id),
442            ConstraintSegment::Origin(_) => None,
443        })
444    }
445
446    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
447        self.segment_ids().any(|id| id == segment_id)
448    }
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
452#[ts(export, export_to = "FrontendApi.ts")]
453#[serde(untagged)]
454pub enum ConstraintSegment {
455    Segment(ObjectId),
456    Origin(OriginLiteral),
457}
458
459impl ConstraintSegment {
460    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
464#[ts(export, export_to = "FrontendApi.ts")]
465#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
466pub enum OriginLiteral {
467    Origin,
468}
469
470impl From<ObjectId> for ConstraintSegment {
471    fn from(value: ObjectId) -> Self {
472        Self::Segment(value)
473    }
474}
475
476#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
477#[ts(export, export_to = "FrontendApi.ts")]
478pub struct Distance {
479    pub points: Vec<ConstraintSegment>,
480    pub distance: Number,
481    pub source: ConstraintSource,
482}
483
484impl Distance {
485    pub fn point_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
486        self.points.iter().filter_map(|point| match point {
487            ConstraintSegment::Segment(id) => Some(*id),
488            ConstraintSegment::Origin(_) => None,
489        })
490    }
491
492    pub fn contains_point(&self, point_id: ObjectId) -> bool {
493        self.point_ids().any(|id| id == point_id)
494    }
495}
496
497#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
498#[ts(export, export_to = "FrontendApi.ts")]
499pub struct Angle {
500    pub lines: Vec<ObjectId>,
501    pub angle: Number,
502    pub source: ConstraintSource,
503}
504
505#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
506#[ts(export, export_to = "FrontendApi.ts")]
507pub struct ConstraintSource {
508    pub expr: String,
509    pub is_literal: bool,
510}
511
512#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
513#[ts(export, export_to = "FrontendApi.ts")]
514pub struct Radius {
515    pub arc: ObjectId,
516    pub radius: Number,
517    #[serde(default)]
518    pub source: ConstraintSource,
519}
520
521#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
522#[ts(export, export_to = "FrontendApi.ts")]
523pub struct Diameter {
524    pub arc: ObjectId,
525    pub diameter: Number,
526    #[serde(default)]
527    pub source: ConstraintSource,
528}
529
530#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
531#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
532pub struct EqualRadius {
533    pub input: Vec<ObjectId>,
534}
535
536/// Multiple fixed constraints, allowing callers to add fixed constraints on
537/// multiple points at once.
538#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
539#[ts(export, export_to = "FrontendApi.ts")]
540pub struct Fixed {
541    pub points: Vec<FixedPoint>,
542}
543
544/// A fixed constraint on a single point.
545#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
546#[ts(export, export_to = "FrontendApi.ts")]
547pub struct FixedPoint {
548    pub point: ObjectId,
549    pub position: Point2d<Number>,
550}
551
552#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
553#[ts(export, export_to = "FrontendApi.ts")]
554pub struct Horizontal {
555    pub line: ObjectId,
556}
557
558#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
559#[ts(export, export_to = "FrontendApi.ts")]
560pub struct LinesEqualLength {
561    pub lines: Vec<ObjectId>,
562}
563
564#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
565#[ts(export, export_to = "FrontendApi.ts")]
566pub struct Vertical {
567    pub line: ObjectId,
568}
569
570#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
571#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
572pub struct Parallel {
573    pub lines: Vec<ObjectId>,
574}
575
576#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
577#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
578pub struct Perpendicular {
579    pub lines: Vec<ObjectId>,
580}
581
582#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
583#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
584pub struct Tangent {
585    pub input: Vec<ObjectId>,
586}