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:
2
.github/workflows/cargo-test.yml
vendored
2
.github/workflows/cargo-test.yml
vendored
@ -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}}
|
||||
|
||||
|
14
src/App.tsx
14
src/App.tsx
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ show(mySketch001)
|
||||
{
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathId: '',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [
|
||||
|
@ -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'
|
||||
|
22
src/wasm-lib/.config/nextest.toml
Normal file
22
src/wasm-lib/.config/nextest.toml
Normal 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
|
5
src/wasm-lib/Cargo.lock
generated
5
src/wasm-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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"] }
|
||||
|
2
src/wasm-lib/kcl/src/ast/mod.rs
Normal file
2
src/wasm-lib/kcl/src/ast/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod modify;
|
||||
pub mod types;
|
293
src/wasm-lib/kcl/src/ast/modify.rs
Normal file
293
src/wasm-lib/kcl/src/ast/modify.rs
Normal 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
|
||||
}
|
@ -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!(
|
@ -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),
|
||||
}
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -1,4 +1,4 @@
|
||||
pub mod abstract_syntax_tree_types;
|
||||
pub mod ast;
|
||||
pub mod docs;
|
||||
pub mod engine;
|
||||
pub mod errors;
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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)]
|
||||
|
@ -47,6 +47,7 @@ impl Angle {
|
||||
///
|
||||
/// ```
|
||||
/// use std::f64::consts::PI;
|
||||
///
|
||||
/// use kcl_lib::std::utils::Angle;
|
||||
///
|
||||
/// assert_eq!(
|
||||
@ -106,11 +107,11 @@ pub fn normalize_rad(angle: f64) -> f64 {
|
||||
/// use kcl_lib::executor::Point2d;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d{x: 0.0, y: 5.0}),
|
||||
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d { x: 0.0, y: 5.0 }),
|
||||
/// 5.0
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d{x: 3.0, y: 4.0}),
|
||||
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d { x: 3.0, y: 4.0 }),
|
||||
/// 5.0
|
||||
/// );
|
||||
/// ```
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
257
src/wasm-lib/tests/modify/main.rs
Normal file
257
src/wasm-lib/tests/modify/main.rs
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user