diff --git a/rust/kcl-api/src/lib.rs b/rust/kcl-api/src/lib.rs index 881b985fd5f..1d66fa5d6c5 100644 --- a/rust/kcl-api/src/lib.rs +++ b/rust/kcl-api/src/lib.rs @@ -64,6 +64,13 @@ impl SceneGraphDelta { #[ts(export)] pub struct SourceDelta {} +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +// TODO not sure if this needs to be file name and content? +pub struct KclSource { + pub text: String, +} + #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)] #[ts(export, rename = "ApiObjectId")] pub struct ObjectId(pub usize); diff --git a/rust/kcl-api/src/sketch.rs b/rust/kcl-api/src/sketch.rs index a4385eede6f..1dae3fcfdcd 100644 --- a/rust/kcl-api/src/sketch.rs +++ b/rust/kcl-api/src/sketch.rs @@ -30,7 +30,7 @@ pub trait SketchApi { sketch: ObjectId, segment: SegmentCtor, label: Option, - ) -> Result<(SourceDelta, SceneGraphDelta)>; + ) -> Result<(KclSource, SketchExecOutcome)>; async fn edit_segment( &self, @@ -114,7 +114,7 @@ pub enum Segment { #[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] #[ts(export)] pub enum SegmentCtor { - Point(Point2d), + Point(PointCtor), Line(LineCtor), MidPointLine(MidPointLineCtor), Arc(ArcCtor), @@ -124,6 +124,12 @@ pub enum SegmentCtor { ThreePointCircle(ThreePointCircleCtor), } +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub struct PointCtor { + pub position: Point2d, +} + #[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] #[ts(export, rename = "ApiPoint2d")] pub struct Point2d { @@ -246,3 +252,166 @@ pub struct Parallel { lines: Vec, distance: Option, } + +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub struct SketchExecOutcome { + // The solved segments, including their locations so that they can be drawn. + pub segments: Vec, + // The interpreted constraints. + pub constraints: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub enum SolveSegment { + Point(SolvePointSegment), +} + +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub struct SolvePointSegment { + pub object_id: String, + pub constrained_status: ConstrainedStatus, + pub handles: Vec, + pub position: Point2d, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub enum ConstrainedStatus { + None, + Partial, + Full, +} + +// Handles, i.e. UI elements that the user can interact with. +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub struct PointHandle { + pub position: Point2d, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub enum SolveConstraint { + // e.g. Make these things coincident. + Relation { kind: RelationKind, segment_ids: Vec }, + // If segment2 is given, it's the perpendicular distance between them. + Dimension { + segment1_id: String, + segment2_id: Option, + value: Number, + }, + Angle { + segment1_id: String, + segment2_id: Option, + value: Number, + }, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)] +#[ts(export)] +pub enum RelationKind { + Coincidence, + Horizontal, + Vertical, + Tangent, + Equal, + Fixed, +} + +// Stub implementation of SketchApi for testing/development +pub struct SketchApiStub; + +impl SketchApi for SketchApiStub { + async fn new_sketch( + &self, + _project: ProjectId, + _file: FileId, + _version: Version, + _args: SketchArgs, + ) -> Result<(SourceDelta, SceneGraphDelta, ObjectId)> { + todo!("new_sketch not implemented") + } + + async fn edit_sketch( + &self, + _project: ProjectId, + _file: FileId, + _version: Version, + _sketch: ObjectId, + ) -> Result { + todo!("edit_sketch not implemented") + } + + async fn exit_sketch(&self, _version: Version, _sketch: ObjectId) -> Result { + todo!("exit_sketch not implemented") + } + + async fn add_segment( + &self, + _version: Version, + _sketch: ObjectId, + _segment: SegmentCtor, + _label: Option, + ) -> Result<(KclSource, SketchExecOutcome)> { + // Return empty stub data + Ok(( + KclSource { + text: String::new(), + }, + SketchExecOutcome { + segments: Vec::new(), + constraints: Vec::new(), + }, + )) + } + + async fn edit_segment( + &self, + _version: Version, + _sketch: ObjectId, + _segment_id: ObjectId, + _segment: SegmentCtor, + ) -> Result<(SourceDelta, SceneGraphDelta)> { + todo!("edit_segment not implemented") + } + + async fn delete_segment( + &self, + _version: Version, + _sketch: ObjectId, + _segment_id: ObjectId, + ) -> Result<(SourceDelta, SceneGraphDelta)> { + todo!("delete_segment not implemented") + } + + async fn add_constraint( + &self, + _version: Version, + _sketch: ObjectId, + _constraint: Constraint, + ) -> Result<(SourceDelta, SceneGraphDelta)> { + todo!("add_constraint not implemented") + } + + async fn edit_constraint( + &self, + _version: Version, + _sketch: ObjectId, + _constraint_id: ObjectId, + _constraint: Constraint, + ) -> Result<(SourceDelta, SceneGraphDelta)> { + todo!("edit_constraint not implemented") + } + + async fn delete_constraint( + &self, + _version: Version, + _sketch: ObjectId, + _constraint_id: ObjectId, + ) -> Result<(SourceDelta, SceneGraphDelta)> { + todo!("delete_constraint not implemented") + } +} \ No newline at end of file diff --git a/rust/kcl-wasm-lib/src/api.rs b/rust/kcl-wasm-lib/src/api.rs index 26c1cb3b0d8..015dd435a5f 100644 --- a/rust/kcl-wasm-lib/src/api.rs +++ b/rust/kcl-wasm-lib/src/api.rs @@ -1,5 +1,6 @@ use gloo_utils::format::JsValueSerdeExt; -use kcl_api::{Error, File, FileId, LifecycleApi, ProjectId}; +use kcl_api::sketch::{SegmentCtor, SketchApi, SketchApiStub, SketchExecOutcome}; +use kcl_api::{Error, File, FileId, KclSource, LifecycleApi, ObjectId, ProjectId, Version}; use wasm_bindgen::prelude::*; use crate::Context; @@ -71,4 +72,27 @@ impl Context { .await .map_err(|e: Error| JsValue::from_serde(&e).unwrap()) } + + #[wasm_bindgen] + pub async fn add_segment( + &self, + version: usize, + sketch: usize, + segment: &str, + label: Option, + ) -> Result { + console_error_panic_hook::set_once(); + + let segment: SegmentCtor = serde_json::from_str(segment) + .map_err(|e| JsValue::from_serde(&Error::deserialize("segment", e)).unwrap())?; + + // For now, use the stub implementation + let sketch_api = SketchApiStub; + let result: (KclSource, SketchExecOutcome) = sketch_api + .add_segment(Version(version), ObjectId(sketch), segment, label) + .await + .map_err(|e: Error| JsValue::from_serde(&e).unwrap())?; + + Ok(JsValue::from_str(&serde_json::to_string(&result).unwrap())) + } } diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 2dd53328624..3d3f3765324 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -4,12 +4,14 @@ set -euo pipefail rm -rf rust/kcl-wasm-lib/pkg mkdir -p rust/kcl-wasm-lib/pkg rm -rf rust/kcl-lib/bindings +rm -rf rust/kcl-api/bindings cd rust export RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build kcl-wasm-lib --release --target web --out-dir pkg export RUSTFLAGS='' cargo test -p kcl-lib --features artifact-graph export_bindings +cargo test -p kcl-api export_bindings cd .. cp rust/kcl-wasm-lib/pkg/kcl_wasm_lib_bg.wasm public diff --git a/src/lib/rustContext.ts b/src/lib/rustContext.ts index b6296302d19..4b1b876b2e0 100644 --- a/src/lib/rustContext.ts +++ b/src/lib/rustContext.ts @@ -7,6 +7,9 @@ import type { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError' import type { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd' import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Program } from '@rust/kcl-lib/bindings/Program' +import type { SegmentCtor } from '@rust/kcl-lib/bindings/SegmentCtor' +import type { SketchExecOutcome } from '@rust/kcl-api/bindings/SketchExecOutcome' +import type { KclSource } from '@rust/kcl-api/bindings/KclSource' import { type Context } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib' import { BSON } from 'bson' @@ -239,6 +242,32 @@ export default class RustContext { } } + /** Add a segment to a sketch. */ + async addSegment( + version: number, + sketch: number, + segment: SegmentCtor, + label?: string + ): Promise<{ + kclSource: KclSource + sketchExecOutcome: SketchExecOutcome + }> { + const instance = this._checkInstance() + + try { + const result = await instance.add_segment( + version, + sketch, + JSON.stringify(segment), + label + ) + return JSON.parse(result) + } catch (e: any) { + const err = errFromErrWithOutputs(e) + return Promise.reject(err) + } + } + /** Helper to check if context instance exists */ private _checkInstance(): Context { if (!this.ctxInstance) { diff --git a/src/machines/sketchSolve/sketchSolveMode.ts b/src/machines/sketchSolve/sketchSolveMode.ts index 3c216293554..910b81086ea 100644 --- a/src/machines/sketchSolve/sketchSolveMode.ts +++ b/src/machines/sketchSolve/sketchSolveMode.ts @@ -7,14 +7,12 @@ import { setup, } from 'xstate' import type { ActorRefFrom } from 'xstate' -import { modelingMachineDefaultContext } from '@src/machines/modelingSharedContext' -import type { - ModelingMachineContext, - SetSelections, -} from '@src/machines/modelingSharedTypes' +import type { SetSelections } from '@src/machines/modelingSharedTypes' import { machine as centerRectTool } from '@src/machines/sketchSolve/tools/centerRectTool' import { machine as dimensionTool } from '@src/machines/sketchSolve/tools/dimensionTool' import { machine as pointTool } from '@src/machines/sketchSolve/tools/pointTool' +import type { SketchExecOutcome } from '@rust/kcl-api/bindings/SketchExecOutcome' +import type { KclSource } from '@rust/kcl-api/bindings/KclSource' const equipTools = Object.freeze({ centerRectTool, @@ -41,10 +39,21 @@ export type SketchSolveMachineEvent = | { type: 'unequip tool' } | { type: 'equip tool'; data: { tool: EquipTool } } | { type: typeof CHILD_TOOL_DONE_EVENT } + | { + type: 'update sketch outcome' + data: { + kclSource: KclSource + sketchExecOutcome: SketchExecOutcome + } + } -type SketchSolveContext = ModelingMachineContext & { +type SketchSolveContext = { sketchSolveToolName: EquipTool | null pendingToolName?: EquipTool + sketchExecOutcome?: { + kclSource: KclSource + sketchExecOutcome: SketchExecOutcome + } } export const sketchSolveMachine = setup({ @@ -72,6 +81,10 @@ export const sketchSolveMachine = setup({ type: 'sketch solve tool changed', data: { tool: null }, }), + 'update sketch outcome': assign(({ event }) => { + assertEvent(event, 'update sketch outcome') + return { sketchExecOutcome: event.data } + }), 'spawn tool': assign(({ event, spawn, context }) => { // Determine which tool to spawn based on event type let nameOfToolToSpawn: EquipTool @@ -105,9 +118,8 @@ export const sketchSolveMachine = setup({ ...equipTools, }, }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QGUDWYAuBjAFgAmQHsAbANzDwFlCIwBiMADwEsMBtABgF1FQAHQrFbNCAO14hGiAIwB2ABwBWAHTyAbAGZFAFiXyOAJg0BOADQgAnolkrpHQwe1rFz59I0BfD+bSZcBEnIqGnoAVz4IAEMMClgwYjAsDBFRTh4kEAEhZLEJKQRZY1lVWWk1YzV5A3ltDg1tcysEAFoDI2UDWQ4lNWk5A0Uqz28QX2x8IjIKalo6UNEwAEdQ5j48DEISNIks4VyM-O1FFRqB2Sd6jlk1BstEVo1pZQ1e+Rt5eWlB0u0vH3RxgEpsFaMoALaEIKRUQQPBxBJJOgQMRgZTMUSkQjocGQsAAFU2xG2GV2OXEBxkBmMTw0L06ig42jkZjuBTUykGBjUN0G3Q0jL+owB-kmQRmqIhUJhcPiiQwDAATgrCArlHxiNEAGYqsE48gErbcHaCPbk0D5aRUgzKexVV6yc7aAaNSnaVRcz7lakuDiWwVjEWBaYhPUUaGw+FyhjLVbrQnE-gmsl5GR2G2FRTGeQaapFY7yF0IAzuVSDYx1SptRRc-3CiZBkGo0JCURQOMkOYLGNrDaG9KJ7IpFMIOzcm19NTFqnSYwaT6FjTnZS6XqyAxXLpz5y1vz14Hi5TN9Ft3vEaMrHvxo0kpNDikIWfFTo1IpGF-OQsMt05xwcTTdQpZGGf5dyBMUQ1gAB3VhcGPdsz0YWAMGiVFkQWZRIiSFVlFPBNMlvfZzRkR4OUfRRrgZQZ1ELbQumUZxtEY7QTHXZjgKFUDRWDUF5iWC84NPOhEOQmJlDQ1FMI2VVcOvAdTWHOwmRtQYmX5IwgKAws5Dda5LV0P9pB6LQvBGUQQngDIAz3cDaGNQdCMkRBJ3TYxM2zXMbEGQtmktZ5jCKB0uRcAYqR3QEuMbUM8HDGUEQwOz5PvRcn2MTpix9NpnVZOxrQUKkHH5BlZDCwN9xDI9W3ghLk3vWoOGeNQ1w4HQ7EURdFAXGcOXURdaRMMoc3YqywO41EoJgnABMJaq7yIotjDdTMuXKdxaWpAtWUnYpGLeep6nXa4DBK6zRuUJhhFbGaHItFdl2uc4TAW7kikLfz3UqRlPIWxx5GOkbIt47sppIK6zUch8BmUQpqkeaQnRUjamk6J56Q+c4QocEyPCAA */ - context: ({ input }): SketchSolveContext => ({ - ...modelingMachineDefaultContext, + /** @xstate-layout N4IgpgJg5mDOIC5QGUDWYAuBjAFgAmQHsAbANzDwFlCIwBiMADwEsMBtABgF1FQAHQrFbNCAO14hGiAIwB2ABwBWAHTyAbAGZFAFiXyOAJg0BOADQgAnolkrpHQwe1rFz59I0BfD+bSZcBEnIqGnoAVz4IAEMMClgwYjAsDBFRTh4kEAEhZLEJKQRZY1lVWWk1YzV5A3ltDg1tcysEAFoDI2UDWQ4lNWk5A0Uqz28QX2x8IjIKalo6cKiYvFh0cbxCUOxCAFswNIks4VyM-OkBg2VjU40a6-ljF0VGxFbpY2Uug0ubTp07rx8Vv5JkEZmFRGAAI6hZh8PAYQgkPYZA45cTHRDaRQqGoDWROeocWRqBqWZ5GaTKDS9eQ2eTyaSDUraf6jQETQLTELKLaEIKRUQQJbxRIYOgQMRgZTMUSkQjobm8sAAFQRxCR-EEhzRoBOnwpGipPw42jkZlJBTUykGBjUxMG3Q0xpZYyBHOCtAVfIFQoSSQYACd-YR-co+MRogAzYNbT3K1XqzKa1F5GSfc72KrU2R47QDJ4IU7aVQ2+nlS4uDinZ1sgJTd2SnlewVxX2iyHQ2HwxHcfZJlIpgt2ZSE+7GeQaapFLHyfMGdyqQbGOqVNqKG3Vvzsuug5ShISiKBw1VzcFQmFH7vpDXZfvogvG+SUsp49RdA3E-NaItY4y-3Mm20nRGF0txBLk92lQ8u2IBgz07eMe2RPsjh1RAigpT46m0XQTFkG01HzbQFHeLFZA0BQ6kUeppA3VZgU5D1YAAd1YXBIIvGDGFgDBoklcVwWUSIkmDZQExRW9UIQIpzjuCp9ExJdygI81iUfBk8InKi1AUOdaNdbdwNPDs+HY6C6C4niYmUfjJSE+EQzE5DtUkRAXEfFw83NBRimMXQyO0eoDEJNQDC8EZRBCeAMhA2swNoXsbxQlyEBC4dCkUMcJzuGxBnzZpTkpX9s0KXRn06PTQIYhtFTwflm2FJIEq1AdyOKNM8IZNQHE8po7HOHSlzaR1FEJCrYqq3d9yg1UmuTO8TBUapyXUaodEec1HXczpNC0NbFDG+j62UZjWJwUyZqQxLnPyT5v2MfDLgNExpBnFTOmUbCaXqQLgtC4Ca0OncmGEA9Zok5LpF0S0iO0gK-1tIp81-YtKmNHLfMceQDrdHdQiMmETIPDiwaS3VyOUV5ekqDKurKfMsSLUobAZRRCiqRQaLCoA */ + context: (): SketchSolveContext => ({ sketchSolveToolName: null, }), id: 'Sketch Solve Mode', @@ -127,6 +139,11 @@ export const sketchSolveMachine = setup({ description: 'sketch mode consumes the current selection from its source of truth (currently modelingMachine). Whenever it receives', }, + 'update sketch outcome': { + actions: 'update sketch outcome', + description: + 'Updates the sketch execution outcome in the context when tools complete operations', + }, 'unequip tool': { actions: 'send unequip to tool', }, diff --git a/src/machines/sketchSolve/tools/pointTool.ts b/src/machines/sketchSolve/tools/pointTool.ts index 835e9704fef..c3eab07bcfb 100644 --- a/src/machines/sketchSolve/tools/pointTool.ts +++ b/src/machines/sketchSolve/tools/pointTool.ts @@ -1,9 +1,24 @@ -import { fromPromise, setup } from 'xstate' +import { assertEvent, fromPromise, setup } from 'xstate' + +import { sceneInfra, rustContext } from '@src/lib/singletons' +import type { SegmentCtor } from '@rust/kcl-lib/bindings/SegmentCtor' +import type { KclSource } from '@rust/kcl-api/bindings/KclSource' +import type { SketchExecOutcome } from '@rust/kcl-api/bindings/SketchExecOutcome' + +const CONFIRMING_DIMENSIONS = 'Confirming dimensions' +const CONFIRMING_DIMENSIONS_DONE = `xstate.done.actor.0.Point tool.${CONFIRMING_DIMENSIONS}` type PointEvent = | { type: 'unequip' } | { type: 'add point'; data: [x: number, y: number] } | { type: 'update selection' } + | { + type: `xstate.done.actor.0.Point tool.${typeof CONFIRMING_DIMENSIONS}` + output: { + kclSource: KclSource + sketchExecOutcome: SketchExecOutcome + } + } export const machine = setup({ types: { @@ -11,9 +26,24 @@ export const machine = setup({ events: {} as PointEvent, }, actions: { - 'add point listener': () => { - // Add your action code here - // ... + 'add point listener': ({ self }) => { + console.log('Point tool ready for user click') + + sceneInfra.setCallbacks({ + onClick: (args) => { + if (!args) return + if (args.mouseEvent.which !== 1) return // Only left click + + const twoD = args.intersectionPoint?.twoD + if (twoD) { + // Send the add point event with the clicked coordinates + self.send({ + type: 'add point', + data: [twoD.x, twoD.y] as [number, number], + }) + } + }, + }) }, 'show draft geometry': () => { // Add your action code here @@ -21,12 +51,59 @@ export const machine = setup({ }, 'remove point listener': () => { console.log('should be exiting point tool now') - // Add your action code here - // ... + // Reset callbacks to remove the onClick listener + sceneInfra.setCallbacks({ + onClick: () => {}, + }) + }, + 'send result to parent': ({ event, self }) => { + if (event.type !== CONFIRMING_DIMENSIONS_DONE) { + return + } + self._parent?.send({ + type: 'update sketch outcome', + data: event.output, + }) }, }, actors: { - modAndSolve: fromPromise(async () => {}), + modAndSolve: fromPromise( + async ({ input }: { input: { pointData: [number, number] } }) => { + const { pointData } = input + const [x, y] = pointData + + try { + // TODO not sure if we should be sending through units with this + const segmentCtor: SegmentCtor = { + Point: { + position: { + x: { Number: { value: x, units: 'Mm' } } as any, + y: { Number: { value: y, units: 'Mm' } } as any, + }, + }, + } + + console.log('Adding point segment:', segmentCtor) + + // Call the addSegment method using the singleton rustContext + const result = await rustContext.addSegment( + 1, // version - TODO: Get this from actual context + 0, // sketchId - TODO: Get this from actual context + segmentCtor, + 'point-tool-point' // label + ) + + console.log('Point segment added successfully:', result) + + return result + } catch (error) { + console.error('Failed to add point segment:', error) + return { + error: error instanceof Error ? error.message : 'Unknown error', + } + } + } + ), }, }).createMachine({ /** @xstate-layout N4IgpgJg5mDOIC5QAUD2BLAdgFwATdVQBsBiAV0zAEcz0AHAbQAYBdRUO1WdbdVTdiAAeiAIwAmcaIB0AZgCcANlniAHAHZZAVkXilAGhABPRABZp61VvWnFW8Uy0KdogL6vDaLHgLFydCABDbDBcWDAiMABjXn5mNiQQTm5YgUSRBAVxaUVRJlU1JwL5UWtDEwQAWkLpJiZxbVN1ayY7VUV3TwwcfEIiaQBhfgAzdAAnAFssKFwIdAmwTG5+WBIIfjBpLAA3VABrTa8e336hzFHJ6dn5xeWlhB3UKOC+THj4wWSeV8EMnXlamolPI6g5lKZyogQdI1M1tIpTKokQpZJ0QEcfH1BiNxlNMDM5gslq9VmAxmNUGNpHQiMFhpSJtIMb1iNjzriroTbiSHphds9Uu9WJ8uN9+L8xI5VNJ2op1HLwTokZCEKJ2jDTPItaomEpVKJ5Fo0cyTtIxmBAhAjLh6WNcGRwnaokR0FE9iRLRBcJxvB9El9UhKELppYa1KVSvlVAoVWrzM06rII5otR00ZhUBA4IITX0RSkfulEEn1BYwYjHFJROpxOoVZUJKWpKZxLktPJZDX1ExTMbupjWebLdbbfbHbhna69vmxWlQBlZHYy60K-ZRNXa7HcjCtKCWoiky2+94WaccZd8dciXd4P7RYGiwgCjJxKZZKZq+3HLrRFuX7v6nUDtVC1bVj2OLEKGoWg6DoaYZwfecxFrAEGkkBEezyeR5VjUxzHEACmEbYpTHsdx3CAA */ @@ -49,20 +126,27 @@ export const machine = setup({ entry: 'add point listener', on: { - 'add point': 'Confirming dimensions', + 'add point': CONFIRMING_DIMENSIONS, }, }, - 'Confirming dimensions': { + [CONFIRMING_DIMENSIONS]: { invoke: { - input: {}, + input: ({ event }) => { + assertEvent(event, 'add point') + return { pointData: event.data } + }, onDone: { target: 'ready for user click', reenter: true, + actions: 'send result to parent', }, onError: { target: 'unequipping', }, + onExit: { + actions: 'send result to parent', + }, src: 'modAndSolve', }, },