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