both edit and move in one PR (#566)

* get the data for where lines are

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make pretty

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* new shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* beginning of stufff

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add new fns

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* basic function

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix ups to keep order

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* further

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* failing test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* do it in rust

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* trait

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of ui integration

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* weird shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* generate close on close

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of constraint functions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* helper functions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* constraints w ranges

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* skip

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* comment

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* throw

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make close a bit less sensitive in move scenario

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup shit we didnt end up using

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make it less hard to close

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Fix edit after sketch

* Move to plane for sketch

* Fix pathToNode for ast mods

* Fix exit sketch mode with escape

* Fix fmt since my editor did it wrong

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix link

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Adam Sunderland <adam@kittycad.io>
This commit is contained in:
Jess Frazelle
2023-09-17 21:57:43 -07:00
committed by GitHub
parent f71f44968b
commit 5297d3e142
32 changed files with 1457 additions and 162 deletions

View File

@ -55,7 +55,7 @@ jobs:
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo test --all
cargo nextest run --workspace --no-fail-fast -P ci
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

View File

@ -47,6 +47,7 @@ export function App() {
streamDimensions,
guiMode,
setGuiMode,
executeAst,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
@ -57,6 +58,7 @@ export function App() {
setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream,
streamDimensions: s.streamDimensions,
executeAst: s.executeAst,
}))
const {
@ -87,12 +89,23 @@ export function App() {
if (guiMode.mode === 'sketch') {
if (guiMode.sketchMode === 'selectFace') return
if (guiMode.sketchMode === 'sketchEdit') {
// TODO: share this with Toolbar's "Exit sketch" button
// exiting sketch should be done consistently across all exits
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
setGuiMode({ mode: 'default' })
// this is necessary to get the UI back into a consistent
// state right now, hopefully won't need to rerender
// when exiting sketch mode in the future
executeAst()
} else {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
@ -108,6 +121,7 @@ export function App() {
rotation: guiMode.rotation,
position: guiMode.position,
pathToNode: guiMode.pathToNode,
pathId: guiMode.pathId,
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
})
}

View File

@ -109,21 +109,17 @@ export const Toolbar = () => {
{guiMode.mode === 'canEditSketch' && (
<button
onClick={() => {
console.log('guiMode.pathId', guiMode.pathId)
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: guiMode.pathId,
},
})
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
pathToNode: guiMode.pathToNode,
rotation: guiMode.rotation,
position: guiMode.position,
sketchMode: 'enterSketchEdit',
pathToNode: pathToNode,
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathId: guiMode.pathId,
})
}}
className="group"
@ -240,6 +236,7 @@ export const Toolbar = () => {
sketchMode: sketchFnName,
waitingFirstClick: true,
isTooltip: true,
pathId: guiMode.pathId,
}),
})
}}

View File

@ -40,12 +40,12 @@ const DownloadAppBanner = () => {
</code>
, and isn't backed up anywhere! Visit{' '}
<a
href="https://github.com/KittyCAD/modeling-app/releases"
href="https://kittycad.io/modeling-app/download"
rel="noopener noreferrer"
target="_blank"
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
>
our GitHub repository
our website
</a>{' '}
to download the app for the best experience.
</p>

View File

@ -21,6 +21,10 @@ import {
} from 'lang/std/sketch'
import { getNodeFromPath } from 'lang/queryAst'
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
import { modify_ast_for_sketch } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from 'lang/errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from 'lang/abstractSyntaxTree'
export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true)
@ -211,14 +215,9 @@ export const Stream = ({ className = '' }) => {
}
}
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
if (command?.cmd?.type !== 'mouse_click' || !ast) return
if (
!(
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any as 'line')
)
)
return
if (!(guiMode.mode === 'sketch')) return
if (guiMode.sketchMode === 'selectFace') return
// Check if the sketch group already exists.
const varDec = getNodeFromPath<VariableDeclarator>(
@ -230,6 +229,56 @@ export const Stream = ({ className = '' }) => {
const sketchGroup = programMemory.root[variableName]
const isEditingExistingSketch =
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
let sketchGroupId = ''
if (sketchGroup && sketchGroup.type === 'SketchGroup') {
sketchGroupId = sketchGroup.id
}
if (
guiMode.sketchMode === ('move' as any as 'line') &&
command.cmd.type === 'handle_mouse_drag_end'
) {
// Let's get the updated ast.
if (sketchGroupId === '') return
console.log('guiMode.pathId', guiMode.pathId)
// We have a problem if we do not have an id for the sketch group.
if (
guiMode.pathId === undefined ||
guiMode.pathId === null ||
guiMode.pathId === ''
)
return
let engineId = guiMode.pathId
try {
const updatedAst: Program = await modify_ast_for_sketch(
engineCommandManager,
JSON.stringify(ast),
variableName,
engineId
)
updateAst(updatedAst, false)
} catch (e: any) {
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)
throw kclError
}
return
}
if (command?.cmd?.type !== 'mouse_click' || !ast) return
if (!(guiMode.sketchMode === ('sketch_line' as any as 'line'))) return
if (
resp?.data?.data?.entities_modified?.length &&
@ -257,6 +306,16 @@ export const Stream = ({ className = '' }) => {
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
// We need to update the guiMode with the right pathId so that we can
// move lines later and send the right sketch id to the engine.
for (const [id, artifact] of Object.entries(
engineCommandManager.artifactMap
)) {
if (artifact.commandType === 'start_path') {
guiMode.pathId = id
}
}
setGuiMode({
...guiMode,
pathToNode: _pathToNode,
@ -315,9 +374,12 @@ export const Stream = ({ className = '' }) => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_disable',
},
cmd: { type: 'edit_mode_exit' },
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
updateAst(_modifiedAst, true)
}

View File

@ -37,27 +37,62 @@ export function useAppMode() {
guiMode.sketchMode === 'selectFace' &&
engineCommandManager
) {
const createAndShowPlanes = async () => {
let localDefaultPlanes: DefaultPlanes
if (!defaultPlanes) {
const xy = createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
})
// TODO re-enable
// const yz = createPlane(engineCommandManager, {
// x_axis: { x: 0, y: 1, z: 0 },
// y_axis: { x: 0, y: 0, z: 1 },
// color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
// })
// const xz = createPlane(engineCommandManager, {
// x_axis: { x: 1, y: 0, z: 0 },
// y_axis: { x: 0, y: 0, z: 1 },
// color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
// })
setDefaultPlanes({ xy })
localDefaultPlanes = await initDefaultPlanes(engineCommandManager)
setDefaultPlanes(localDefaultPlanes)
} else {
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, false)
localDefaultPlanes = defaultPlanes
}
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, false)
}
createAndShowPlanes()
}
if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === 'enterSketchEdit' &&
engineCommandManager
) {
const enableSketchMode = async () => {
let localDefaultPlanes: DefaultPlanes
if (!defaultPlanes) {
localDefaultPlanes = await initDefaultPlanes(engineCommandManager)
setDefaultPlanes(localDefaultPlanes)
} else {
localDefaultPlanes = defaultPlanes
}
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, true)
// TODO figure out the plane to use based on the sketch
// maybe it's easier to make a new plane than rely on the defaults
await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_enable',
plane_id: localDefaultPlanes.xy,
ortho: true,
animated: !isReducedMotion(),
},
})
const proms: any[] = []
proms.push(
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: guiMode.pathId,
},
})
)
await Promise.all(proms)
}
enableSketchMode()
setGuiMode({
...guiMode,
sketchMode: 'sketchEdit',
})
}
if (guiMode.mode !== 'sketch' && defaultPlanes) {
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
@ -151,6 +186,7 @@ export function useAppMode() {
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: [],
pathId: sketchUuid,
})
console.log('sketchModeResponse', sketchModeResponse)
@ -160,7 +196,7 @@ export function useAppMode() {
}, [engineCommandManager, defaultPlanes])
}
function createPlane(
async function createPlane(
engineCommandManager: EngineCommandManager,
{
x_axis,
@ -173,7 +209,7 @@ function createPlane(
}
) {
const planeId = uuidv4()
engineCommandManager?.sendSceneCommand({
await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'make_plane',
@ -185,7 +221,7 @@ function createPlane(
},
cmd_id: planeId,
})
engineCommandManager?.sendSceneCommand({
await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'plane_set_color',
@ -215,6 +251,28 @@ function setDefaultPlanesHidden(
})
}
async function initDefaultPlanes(
engineCommandManager: EngineCommandManager
): Promise<DefaultPlanes> {
const xy = await createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
})
// TODO re-enable
// const yz = createPlane(engineCommandManager, {
// x_axis: { x: 0, y: 1, z: 0 },
// y_axis: { x: 0, y: 0, z: 1 },
// color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
// })
// const xz = createPlane(engineCommandManager, {
// x_axis: { x: 1, y: 0, z: 0 },
// y_axis: { x: 0, y: 0, z: 1 },
// color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
// })
return { xy }
}
function isCursorInSketchCommandRange(
artifactMap: ArtifactMap,
selectionRanges: Selections

View File

@ -15,9 +15,13 @@ interface CommandInfo {
range: SourceRange
parentId?: string
}
type WebSocketResponse = Models['OkWebSocketResponseData_type']
interface ResultCommand extends CommandInfo {
type: 'result'
data: any
raw: WebSocketResponse
}
interface PendingCommand extends CommandInfo {
type: 'pending'
@ -37,8 +41,6 @@ interface NewTrackArgs {
mediaStream: MediaStream
}
type WebSocketResponse = Models['OkWebSocketResponseData_type']
type ClientMetrics = Models['ClientMetrics_type']
// EngineConnection encapsulates the connection(s) to the Engine
@ -652,12 +654,14 @@ export class EngineCommandManager {
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse,
raw: message,
}
resolve({
id,
commandType: command.commandType,
range: command.range,
data: modelingResponse,
raw: message,
})
} else {
this.artifactMap[id] = {
@ -665,6 +669,7 @@ export class EngineCommandManager {
commandType: command?.commandType,
range: command?.range,
data: modelingResponse,
raw: message,
}
}
}
@ -873,7 +878,10 @@ export class EngineCommandManager {
}
const range: SourceRange = JSON.parse(rangeStr)
return this.sendModelingCommand({ id, range, command: commandStr })
// We only care about the modeling command response.
return this.sendModelingCommand({ id, range, command: commandStr }).then(
({ raw }) => JSON.stringify(raw)
)
}
commandResult(id: string): Promise<any> {
const command = this.artifactMap[id]
@ -943,7 +951,6 @@ export class EngineCommandManager {
pathInfos.forEach(({ originalId, segments }) => {
const originalArtifact = this.artifactMap[originalId]
if (!originalArtifact || originalArtifact.type === 'pending') {
console.log('problem')
return
}
const pipeExpPath = getNodePathFromSourceRange(
@ -956,23 +963,20 @@ export class EngineCommandManager {
'VariableDeclarator'
).node
if (pipeExp.type !== 'VariableDeclarator') {
console.log('problem', pipeExp, pipeExpPath, ast)
return
}
const variableName = pipeExp.id.name
const memoryItem = programMemory.root[variableName]
if (!memoryItem) {
console.log('problem', variableName, programMemory)
return
} else if (memoryItem.type !== 'SketchGroup') {
console.log('problem', memoryItem, programMemory)
return
}
const relevantSegments = segments.filter(
({ command_id }: { command_id: string | null }) => command_id
)
if (memoryItem.value.length !== relevantSegments.length) {
console.log('problem', memoryItem.value, relevantSegments)
return
}
for (let i = 0; i < relevantSegments.length; i++) {
@ -982,9 +986,11 @@ export class EngineCommandManager {
const artifact = this.artifactMap[oldId]
delete this.artifactMap[oldId]
delete this.sourceRangeMap[oldId]
if (artifact) {
this.artifactMap[engineSegment.command_id] = artifact
this.sourceRangeMap[engineSegment.command_id] = artifact.range
}
}
})
}
}

View File

@ -117,6 +117,7 @@ show(mySketch001)
{
mode: 'sketch',
sketchMode: 'sketchEdit',
pathId: '',
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: [

View File

@ -71,7 +71,7 @@ export type GuiModes =
waitingFirstClick: boolean
rotation: Rotation
position: Position
id?: string
pathId: string
pathToNode: PathToNode
}
| {
@ -80,6 +80,15 @@ export type GuiModes =
rotation: Rotation
position: Position
pathToNode: PathToNode
pathId: string
}
| {
mode: 'sketch'
sketchMode: 'enterSketchEdit'
rotation: Rotation
position: Position
pathToNode: PathToNode
pathId: string
}
| {
mode: 'sketch'

View File

@ -0,0 +1,22 @@
# Each test can have at most 4 threads, but if its name contains "serial_test_", then it
# also requires 4 threads.
# This means such tests run one at a time, with 4 threads.
[test-groups]
serial-integration = { max-threads = 4 }
[profile.default]
slow-timeout = { period = "10s", terminate-after = 1 }
[profile.ci]
slow-timeout = { period = "60s", terminate-after = 10 }
[[profile.default.overrides]]
filter = "test(serial_test_)"
test-group = "serial-integration"
threads-required = 4
[[profile.ci.overrides]]
filter = "test(serial_test_)"
test-group = "serial-integration"
threads-required = 4

View File

@ -1310,9 +1310,10 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.28"
version = "0.1.29"
dependencies = [
"anyhow",
"async-trait",
"bson",
"clap",
"dashmap",
@ -1338,6 +1339,7 @@ dependencies = [
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
@ -3441,6 +3443,7 @@ dependencies = [
"js-sys",
"kcl-lib",
"kittycad",
"pretty_assertions",
"reqwest",
"serde_json",
"tokio",

View File

@ -13,6 +13,7 @@ gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
serde_json = "1.0.107"
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
@ -20,6 +21,7 @@ wasm-bindgen-futures = "0.4.37"
anyhow = "1"
image = "0.24.7"
kittycad = "0.2.25"
pretty_assertions = "1.4.0"
reqwest = { version = "0.11.20", default-features = false }
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.6.1"
@ -50,3 +52,11 @@ members = [
"derive-docs",
"kcl",
]
[[test]]
name = "executor"
path = "tests/executor/main.rs"
[[test]]
name = "modify"
path = "tests/modify/main.rs"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language"
version = "0.1.28"
version = "0.1.29"
edition = "2021"
license = "MIT"
@ -9,6 +9,7 @@ license = "MIT"
[dependencies]
anyhow = { version = "1.0.75", features = ["backtrace"] }
async-trait = "0.1.73"
clap = { version = "4.4.3", features = ["cargo", "derive", "env", "unicode"] }
dashmap = "5.5.3"
derive-docs = { version = "0.1.3" }
@ -29,6 +30,7 @@ js-sys = { version = "0.3.64" }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
web-sys = { version = "0.3.64", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }

View File

@ -0,0 +1,2 @@
pub mod modify;
pub mod types;

View File

@ -0,0 +1,293 @@
use kittycad::types::{ModelingCmd, Point3D};
use super::types::ConstraintLevel;
use crate::{
ast::types::{
ArrayExpression, CallExpression, FormatOptions, Literal, PipeExpression, PipeSubstitution, Program,
VariableDeclarator,
},
engine::{EngineConnection, EngineManager},
errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange},
};
#[derive(Debug)]
/// The control point data for a curve or line.
pub struct ControlPointData {
/// The control points for the curve or line.
pub points: Vec<kittycad::types::Point3D>,
/// The command that created this curve or line.
pub command: kittycad::types::PathCommand,
/// The id of the curve or line.
pub id: uuid::Uuid,
}
const EPSILON: f64 = 0.015625; // or 2^-6
/// Update the AST to reflect the new state of the program after something like
/// a move or a new line.
pub async fn modify_ast_for_sketch(
engine: &mut EngineConnection,
program: &mut Program,
// The name of the sketch.
sketch_name: &str,
// The ID of the parent sketch.
sketch_id: uuid::Uuid,
) -> Result<String, KclError> {
// First we need to check if this sketch is constrained (even partially).
// If it is, we cannot modify it.
// Get the information about the sketch.
if let Some(ast_sketch) = program.get_variable(sketch_name) {
let constraint_level = ast_sketch.get_constraint_level();
match &constraint_level {
ConstraintLevel::None { source_ranges: _ } => {}
ConstraintLevel::Ignore { source_ranges: _ } => {}
ConstraintLevel::Partial {
source_ranges: _,
levels,
} => {
return Err(KclError::Engine(KclErrorDetails {
message: format!(
"Sketch {} is constrained `{}` and cannot be modified",
sketch_name, constraint_level
),
source_ranges: levels.get_all_partial_or_full_source_ranges(),
}));
}
ConstraintLevel::Full { source_ranges } => {
return Err(KclError::Engine(KclErrorDetails {
message: format!(
"Sketch {} is constrained `{}` and cannot be modified",
sketch_name, constraint_level
),
source_ranges: source_ranges.clone(),
}));
}
}
}
// Let's start by getting the path info.
// Let's get the path info.
let resp = engine
.send_modeling_cmd_get_response(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::PathGetInfo { path_id: sketch_id },
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::PathGetInfo { data: path_info },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Get path info response was not as expected: {:?}", resp),
source_ranges: vec![SourceRange::default()],
}));
};
/* // Let's try to get the children of the sketch.
let resp = engine
.send_modeling_cmd_get_response(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::EntityGetAllChildUuids { entity_id: sketch_id },
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::EntityGetAllChildUuids { data: children_info },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Get child info response was not as expected: {:?}", resp),
source_ranges: vec![SourceRange::default()],
}));
};
println!("children_info: {:#?}", children_info);
// Let's try to get the parent id.
let resp = engine
.send_modeling_cmd_get_response(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::EntityGetParentId { entity_id: sketch_id },
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::EntityGetParentId { data: parent_info },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Get parent id response was not as expected: {:?}", resp),
source_ranges: vec![SourceRange::default()],
}));
};
println!("parent_info: {:#?}", parent_info);*/
// Now let's get the control points for all the segments.
// TODO: We should probably await all these at once so we aren't going one by one.
// But I guess this is fine for now.
// We absolutely have to preserve the order of the control points.
let mut control_points = Vec::new();
for segment in &path_info.segments {
if let Some(command_id) = &segment.command_id {
let h = engine.send_modeling_cmd_get_response(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::CurveGetControlPoints { curve_id: *command_id },
);
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::CurveGetControlPoints { data },
} = h.await?
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Curve get control points response was not as expected: {:?}", resp),
source_ranges: vec![SourceRange::default()],
}));
};
control_points.push(ControlPointData {
points: data.control_points.clone(),
command: segment.command.clone(),
id: *command_id,
});
}
}
if control_points.is_empty() {
return Err(KclError::Engine(KclErrorDetails {
message: format!("No control points found for sketch {}", sketch_name),
source_ranges: vec![SourceRange::default()],
}));
}
let first_control_points = control_points.first().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("No control points found for sketch {}", sketch_name),
source_ranges: vec![SourceRange::default()],
})
})?;
let mut additional_lines = Vec::new();
let mut last_point = first_control_points.points[1].clone();
for control_point in control_points[1..].iter() {
additional_lines.push([
(control_point.points[1].x - last_point.x),
(control_point.points[1].y - last_point.y),
]);
last_point = Point3D {
x: control_point.points[1].x,
y: control_point.points[1].y,
z: control_point.points[1].z,
};
}
// Okay now let's recalculate the sketch from the control points.
let start_sketch_at_end = Point3D {
x: (first_control_points.points[1].x - first_control_points.points[0].x),
y: (first_control_points.points[1].y - first_control_points.points[0].y),
z: (first_control_points.points[1].z - first_control_points.points[0].z),
};
let sketch = create_start_sketch_at(
sketch_name,
[first_control_points.points[0].x, first_control_points.points[0].y],
[start_sketch_at_end.x, start_sketch_at_end.y],
additional_lines,
)?;
// Add the sketch back to the program.
program.replace_variable(sketch_name, sketch);
let recasted = program.recast(&FormatOptions::default(), 0);
// Re-parse the ast so we get the correct source ranges.
let tokens = crate::tokeniser::lexer(&recasted);
let parser = crate::parser::Parser::new(tokens);
*program = parser.ast()?;
Ok(recasted)
}
/// Create a pipe expression that starts a sketch at the given point and draws a line to the given point.
fn create_start_sketch_at(
name: &str,
start: [f64; 2],
end: [f64; 2],
additional_lines: Vec<[f64; 2]>,
) -> Result<VariableDeclarator, KclError> {
let start_sketch_at = CallExpression::new(
"startSketchAt",
vec![ArrayExpression::new(vec![
Literal::new(round_before_recast(start[0]).into()).into(),
Literal::new(round_before_recast(start[1]).into()).into(),
])
.into()],
)?;
// Keep track of where we are so we can close the sketch if we need to.
let mut current_position = Point2d {
x: start[0],
y: start[1],
};
current_position.x += end[0];
current_position.y += end[1];
let initial_line = CallExpression::new(
"line",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(end[0]).into()).into(),
Literal::new(round_before_recast(end[1]).into()).into(),
])
.into(),
PipeSubstitution::new().into(),
],
)?;
let mut pipe_body = vec![start_sketch_at.into(), initial_line.into()];
for (index, line) in additional_lines.iter().enumerate() {
current_position.x += line[0];
current_position.y += line[1];
// If we are on the last line, check if we have to close the sketch.
if index == additional_lines.len() - 1 {
let diff_x = (current_position.x - start[0]).abs();
let diff_y = (current_position.y - start[1]).abs();
// Compare the end of the last line to the start of the first line.
// This is a bit more lenient if you look at the value of epsilon.
if diff_x <= EPSILON && diff_y <= EPSILON {
// We have to close the sketch.
let close = CallExpression::new("close", vec![PipeSubstitution::new().into()])?;
pipe_body.push(close.into());
break;
}
}
// TODO: we should check if we should close the sketch.
let line = CallExpression::new(
"line",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(line[0]).into()).into(),
Literal::new(round_before_recast(line[1]).into()).into(),
])
.into(),
PipeSubstitution::new().into(),
],
)?;
pipe_body.push(line.into());
}
Ok(VariableDeclarator::new(name, PipeExpression::new(pipe_body).into()))
}
fn round_before_recast(num: f64) -> f64 {
(num * 100000.0).round() / 100000.0
}

View File

@ -237,6 +237,47 @@ impl Program {
}
}
}
/// Replace a variable declaration with the given name with a new one.
pub fn replace_variable(&mut self, name: &str, declarator: VariableDeclarator) {
for item in &mut self.body {
match item {
BodyItem::ExpressionStatement(_expression_statement) => {
continue;
}
BodyItem::VariableDeclaration(ref mut variable_declaration) => {
for declaration in &mut variable_declaration.declarations {
if declaration.id.name == name {
*declaration = declarator;
return;
}
}
}
BodyItem::ReturnStatement(_return_statement) => continue,
}
}
}
/// Get the variable declaration with the given name.
pub fn get_variable(&self, name: &str) -> Option<&VariableDeclarator> {
for item in &self.body {
match item {
BodyItem::ExpressionStatement(_expression_statement) => {
continue;
}
BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations {
if declaration.id.name == name {
return Some(declaration);
}
}
}
BodyItem::ReturnStatement(_return_statement) => continue,
}
}
None
}
}
pub trait ValueMeta {
@ -247,7 +288,7 @@ pub trait ValueMeta {
macro_rules! impl_value_meta {
{$name:ident} => {
impl crate::abstract_syntax_tree_types::ValueMeta for $name {
impl crate::ast::types::ValueMeta for $name {
fn start(&self) -> usize {
self.start
}
@ -426,6 +467,26 @@ impl Value {
Value::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
}
}
/// Get the constraint level for a value type.
pub fn get_constraint_level(&self) -> ConstraintLevel {
match self {
Value::Literal(literal) => literal.get_constraint_level(),
Value::Identifier(identifier) => identifier.get_constraint_level(),
Value::BinaryExpression(binary_expression) => binary_expression.get_constraint_level(),
Value::FunctionExpression(function_identifier) => function_identifier.get_constraint_level(),
Value::CallExpression(call_expression) => call_expression.get_constraint_level(),
Value::PipeExpression(pipe_expression) => pipe_expression.get_constraint_level(),
Value::PipeSubstitution(pipe_substitution) => ConstraintLevel::Ignore {
source_ranges: vec![pipe_substitution.into()],
},
Value::ArrayExpression(array_expression) => array_expression.get_constraint_level(),
Value::ObjectExpression(object_expression) => object_expression.get_constraint_level(),
Value::MemberExpression(member_expression) => member_expression.get_constraint_level(),
Value::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
}
}
}
impl From<Value> for crate::executor::SourceRange {
@ -465,6 +526,18 @@ impl From<&BinaryPart> for crate::executor::SourceRange {
}
impl BinaryPart {
/// Get the constraint level.
pub fn get_constraint_level(&self) -> ConstraintLevel {
match self {
BinaryPart::Literal(literal) => literal.get_constraint_level(),
BinaryPart::Identifier(identifier) => identifier.get_constraint_level(),
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_constraint_level(),
BinaryPart::CallExpression(call_expression) => call_expression.get_constraint_level(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
BinaryPart::MemberExpression(member_expression) => member_expression.get_constraint_level(),
}
}
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
match &self {
BinaryPart::Literal(literal) => literal.recast(),
@ -639,7 +712,7 @@ pub enum NoneCodeValue {
NewLine,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct NoneCodeMeta {
@ -698,7 +771,33 @@ pub struct CallExpression {
impl_value_meta!(CallExpression);
impl From<CallExpression> for Value {
fn from(call_expression: CallExpression) -> Self {
Value::CallExpression(Box::new(call_expression))
}
}
impl CallExpression {
pub fn new(name: &str, arguments: Vec<Value>) -> Result<Self, KclError> {
// Create our stdlib.
let stdlib = crate::std::StdLib::new();
let func = stdlib.get(name).ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Function {} is not defined", name),
source_ranges: vec![],
})
})?;
Ok(Self {
start: 0,
end: 0,
callee: Identifier::new(name),
arguments,
optional: false,
function: Function::StdLib { func },
})
}
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
format!(
"{}({})",
@ -839,6 +938,23 @@ impl CallExpression {
arg.rename_identifiers(old_name, new_name);
}
}
/// Return the constraint level for this call expression.
pub fn get_constraint_level(&self) -> ConstraintLevel {
if self.arguments.is_empty() {
return ConstraintLevel::Ignore {
source_ranges: vec![self.into()],
};
}
// Iterate over the arguments and get the constraint level for each one.
let mut constraint_levels = ConstraintLevels::new();
for arg in &self.arguments {
constraint_levels.push(arg.get_constraint_level());
}
constraint_levels.get_constraint_level(self.into())
}
}
/// A function declaration.
@ -879,6 +995,15 @@ pub struct VariableDeclaration {
impl_value_meta!(VariableDeclaration);
impl VariableDeclaration {
pub fn new(declarations: Vec<VariableDeclarator>, kind: VariableKind) -> Self {
Self {
start: 0,
end: 0,
declarations,
kind,
}
}
/// Returns a value that includes the given character position.
pub fn get_value_for_position(&self, pos: usize) -> Option<&Value> {
for declaration in &self.declarations {
@ -1059,6 +1184,21 @@ pub struct VariableDeclarator {
impl_value_meta!(VariableDeclarator);
impl VariableDeclarator {
pub fn new(name: &str, init: Value) -> Self {
Self {
start: 0,
end: 0,
id: Identifier::new(name),
init,
}
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
self.init.get_constraint_level()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -1071,7 +1211,30 @@ pub struct Literal {
impl_value_meta!(Literal);
impl From<Literal> for Value {
fn from(literal: Literal) -> Self {
Value::Literal(Box::new(literal))
}
}
impl Literal {
pub fn new(value: serde_json::Value) -> Self {
Self {
start: 0,
end: 0,
raw: value.to_string(),
value,
}
}
/// Get the constraint level for this literal.
/// Literals are always not constrained.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::None {
source_ranges: vec![self.into()],
}
}
fn recast(&self) -> String {
if let serde_json::Value::String(value) = &self.value {
let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
@ -1116,6 +1279,22 @@ pub struct Identifier {
impl_value_meta!(Identifier);
impl Identifier {
pub fn new(name: &str) -> Self {
Self {
start: 0,
end: 0,
name: name.to_string(),
}
}
/// Get the constraint level for this identifier.
/// Identifier are always fully constrained.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::Full {
source_ranges: vec![self.into()],
}
}
/// Rename all identifiers that have the old name to the new given name.
fn rename(&mut self, old_name: &str, new_name: &str) {
if self.name == old_name {
@ -1134,6 +1313,24 @@ pub struct PipeSubstitution {
impl_value_meta!(PipeSubstitution);
impl PipeSubstitution {
pub fn new() -> Self {
Self { start: 0, end: 0 }
}
}
impl Default for PipeSubstitution {
fn default() -> Self {
Self::new()
}
}
impl From<PipeSubstitution> for Value {
fn from(pipe_substitution: PipeSubstitution) -> Self {
Value::PipeSubstitution(Box::new(pipe_substitution))
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -1145,7 +1342,36 @@ pub struct ArrayExpression {
impl_value_meta!(ArrayExpression);
impl From<ArrayExpression> for Value {
fn from(array_expression: ArrayExpression) -> Self {
Value::ArrayExpression(Box::new(array_expression))
}
}
impl ArrayExpression {
pub fn new(elements: Vec<Value>) -> Self {
Self {
start: 0,
end: 0,
elements,
}
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
if self.elements.is_empty() {
return ConstraintLevel::Ignore {
source_ranges: vec![self.into()],
};
}
let mut constraint_levels = ConstraintLevels::new();
for element in &self.elements {
constraint_levels.push(element.get_constraint_level());
}
constraint_levels.get_constraint_level(self.into())
}
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
let flat_recast = format!(
"[{}]",
@ -1268,6 +1494,29 @@ pub struct ObjectExpression {
}
impl ObjectExpression {
pub fn new(properties: Vec<ObjectProperty>) -> Self {
Self {
start: 0,
end: 0,
properties,
}
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
if self.properties.is_empty() {
return ConstraintLevel::Ignore {
source_ranges: vec![self.into()],
};
}
let mut constraint_levels = ConstraintLevels::new();
for property in &self.properties {
constraint_levels.push(property.value.get_constraint_level());
}
constraint_levels.get_constraint_level(self.into())
}
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
let flat_recast = format!(
"{{ {} }}",
@ -1523,6 +1772,14 @@ pub struct MemberExpression {
impl_value_meta!(MemberExpression);
impl MemberExpression {
/// Get the constraint level for a member expression.
/// This is always fully constrained.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::Full {
source_ranges: vec![self.into()],
}
}
fn recast(&self) -> String {
let key_str = match &self.property {
LiteralIdentifier::Identifier(identifier) => {
@ -1672,6 +1929,26 @@ pub struct BinaryExpression {
impl_value_meta!(BinaryExpression);
impl BinaryExpression {
pub fn new(operator: BinaryOperator, left: BinaryPart, right: BinaryPart) -> Self {
Self {
start: left.start(),
end: right.end(),
operator,
left,
right,
}
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
let left_constraint_level = self.left.get_constraint_level();
let right_constraint_level = self.right.get_constraint_level();
let mut constraint_levels = ConstraintLevels::new();
constraint_levels.push(left_constraint_level);
constraint_levels.push(right_constraint_level);
constraint_levels.get_constraint_level(self.into())
}
pub fn precedence(&self) -> u8 {
self.operator.precedence()
}
@ -1870,6 +2147,19 @@ pub struct UnaryExpression {
impl_value_meta!(UnaryExpression);
impl UnaryExpression {
pub fn new(operator: UnaryOperator, argument: BinaryPart) -> Self {
Self {
start: 0,
end: argument.end(),
operator,
argument,
}
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
self.argument.get_constraint_level()
}
fn recast(&self, options: &FormatOptions) -> String {
format!("{}{}", &self.operator, self.argument.recast(options, 0))
}
@ -1944,7 +2234,38 @@ pub struct PipeExpression {
impl_value_meta!(PipeExpression);
impl From<PipeExpression> for Value {
fn from(pipe_expression: PipeExpression) -> Self {
Value::PipeExpression(Box::new(pipe_expression))
}
}
impl PipeExpression {
pub fn new(body: Vec<Value>) -> Self {
Self {
start: 0,
end: 0,
body,
non_code_meta: Default::default(),
}
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
if self.body.is_empty() {
return ConstraintLevel::Ignore {
source_ranges: vec![self.into()],
};
}
// Iterate over all body expressions.
let mut constraint_levels = ConstraintLevels::new();
for expression in &self.body {
constraint_levels.push(expression.get_constraint_level());
}
constraint_levels.get_constraint_level(self.into())
}
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
self.body
.iter()
@ -2063,6 +2384,13 @@ pub struct FunctionExpression {
impl_value_meta!(FunctionExpression);
impl FunctionExpression {
/// Function expressions don't really apply.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::Ignore {
source_ranges: vec![self.into()],
}
}
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
// We don't want to end with a new line inside nested functions.
let mut new_options = options.clone();
@ -2167,11 +2495,150 @@ impl FormatOptions {
}
}
/// The constraint level.
#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema, Display)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[display(style = "snake_case")]
pub enum ConstraintLevel {
/// Ignore constraints.
/// This is useful for stuff like pipe substitutions where we don't want it to
/// factor into the overall constraint level.
/// Like empty arrays or objects, etc.
#[display("ignore")]
Ignore { source_ranges: Vec<SourceRange> },
/// No constraints.
#[display("none")]
None { source_ranges: Vec<SourceRange> },
/// Partially constrained.
#[display("partial")]
Partial {
source_ranges: Vec<SourceRange>,
levels: ConstraintLevels,
},
/// Fully constrained.
#[display("full")]
Full { source_ranges: Vec<SourceRange> },
}
impl From<ConstraintLevel> for Vec<SourceRange> {
fn from(constraint_level: ConstraintLevel) -> Self {
match constraint_level {
ConstraintLevel::Ignore { source_ranges } => source_ranges,
ConstraintLevel::None { source_ranges } => source_ranges,
ConstraintLevel::Partial {
source_ranges,
levels: _,
} => source_ranges,
ConstraintLevel::Full { source_ranges } => source_ranges,
}
}
}
impl PartialEq for ConstraintLevel {
fn eq(&self, other: &Self) -> bool {
// Just check the variant.
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}
impl ConstraintLevel {
pub fn update_source_ranges(&self, source_range: SourceRange) -> Self {
match self {
ConstraintLevel::Ignore { source_ranges: _ } => ConstraintLevel::Ignore {
source_ranges: vec![source_range],
},
ConstraintLevel::None { source_ranges: _ } => ConstraintLevel::None {
source_ranges: vec![source_range],
},
ConstraintLevel::Partial {
source_ranges: _,
levels,
} => ConstraintLevel::Partial {
source_ranges: vec![source_range],
levels: levels.clone(),
},
ConstraintLevel::Full { source_ranges: _ } => ConstraintLevel::Full {
source_ranges: vec![source_range],
},
}
}
}
/// A vector of constraint levels.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ConstraintLevels(pub Vec<ConstraintLevel>);
impl Default for ConstraintLevels {
fn default() -> Self {
Self::new()
}
}
impl ConstraintLevels {
pub fn new() -> Self {
Self(vec![])
}
pub fn push(&mut self, constraint_level: ConstraintLevel) {
self.0.push(constraint_level);
}
/// Get the overall constraint level.
pub fn get_constraint_level(&self, source_range: SourceRange) -> ConstraintLevel {
if self.0.is_empty() {
return ConstraintLevel::Ignore {
source_ranges: vec![source_range],
};
}
// Check if all the constraint levels are the same.
if self
.0
.iter()
.all(|level| *level == self.0[0] || matches!(level, ConstraintLevel::Ignore { .. }))
{
self.0[0].clone()
} else {
ConstraintLevel::Partial {
source_ranges: vec![source_range],
levels: self.clone(),
}
}
}
pub fn get_all_partial_or_full_source_ranges(&self) -> Vec<SourceRange> {
let mut source_ranges = Vec::new();
// Add to our source ranges anything that is not none or ignore.
for level in &self.0 {
match level {
ConstraintLevel::None { source_ranges: _ } => {}
ConstraintLevel::Ignore { source_ranges: _ } => {}
ConstraintLevel::Partial {
source_ranges: _,
levels,
} => {
source_ranges.extend(levels.get_all_partial_or_full_source_ranges());
}
ConstraintLevel::Full {
source_ranges: full_source_ranges,
} => {
source_ranges.extend(full_source_ranges);
}
}
}
source_ranges
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
// We have this as a test so we can ensure it never panics with an unwrap in the server.
#[test]
fn test_variable_kind_to_completion() {
@ -2205,8 +2672,7 @@ show(part001)"#;
#[test]
fn test_recast_with_std_and_non_stdlib() {
let some_program_string = r#"{"body":[{"type":"VariableDeclaration","start":0,"end":0,"declarations":[{"type":"VariableDeclarator","start":0,"end":0,"id":{"type":"Identifier","start":0,"end":0,"name":"part001"},"init":{"type":"PipeExpression","start":0,"end":0,"body":[{"type":"CallExpression","start":0,"end":0,"callee":{"type":"Identifier","start":0,"end":0,"name":"startSketchAt"},"function":{"type":"StdLib","func":{"name":"startSketchAt","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}},"optional":false,"arguments":[{"type":"Literal","start":0,"end":0,"value":"default","raw":"default"}]},{"type":"CallExpression","start":0,"end":0,"callee":{"type":"Identifier","start":0,"end":0,"name":"ry"},"function":{"type":"InMemory"},"optional":false,"arguments":[{"type":"Literal","start":0,"end":0,"value":90,"raw":"90"},{"type":"PipeSubstitution","start":0,"end":0}]},{"type":"CallExpression","start":0,"end":0,"callee":{"type":"Identifier","start":0,"end":0,"name":"line"},"function":{"type":"StdLib","func":{"name":"line","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}},"optional":false,"arguments":[{"type":"Literal","start":0,"end":0,"value":"default","raw":"default"},{"type":"PipeSubstitution","start":0,"end":0}]}],"nonCodeMeta":{"noneCodeNodes":{},"start":null}}}],"kind":"const"},{"type":"ExpressionStatement","start":0,"end":0,"expression":{"type":"CallExpression","start":0,"end":0,"callee":{"type":"Identifier","start":0,"end":0,"name":"show"},"function":{"type":"StdLib","func":{"name":"show","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}},"optional":false,"arguments":[{"type":"Identifier","start":0,"end":0,"name":"part001"}]}}],"start":0,"end":0,"nonCodeMeta":{"noneCodeNodes":{},"start":null}}"#;
let some_program: crate::abstract_syntax_tree_types::Program =
serde_json::from_str(some_program_string).unwrap();
let some_program: crate::ast::types::Program = serde_json::from_str(some_program_string).unwrap();
let recasted = some_program.recast(&Default::default(), 0);
assert_eq!(

View File

@ -486,7 +486,7 @@ mod tests {
#[test]
fn test_serialize_function() {
let some_function = crate::abstract_syntax_tree_types::Function::StdLib {
let some_function = crate::ast::types::Function::StdLib {
func: Box::new(crate::std::sketch::Line),
};
let serialized = serde_json::to_string(&some_function).unwrap();
@ -496,12 +496,11 @@ mod tests {
#[test]
fn test_deserialize_function() {
let some_function_string = r#"{"type":"StdLib","func":{"name":"line","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}}"#;
let some_function: crate::abstract_syntax_tree_types::Function =
serde_json::from_str(some_function_string).unwrap();
let some_function: crate::ast::types::Function = serde_json::from_str(some_function_string).unwrap();
assert_eq!(
some_function,
crate::abstract_syntax_tree_types::Function::StdLib {
crate::ast::types::Function::StdLib {
func: Box::new(crate::std::sketch::Line),
}
);
@ -510,12 +509,11 @@ mod tests {
#[test]
fn test_deserialize_function_show() {
let some_function_string = r#"{"type":"StdLib","func":{"name":"show","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}}"#;
let some_function: crate::abstract_syntax_tree_types::Function =
serde_json::from_str(some_function_string).unwrap();
let some_function: crate::ast::types::Function = serde_json::from_str(some_function_string).unwrap();
assert_eq!(
some_function,
crate::abstract_syntax_tree_types::Function::StdLib {
crate::ast::types::Function::StdLib {
func: Box::new(crate::std::Show),
}
);

View File

@ -9,7 +9,10 @@ use futures::{SinkExt, StreamExt};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use tokio_tungstenite::tungstenite::Message as WsMsg;
use crate::errors::{KclError, KclErrorDetails};
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
};
#[derive(Debug)]
pub struct EngineConnection {
@ -70,7 +73,7 @@ impl EngineConnection {
}
Err(e) => {
println!("got ws error: {:?}", e);
continue;
return Err(e);
}
}
}
@ -89,10 +92,13 @@ impl EngineConnection {
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl EngineManager for EngineConnection {
/// Send a modeling command.
/// Do not wait for the response message.
pub fn send_modeling_cmd(
fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
@ -110,13 +116,20 @@ impl EngineConnection {
}
/// Send a modeling command and wait for the response message.
pub fn send_modeling_cmd_get_response(
async fn send_modeling_cmd_get_response(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
self.send_modeling_cmd(id, source_range, cmd)?;
self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id })
.await
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
})?;
// Wait for the response.
loop {

View File

@ -2,6 +2,7 @@
//! engine.
use anyhow::Result;
use kittycad::types::OkWebSocketResponseData;
use crate::errors::KclError;
@ -12,8 +13,11 @@ impl EngineConnection {
pub async fn new() -> Result<EngineConnection> {
Ok(EngineConnection {})
}
}
pub fn send_modeling_cmd(
#[async_trait::async_trait(?Send)]
impl crate::engine::EngineManager for EngineConnection {
fn send_modeling_cmd(
&mut self,
_id: uuid::Uuid,
_source_range: crate::executor::SourceRange,
@ -21,4 +25,13 @@ impl EngineConnection {
) -> Result<(), KclError> {
Ok(())
}
async fn send_modeling_cmd_get_response(
&mut self,
_id: uuid::Uuid,
_source_range: crate::executor::SourceRange,
_cmd: kittycad::types::ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
todo!()
}
}

View File

@ -30,8 +30,11 @@ impl EngineConnection {
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
Ok(EngineConnection { manager })
}
}
pub fn send_modeling_cmd(
#[async_trait::async_trait(?Send)]
impl crate::engine::EngineManager for EngineConnection {
fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
@ -55,4 +58,53 @@ impl EngineConnection {
.sendModelingCommandFromWasm(id.to_string(), source_range_str, cmd_str);
Ok(())
}
async fn send_modeling_cmd_get_response(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<kittycad::types::OkWebSocketResponseData, KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let ws_msg = WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id };
let cmd_str = serde_json::to_string(&ws_msg).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let promise = self
.manager
.sendModelingCommandFromWasm(id.to_string(), source_range_str, cmd_str);
let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
// Parse the value as a string.
let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get string from response from engine: `{:?}`", value),
source_ranges: vec![source_range],
})
})?;
let modeling_result: kittycad::types::OkWebSocketResponseData = serde_json::from_str(&s).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize response from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
Ok(modeling_result)
}
}

View File

@ -1,10 +1,5 @@
//! Functions for managing engine communications.
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[cfg(feature = "engine")]
use wasm_bindgen::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))]
#[cfg(feature = "engine")]
@ -31,37 +26,27 @@ pub use conn_mock::EngineConnection;
#[cfg(not(feature = "engine"))]
#[cfg(not(test))]
pub mod conn_mock;
use anyhow::Result;
#[cfg(not(feature = "engine"))]
#[cfg(not(test))]
pub use conn_mock::EngineConnection;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[derive(Debug)]
#[wasm_bindgen]
pub struct EngineManager {
connection: EngineConnection,
}
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[cfg(feature = "engine")]
#[wasm_bindgen]
impl EngineManager {
#[wasm_bindgen(constructor)]
pub async fn new(manager: conn_wasm::EngineCommandManager) -> EngineManager {
EngineManager {
// This unwrap is safe because the connection is always created.
connection: EngineConnection::new(manager).await.unwrap(),
}
}
#[async_trait::async_trait(?Send)]
pub trait EngineManager {
/// Send a modeling command.
/// Do not wait for the response message.
fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), crate::errors::KclError>;
pub fn send_modeling_cmd(&mut self, id_str: &str, cmd_str: &str) -> Result<(), String> {
let id = uuid::Uuid::parse_str(id_str).map_err(|e| e.to_string())?;
let cmd = serde_json::from_str(cmd_str).map_err(|e| e.to_string())?;
self.connection
.send_modeling_cmd(id, crate::executor::SourceRange::default(), cmd)
.map_err(String::from)?;
Ok(())
}
/// Send a modeling command and wait for the response message.
async fn send_modeling_cmd_get_response(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError>;
}

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
use crate::{
abstract_syntax_tree_types::{BodyItem, Function, FunctionExpression, Value},
ast::types::{BodyItem, Function, FunctionExpression, Value},
engine::EngineConnection,
errors::{KclError, KclErrorDetails},
};
@ -578,7 +578,7 @@ impl Default for PipeInfo {
/// Execute a AST's program.
pub fn execute(
program: crate::abstract_syntax_tree_types::Program,
program: crate::ast::types::Program,
memory: &mut ProgramMemory,
options: BodyType,
engine: &mut EngineConnection,

View File

@ -1,4 +1,4 @@
pub mod abstract_syntax_tree_types;
pub mod ast;
pub mod docs;
pub mod engine;
pub mod errors;

View File

@ -4,7 +4,7 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::{
abstract_syntax_tree_types::{
ast::types::{
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, MemberExpression,
UnaryExpression, ValueMeta,
},
@ -41,7 +41,7 @@ pub struct ParenthesisToken {
pub end: usize,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ParenthesisToken);
crate::ast::types::impl_value_meta!(ParenthesisToken);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
@ -56,7 +56,7 @@ pub struct ExtendedBinaryExpression {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedBinaryExpression);
crate::ast::types::impl_value_meta!(ExtendedBinaryExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
@ -70,7 +70,7 @@ pub struct ExtendedLiteral {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedLiteral);
crate::ast::types::impl_value_meta!(ExtendedLiteral);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
@ -444,7 +444,7 @@ impl ReversePolishNotation {
let expression = UnaryExpression {
start: current_token.start,
end: current_token.end,
operator: crate::abstract_syntax_tree_types::UnaryOperator::Neg,
operator: crate::ast::types::UnaryOperator::Neg,
argument: BinaryPart::Identifier(Box::new(Identifier {
name: current_token.value.trim_start_matches('-').to_string(),
start: current_token.start + 1,

View File

@ -1,7 +1,7 @@
use std::{collections::HashMap, str::FromStr};
use crate::{
abstract_syntax_tree_types::{
ast::types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NoneCodeMeta,
NoneCodeNode, NoneCodeValue, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution,
@ -1251,9 +1251,9 @@ impl Parser {
let closing_brace_token = self.get_token(closing_brace_index)?;
let args = self.make_arguments(brace_token.index, vec![])?;
let function = if let Some(stdlib_fn) = self.stdlib.get(&callee.name) {
crate::abstract_syntax_tree_types::Function::StdLib { func: stdlib_fn }
crate::ast::types::Function::StdLib { func: stdlib_fn }
} else {
crate::abstract_syntax_tree_types::Function::InMemory
crate::ast::types::Function::InMemory
};
Ok(CallExpressionResult {
expression: CallExpression {
@ -1790,7 +1790,7 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::abstract_syntax_tree_types::BinaryOperator;
use crate::ast::types::BinaryOperator;
#[test]
fn test_make_identifier() {

View File

@ -7,7 +7,7 @@ use clap::Parser;
use dashmap::DashMap;
use tower_lsp::{jsonrpc::Result as RpcResult, lsp_types::*, Client, LanguageServer};
use crate::{abstract_syntax_tree_types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR};
use crate::{ast::types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR};
/// A subcommand for running the server.
#[derive(Parser, Clone, Debug)]
@ -34,7 +34,7 @@ pub struct Backend {
/// Token maps.
pub token_map: DashMap<String, Vec<crate::tokeniser::Token>>,
/// AST maps.
pub ast_map: DashMap<String, crate::abstract_syntax_tree_types::Program>,
pub ast_map: DashMap<String, crate::ast::types::Program>,
/// Current code.
pub current_code_map: DashMap<String, String>,
/// Diagnostics.
@ -171,19 +171,19 @@ impl Backend {
for item in &ast.body {
match item {
crate::abstract_syntax_tree_types::BodyItem::ExpressionStatement(_) => continue,
crate::abstract_syntax_tree_types::BodyItem::ReturnStatement(_) => continue,
crate::abstract_syntax_tree_types::BodyItem::VariableDeclaration(variable) => {
crate::ast::types::BodyItem::ExpressionStatement(_) => continue,
crate::ast::types::BodyItem::ReturnStatement(_) => continue,
crate::ast::types::BodyItem::VariableDeclaration(variable) => {
// We only want to complete variables.
for declaration in &variable.declarations {
completions.push(CompletionItem {
label: declaration.id.name.to_string(),
label_details: None,
kind: Some(match variable.kind {
crate::abstract_syntax_tree_types::VariableKind::Let => CompletionItemKind::VARIABLE,
crate::abstract_syntax_tree_types::VariableKind::Const => CompletionItemKind::CONSTANT,
crate::abstract_syntax_tree_types::VariableKind::Var => CompletionItemKind::VARIABLE,
crate::abstract_syntax_tree_types::VariableKind::Fn => CompletionItemKind::FUNCTION,
crate::ast::types::VariableKind::Let => CompletionItemKind::VARIABLE,
crate::ast::types::VariableKind::Const => CompletionItemKind::CONSTANT,
crate::ast::types::VariableKind::Var => CompletionItemKind::VARIABLE,
crate::ast::types::VariableKind::Fn => CompletionItemKind::FUNCTION,
}),
detail: Some(variable.kind.to_string()),
documentation: None,
@ -368,7 +368,7 @@ impl LanguageServer for Backend {
};
match hover {
crate::abstract_syntax_tree_types::Hover::Function { name, range } => {
crate::ast::types::Hover::Function { name, range } => {
// Get the docs for this function.
let Some(completion) = self.stdlib_completions.get(&name) else {
return Ok(None);
@ -399,7 +399,7 @@ impl LanguageServer for Backend {
range: Some(range),
}))
}
crate::abstract_syntax_tree_types::Hover::Signature { .. } => Ok(None),
crate::ast::types::Hover::Signature { .. } => Ok(None),
}
}
@ -482,7 +482,7 @@ impl LanguageServer for Backend {
};
match hover {
crate::abstract_syntax_tree_types::Hover::Function { name, range: _ } => {
crate::ast::types::Hover::Function { name, range: _ } => {
// Get the docs for this function.
let Some(signature) = self.stdlib_signatures.get(&name) else {
return Ok(None);
@ -490,7 +490,7 @@ impl LanguageServer for Backend {
Ok(Some(signature.clone()))
}
crate::abstract_syntax_tree_types::Hover::Signature {
crate::ast::types::Hover::Signature {
name,
parameter_index,
range: _,
@ -554,7 +554,7 @@ impl LanguageServer for Backend {
};
// Now recast it.
let recast = ast.recast(
&crate::abstract_syntax_tree_types::FormatOptions {
&crate::ast::types::FormatOptions {
tab_size: params.options.tab_size as usize,
insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
use_tabs: !params.options.insert_spaces,

View File

@ -15,8 +15,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
abstract_syntax_tree_types::parse_json_number_as_f64,
engine::EngineConnection,
ast::types::parse_json_number_as_f64,
engine::{EngineConnection, EngineManager},
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, MemoryItem, Metadata, SketchGroup, SourceRange},
};
@ -518,9 +518,10 @@ pub enum Primitive {
#[cfg(test)]
mod tests {
use crate::std::StdLib;
use itertools::Itertools;
use crate::std::StdLib;
#[test]
fn test_generate_stdlib_markdown_docs() {
let stdlib = StdLib::new();

View File

@ -6,6 +6,7 @@ use kittycad::types::{ModelingCmd, Point3D};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::utils::Angle;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup},
@ -15,8 +16,6 @@ use crate::{
},
};
use super::utils::Angle;
/// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]

View File

@ -47,6 +47,7 @@ impl Angle {
///
/// ```
/// use std::f64::consts::PI;
///
/// use kcl_lib::std::utils::Angle;
///
/// assert_eq!(

View File

@ -18,8 +18,7 @@ pub async fn execute_wasm(
manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
) -> Result<JsValue, String> {
// deserialize the ast from a stringified json
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let program: kcl_lib::ast::types::Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut mem: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let mut engine = kcl_lib::engine::EngineConnection::new(manager)
@ -33,6 +32,36 @@ pub async fn execute_wasm(
JsValue::from_serde(&memory).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for execute
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn modify_ast_for_sketch(
manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
program_str: &str,
sketch_name: &str,
sketch_id: &str,
) -> Result<JsValue, String> {
// deserialize the ast from a stringified json
let mut program: kcl_lib::ast::types::Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut engine = kcl_lib::engine::EngineConnection::new(manager)
.await
.map_err(|e| format!("{:?}", e))?;
let _ = kcl_lib::ast::modify::modify_ast_for_sketch(
&mut engine,
&mut program,
sketch_name,
uuid::Uuid::parse_str(sketch_id).map_err(|e| e.to_string())?,
)
.await
.map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&program).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
@ -73,8 +102,7 @@ pub fn parse_js(js: &str) -> Result<JsValue, String> {
#[wasm_bindgen]
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
// deserialize the ast from a stringified json
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(json_str).map_err(JsError::from)?;
let program: kcl_lib::ast::types::Program = serde_json::from_str(json_str).map_err(JsError::from)?;
// Use the default options until we integrate into the UI the ability to change them.
let result = program.recast(&Default::default(), 0);

View File

@ -1,4 +1,5 @@
use anyhow::Result;
use kcl_lib::engine::EngineManager;
/// Executes a kcl program and takes a snapshot of the result.
/// This returns the bytes of the snapshot.
@ -38,13 +39,15 @@ async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
let _ = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)?;
// Send a snapshot request to the engine.
let resp = engine.send_modeling_cmd_get_response(
let resp = engine
.send_modeling_cmd_get_response(
uuid::Uuid::new_v4(),
kcl_lib::executor::SourceRange::default(),
kittycad::types::ModelingCmd::TakeSnapshot {
format: kittycad::types::ImageFormat::Png,
},
)?;
)
.await?;
if let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
@ -62,7 +65,7 @@ async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
async fn serial_test_execute_with_function_sketch() {
let code = r#"fn box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
@ -83,7 +86,7 @@ show(fnBox)"#;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_angled_line() {
async fn serial_test_execute_with_angled_line() {
let code = r#"const part001 = startSketchAt([4.83, 12.56])
|> line([15.1, 2.48], %)
|> line({ to: [3.15, -9.85], tag: 'seg01' }, %)
@ -100,7 +103,7 @@ show(part001)"#;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_parametric_example() {
async fn serial_test_execute_parametric_example() {
let code = r#"const sigmaAllow = 35000 // psi
const width = 9 // inch
const p = 150 // Force on shelf - lbs

View File

@ -0,0 +1,257 @@
use anyhow::Result;
use kcl_lib::{
ast::{modify::modify_ast_for_sketch, types::Program},
engine::{EngineConnection, EngineManager},
executor::{MemoryItem, SourceRange},
};
use kittycad::types::{ModelingCmd, Point3D};
use pretty_assertions::assert_eq;
/// Setup the engine and parse code for an ast.
async fn setup(code: &str, name: &str) -> Result<(EngineConnection, Program, uuid::Uuid)> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
let ws = client
.modeling()
.commands_ws(None, None, None, None, Some(false))
.await?;
let tokens = kcl_lib::tokeniser::lexer(code);
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
let mut engine = kcl_lib::engine::EngineConnection::new(ws).await?;
let memory = kcl_lib::executor::execute(
program.clone(),
&mut mem,
kcl_lib::executor::BodyType::Root,
&mut engine,
)?;
// We need to get the sketch ID.
// Get the sketch group ID from memory.
let MemoryItem::SketchGroup(sketch_group) = memory.root.get(name).unwrap() else {
anyhow::bail!("part001 not found in memory: {:?}", memory);
};
let sketch_id = sketch_group.id;
let plane_id = uuid::Uuid::new_v4();
engine.send_modeling_cmd(
plane_id,
SourceRange::default(),
ModelingCmd::MakePlane {
clobber: false,
origin: Point3D { x: 0.0, y: 0.0, z: 0.0 },
size: 60.0,
x_axis: Point3D { x: 1.0, y: 0.0, z: 0.0 },
y_axis: Point3D { x: 0.0, y: 1.0, z: 0.0 },
},
)?;
// Enter sketch mode.
// We can't get control points without being in sketch mode.
// You can however get path info without sketch mode.
engine.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::SketchModeEnable {
animated: false,
ortho: true,
plane_id,
},
)?;
// Enter edit mode.
// We can't get control points of an existing sketch without being in edit mode.
engine.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::EditModeEnter { target: sketch_id },
)?;
Ok((engine, program, sketch_id))
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_modify_sketch_part001() {
let name = "part001";
let code = format!(
r#"const {} = startSketchAt([8.41, 5.78])
|> line([7.37, -11.0], %)
|> line([-8.69, -3.75], %)
|> line([-5.0, 4.25], %)
"#,
name
);
let (mut engine, program, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&mut engine, &mut new_program, name, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(code, new_code);
// Make sure the program is the same.
assert_eq!(new_program, program);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_modify_sketch_part002() {
let name = "part002";
let code = format!(
r#"const {} = startSketchAt([8.41, 5.78])
|> line([7.42, -8.62], %)
|> line([-6.38, -3.51], %)
|> line([-3.77, 3.56], %)
"#,
name
);
let (mut engine, program, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&mut engine, &mut new_program, name, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(code, new_code);
// Make sure the program is the same.
assert_eq!(new_program, program);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // until KittyCAD/engine#1434 is fixed.
async fn serial_test_modify_close_sketch() {
let name = "part002";
let code = format!(
r#"const {} = startSketchAt([7.91, 3.89])
|> line([7.42, -8.62], %)
|> line([-6.38, -3.51], %)
|> line([-3.77, 3.56], %)
|> close(%)
"#,
name
);
let (mut engine, program, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&mut engine, &mut new_program, name, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(code, new_code);
// Make sure the program is the same.
assert_eq!(new_program, program);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_modify_line_to_close_sketch() {
let name = "part002";
let code = format!(
r#"const {} = startSketchAt([7.91, 3.89])
|> line([7.42, -8.62], %)
|> line([-6.38, -3.51], %)
|> line([-3.77, 3.56], %)
|> lineTo([7.91, 3.89], %)
"#,
name
);
let (mut engine, program, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&mut engine, &mut new_program, name, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(
new_code,
format!(
r#"const {} = startSketchAt([7.91, 3.89])
|> line([7.42, -8.62], %)
|> line([-6.38, -3.51], %)
|> line([-3.77, 3.56], %)
|> close(%)
"#,
name
)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_modify_with_constraint() {
let name = "part002";
let code = format!(
r#"const thing = 12
const {} = startSketchAt([7.91, 3.89])
|> line([7.42, -8.62], %)
|> line([-6.38, -3.51], %)
|> line([-3.77, 3.56], %)
|> lineTo([thing, 3.89], %)
"#,
name
);
let (mut engine, program, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let result = modify_ast_for_sketch(&mut engine, &mut new_program, name, sketch_id).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"engine: KclErrorDetails { source_ranges: [SourceRange([159, 164])], message: "Sketch part002 is constrained `partial` and cannot be modified" }"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_modify_line_should_close_sketch() {
let name = "part003";
let code = format!(
r#"const {} = startSketchAt([13.69, 3.8])
|> line([4.23, -11.79], %)
|> line([-10.7, -1.16], %)
|> line([-3.72, 8.69], %)
|> line([10.19, 4.26], %)
"#,
name
);
let (mut engine, program, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&mut engine, &mut new_program, name, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(
new_code,
format!(
r#"const {} = startSketchAt([13.69, 3.8])
|> line([4.23, -11.79], %)
|> line([-10.7, -1.16], %)
|> line([-3.72, 8.69], %)
|> close(%)
"#,
name
)
);
}