Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
eea47aae1e | |||
25b9b4cf98 | |||
0f3f0b3b68 | |||
33eb6126d4 | |||
dccb83f614 | |||
b56a3398ad | |||
11658e2ff5 | |||
de255acc59 | |||
d33ddb2f1b | |||
a0730ded4e | |||
afd2b507ef | |||
8983a8231b | |||
9dd708db5d | |||
c25dd1800c | |||
e56e7ba0fa | |||
dbe4e7faa6 | |||
148e125dd7 | |||
75bb91c7e1 |
@ -1,3 +1,4 @@
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
|
@ -1,3 +1,4 @@
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
src/wasm-lib/pkg/wasm_lib.js
|
4
.github/workflows/cargo-build.yml
vendored
4
.github/workflows/cargo-build.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@ -43,5 +43,7 @@ jobs:
|
||||
- name: Run cargo build
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo build --all --no-default-features --features noweb
|
||||
cargo build --all --no-default-features --features web
|
||||
cargo build --all
|
||||
shell: bash
|
||||
|
2
.github/workflows/cargo-clippy.yml
vendored
2
.github/workflows/cargo-clippy.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install latest rust
|
||||
|
2
.github/workflows/cargo-test.yml
vendored
2
.github/workflows/cargo-test.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install latest rust
|
||||
|
17021
docs/kcl.json
Normal file
17021
docs/kcl.json
Normal file
File diff suppressed because it is too large
Load Diff
3048
docs/kcl.md
Normal file
3048
docs/kcl.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
|
||||
<title>KittyCAD Modeling App</title>
|
||||
</head>
|
||||
<body class="body-bg">
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.0.4",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
@ -8,7 +8,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@kittycad/lib": "^0.0.29",
|
||||
"@kittycad/lib": "^0.0.34",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
@ -60,7 +60,7 @@
|
||||
"simpleserver": "http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src",
|
||||
"fmt-check": "prettier --check ./src",
|
||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg --no-default-features --features web && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/bindings",
|
||||
"lint": "eslint --fix src",
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling-app",
|
||||
"version": "0.0.4"
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
17
src/App.tsx
17
src/App.tsx
@ -295,18 +295,6 @@ export function App() {
|
||||
_ast,
|
||||
{
|
||||
root: {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
addLog(a)
|
||||
},
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [],
|
||||
sourceRange: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
_0: {
|
||||
type: 'userVal',
|
||||
value: 0,
|
||||
@ -328,11 +316,8 @@ export function App() {
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
pendingMemory: {},
|
||||
},
|
||||
engineCommandManager,
|
||||
{ bodyType: 'root' },
|
||||
[]
|
||||
engineCommandManager
|
||||
)
|
||||
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
|
@ -5,6 +5,7 @@ import { EngineCommand } from '../lang/std/engineConnection'
|
||||
import { useState } from 'react'
|
||||
import { ActionButton } from '../components/ActionButton'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
|
||||
type SketchModeCmd = Extract<
|
||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||
@ -22,7 +23,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
distance_to_plane: 100,
|
||||
ortho: true,
|
||||
animated: true, // TODO #273 get prefers reduced motion from CSS
|
||||
animated: !isReducedMotion(),
|
||||
})
|
||||
if (!sketchModeCmd) return null
|
||||
return (
|
||||
|
@ -39,6 +39,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
const initialValues: OutputFormat = {
|
||||
type: defaultType,
|
||||
storage: 'embedded',
|
||||
presentation: 'compact',
|
||||
}
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
@ -82,6 +83,8 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
},
|
||||
})
|
||||
|
||||
const yo = formik.values
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
@ -127,7 +130,9 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
id="storage"
|
||||
name="storage"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.storage}
|
||||
value={
|
||||
'storage' in formik.values ? formik.values.storage : ''
|
||||
}
|
||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||
>
|
||||
{type === 'gltf' && (
|
||||
|
@ -14,12 +14,12 @@ describe('processMemory', () => {
|
||||
return a - 2
|
||||
}
|
||||
const otherVar = myFn(5)
|
||||
|
||||
const theExtrude = startSketchAt([0, 0])
|
||||
|
||||
const theExtrude = startSketchAt([0, 0])
|
||||
|> lineTo([-2.4, myVar], %)
|
||||
|> lineTo([-0.76, otherVar], %)
|
||||
|> extrude(4, %)
|
||||
|
||||
|
||||
const theSketch = startSketchAt([0, 0])
|
||||
|> lineTo([-3.35, 0.17], %)
|
||||
|> lineTo([0.98, 5.16], %)
|
||||
@ -28,30 +28,20 @@ describe('processMemory', () => {
|
||||
show(theExtrude, theSketch)`
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
console.log('raw log', a)
|
||||
},
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
pendingMemory: {},
|
||||
root: {},
|
||||
})
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
expect(output.myFn).toEqual('__function__')
|
||||
expect(output.otherVar).toEqual(3)
|
||||
expect(output).toEqual({
|
||||
myVar: 5,
|
||||
myFn: '__function__',
|
||||
myFn: undefined,
|
||||
otherVar: 3,
|
||||
theExtrude: [],
|
||||
theSketch: [
|
||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
|
||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
|
||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
|
||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
|
||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
|
||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
@ -8,4 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
||||
.VITE_KC_API_WS_MODELING_URL
|
||||
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
export const TEST = import.meta.env.TEST
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { KCLUnexpectedError } from './errors'
|
||||
import { initPromise } from './rust'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
@ -213,7 +214,6 @@ describe('testing function declaration', () => {
|
||||
id: null,
|
||||
params: [],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
start: 17,
|
||||
end: 19,
|
||||
body: [],
|
||||
@ -266,7 +266,6 @@ describe('testing function declaration', () => {
|
||||
},
|
||||
],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
start: 21,
|
||||
end: 39,
|
||||
body: [
|
||||
@ -343,7 +342,6 @@ const myVar = funcN(1, 2)`
|
||||
},
|
||||
],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
start: 21,
|
||||
end: 37,
|
||||
body: [
|
||||
@ -1570,8 +1568,8 @@ const key = 'c'`
|
||||
it('comments nested within a block statement', () => {
|
||||
const code = `const mySketch = startSketchAt([0,0])
|
||||
|> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|
||||
|> lineTo([1, 1], %) /* this is
|
||||
a comment
|
||||
|> lineTo([1, 1], %) /* this is
|
||||
a comment
|
||||
spanning a few lines */
|
||||
|> lineTo({ to: [1,0], tag: "rightPath" }, %)
|
||||
|> close(%)
|
||||
@ -1584,9 +1582,8 @@ const key = 'c'`
|
||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||
type: 'NoneCodeNode',
|
||||
start: 106,
|
||||
end: 168,
|
||||
value:
|
||||
' /* this is \n a comment \n spanning a few lines */\n ',
|
||||
end: 166,
|
||||
value: ' /* this is\n a comment\n spanning a few lines */\n ',
|
||||
})
|
||||
})
|
||||
it('comments in a pipe expression', () => {
|
||||
@ -1706,3 +1703,19 @@ describe('should recognise callExpresions in binaryExpressions', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsing errors', () => {
|
||||
it('should return an error when there is a unexpected closed curly brace', async () => {
|
||||
const code = `const myVar = startSketchAt([}], %)`
|
||||
|
||||
let _theError
|
||||
try {
|
||||
const result = expect(parser_wasm(code))
|
||||
console.log('result', result)
|
||||
} catch (e) {
|
||||
_theError = e
|
||||
}
|
||||
const theError = _theError as any
|
||||
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
|
||||
})
|
||||
})
|
||||
|
@ -3,21 +3,23 @@ import { parse_js } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { initPromise } from './rust'
|
||||
import { Token } from './tokeniser'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError'
|
||||
|
||||
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
|
||||
ranges.map(([start, end]) => [start, end])
|
||||
|
||||
export const parser_wasm = (code: string): Program => {
|
||||
try {
|
||||
const program: Program = parse_js(code)
|
||||
return program
|
||||
} catch (e: any) {
|
||||
const parsed: {
|
||||
kind: string
|
||||
msg: string
|
||||
sourceRanges: [number, number][]
|
||||
} = JSON.parse(e.toString())
|
||||
const kclError: KCLError = new KCLError(
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.msg,
|
||||
parsed.sourceRanges
|
||||
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
|
||||
parsed.kind === 'invalid_expression'
|
||||
? [[parsed.start, parsed.end]]
|
||||
: rangeTypeFix(parsed.sourceRanges)
|
||||
)
|
||||
|
||||
console.log(kclError)
|
||||
@ -31,15 +33,13 @@ export async function asyncParser(code: string): Promise<Program> {
|
||||
const program: Program = parse_js(code)
|
||||
return program
|
||||
} catch (e: any) {
|
||||
const parsed: {
|
||||
kind: string
|
||||
msg: string
|
||||
sourceRanges: [number, number][]
|
||||
} = JSON.parse(e.toString())
|
||||
const kclError: KCLError = new KCLError(
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.msg,
|
||||
parsed.sourceRanges
|
||||
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
|
||||
parsed.kind === 'invalid_expression'
|
||||
? [[parsed.start, parsed.end]]
|
||||
: rangeTypeFix(parsed.sourceRanges)
|
||||
)
|
||||
|
||||
console.log(kclError)
|
||||
|
@ -22,7 +22,6 @@ export type SyntaxType =
|
||||
| 'BinaryExpression'
|
||||
| 'CallExpression'
|
||||
| 'Identifier'
|
||||
| 'BlockStatement'
|
||||
| 'ReturnStatement'
|
||||
| 'VariableDeclaration'
|
||||
| 'VariableDeclarator'
|
||||
|
@ -11,51 +11,52 @@ describe('testing artifacts', () => {
|
||||
const mySketch001 = startSketchAt([0, 0])
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||
// @ts-ignore
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'sketchGroup',
|
||||
start: {
|
||||
type: 'base',
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
id: '66366561-6465-4734-a463-366330356563',
|
||||
id: expect.any(String),
|
||||
sourceRange: [21, 42],
|
||||
pathToNode: [],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
type: 'toPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [48, 73],
|
||||
id: '30366338-6462-4330-a364-303935626163',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [79, 103],
|
||||
id: '32653334-6331-4231-b162-663334363535',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
id: '39643164-6130-4734-b432-623638393262',
|
||||
__meta: [{ sourceRange: [21, 42], pathToNode: [] }],
|
||||
id: expect.any(String),
|
||||
__meta: [{ sourceRange: [21, 42] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -69,21 +70,20 @@ const mySketch001 = startSketchAt([0, 0])
|
||||
|> extrude(2, %)
|
||||
show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||
// @ts-ignore
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
id: '65383433-3839-4333-b836-343263636638',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
__meta: [
|
||||
{ sourceRange: [127, 140], pathToNode: [] },
|
||||
{ sourceRange: [21, 42], pathToNode: [] },
|
||||
],
|
||||
__meta: [{ sourceRange: [21, 42] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -106,37 +106,33 @@ const sk2 = startSketchAt([0, 0])
|
||||
|> lineTo([2.5, 0], %)
|
||||
// |> transform(theTransf, %)
|
||||
|> extrude(2, %)
|
||||
|
||||
|
||||
|
||||
show(theExtrude, sk2)`
|
||||
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||
// @ts-ignore
|
||||
const geos = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
({ name }) => programMemory?.root?.[name]
|
||||
)
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
id: '63333330-3631-4230-b664-623132643731',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
__meta: [
|
||||
{ sourceRange: [212, 227], pathToNode: [] },
|
||||
{ sourceRange: [13, 34], pathToNode: [] },
|
||||
],
|
||||
__meta: [{ sourceRange: [13, 34] }],
|
||||
},
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
id: '33316639-3438-4661-a334-663262383737',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
__meta: [
|
||||
{ sourceRange: [453, 466], pathToNode: [] },
|
||||
{ sourceRange: [302, 323], pathToNode: [] },
|
||||
],
|
||||
__meta: [{ sourceRange: [302, 323] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError'
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
export class KCLError {
|
||||
kind: string | undefined
|
||||
kind: ExtractKind<RustKclError> | 'name'
|
||||
sourceRanges: [number, number][]
|
||||
msg: string
|
||||
constructor(
|
||||
kind: string | undefined,
|
||||
kind: ExtractKind<RustKclError> | 'name',
|
||||
msg: string,
|
||||
sourceRanges: [number, number][]
|
||||
) {
|
||||
@ -39,11 +41,18 @@ export class KCLTypeError extends KCLError {
|
||||
|
||||
export class KCLUnimplementedError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
super('unimplemented feature', msg, sourceRanges)
|
||||
super('unimplemented', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLUnexpectedError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
super('unexpected', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLValueAlreadyDefined extends KCLError {
|
||||
constructor(key: string, sourceRanges: [number, number][]) {
|
||||
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)
|
||||
|
@ -5,7 +5,7 @@ import { ProgramMemory } from './executor'
|
||||
import { initPromise } from './rust'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { vi } from 'vitest'
|
||||
import { KCLUndefinedValueError } from './errors'
|
||||
import { KCLError } from './errors'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
||||
@ -30,29 +30,6 @@ const newVar = myVar + 1`
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe('a str another str')
|
||||
})
|
||||
it('test with function call', async () => {
|
||||
const code = `
|
||||
const myVar = "hello"
|
||||
log(5, myVar)`
|
||||
const programMemoryOverride: ProgramMemory['root'] = {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: vi.fn(),
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [0, 0],
|
||||
pathToNode: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
const { root } = await enginelessExecutor(parser_wasm(code), {
|
||||
root: programMemoryOverride,
|
||||
pendingMemory: {},
|
||||
})
|
||||
expect(root.myVar.value).toBe('hello')
|
||||
expect(programMemoryOverride.log.value).toHaveBeenCalledWith(5, 'hello')
|
||||
})
|
||||
it('fn funcN = () => {} execute', async () => {
|
||||
const { root } = await exe(
|
||||
[
|
||||
@ -84,8 +61,7 @@ show(mySketch)
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [43, 80],
|
||||
id: '37333036-3033-4432-b530-643030303837',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
name: 'myPath',
|
||||
},
|
||||
@ -93,10 +69,10 @@ show(mySketch)
|
||||
type: 'toPoint',
|
||||
to: [2, 3],
|
||||
from: [0, 2],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [86, 102],
|
||||
id: '32343136-3330-4134-a462-376437386365',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -105,8 +81,7 @@ show(mySketch)
|
||||
from: [2, 3],
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 151],
|
||||
id: '32306132-6130-4138-b832-636363326330',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
name: 'rightPath',
|
||||
},
|
||||
@ -170,13 +145,12 @@ show(mySketch)
|
||||
expect(root.mySk1).toEqual({
|
||||
type: 'sketchGroup',
|
||||
start: {
|
||||
type: 'base',
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
id: '37663863-3664-4366-a637-623739336334',
|
||||
id: expect.any(String),
|
||||
sourceRange: [14, 34],
|
||||
pathToNode: [],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
@ -184,10 +158,10 @@ show(mySketch)
|
||||
type: 'toPoint',
|
||||
to: [1, 1],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [40, 56],
|
||||
id: '34356231-3362-4363-b935-393033353034',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -196,8 +170,7 @@ show(mySketch)
|
||||
from: [1, 1],
|
||||
__geoMeta: {
|
||||
sourceRange: [62, 100],
|
||||
id: '39623339-3538-4366-b633-356630326639',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
name: 'myPath',
|
||||
},
|
||||
@ -205,17 +178,17 @@ show(mySketch)
|
||||
type: 'toPoint',
|
||||
to: [1, 1],
|
||||
from: [0, 1],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [106, 122],
|
||||
id: '30636135-6232-4335-b665-366562303161',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
id: '30376661-3039-4965-b532-653665313731',
|
||||
__meta: [{ sourceRange: [14, 34], pathToNode: [] }],
|
||||
id: expect.any(String),
|
||||
__meta: [{ sourceRange: [14, 34] }],
|
||||
})
|
||||
})
|
||||
it('execute array expression', async () => {
|
||||
@ -230,13 +203,6 @@ show(mySketch)
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
@ -246,25 +212,8 @@ show(mySketch)
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [27, 49],
|
||||
},
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@ -280,13 +229,6 @@ show(mySketch)
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [27, 83],
|
||||
},
|
||||
],
|
||||
@ -302,13 +244,6 @@ show(mySketch)
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [41, 50],
|
||||
},
|
||||
],
|
||||
@ -451,17 +386,18 @@ const theExtrude = startSketchAt([0, 0])
|
||||
|> extrude(4, %)
|
||||
show(theExtrude)`
|
||||
await expect(exe(code)).rejects.toEqual(
|
||||
new KCLUndefinedValueError('Memory item myVarZ not found', [[100, 106]])
|
||||
new KCLError(
|
||||
'undefined_value',
|
||||
'memory item key `myVarZ` is not defined',
|
||||
[[100, 106]]
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// helpers
|
||||
|
||||
async function exe(
|
||||
code: string,
|
||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} }
|
||||
) {
|
||||
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
|
||||
const ast = parser_wasm(code)
|
||||
|
||||
const result = await enginelessExecutor(ast, programMemory)
|
||||
|
@ -1,35 +1,19 @@
|
||||
import {
|
||||
Program,
|
||||
BinaryPart,
|
||||
BinaryExpression,
|
||||
PipeExpression,
|
||||
ObjectExpression,
|
||||
MemberExpression,
|
||||
Identifier,
|
||||
CallExpression,
|
||||
ArrayExpression,
|
||||
UnaryExpression,
|
||||
} from './abstractSyntaxTreeTypes'
|
||||
import { InternalFnNames } from './std/stdTypes'
|
||||
import { internalFns } from './std/std'
|
||||
import {
|
||||
KCLUndefinedValueError,
|
||||
KCLValueAlreadyDefined,
|
||||
KCLSyntaxError,
|
||||
KCLSemanticError,
|
||||
KCLTypeError,
|
||||
} from './errors'
|
||||
import { Program } from './abstractSyntaxTreeTypes'
|
||||
import {
|
||||
EngineCommandManager,
|
||||
ArtifactMap,
|
||||
SourceRangeMap,
|
||||
} from './std/engineConnection'
|
||||
import { ProgramReturn } from '../wasm-lib/bindings/ProgramReturn'
|
||||
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError'
|
||||
import { rangeTypeFix } from './abstractSyntaxTree'
|
||||
|
||||
export type SourceRange = [number, number]
|
||||
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
|
||||
export type Metadata = {
|
||||
sourceRange: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
export type Position = [number, number, number]
|
||||
export type Rotation = [number, number, number, number]
|
||||
@ -41,7 +25,6 @@ interface BasePath {
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,9 +51,7 @@ export interface AngledLineTo extends BasePath {
|
||||
interface GeoMeta {
|
||||
__geoMeta: {
|
||||
id: string
|
||||
refId?: string
|
||||
sourceRange: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,69 +99,16 @@ type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
|
||||
interface Memory {
|
||||
[key: string]: MemoryItem
|
||||
}
|
||||
interface PendingMemory {
|
||||
[key: string]: Promise<MemoryItem>
|
||||
}
|
||||
|
||||
export interface ProgramMemory {
|
||||
root: Memory
|
||||
pendingMemory: Partial<PendingMemory>
|
||||
return?: Identifier[]
|
||||
}
|
||||
|
||||
const addItemToMemory = (
|
||||
programMemory: ProgramMemory,
|
||||
key: string,
|
||||
sourceRange: [[number, number]],
|
||||
value: MemoryItem | Promise<MemoryItem>
|
||||
) => {
|
||||
const _programMemory = programMemory
|
||||
if (_programMemory.root[key] || _programMemory.pendingMemory[key]) {
|
||||
throw new KCLValueAlreadyDefined(key, sourceRange)
|
||||
}
|
||||
if (value instanceof Promise) {
|
||||
_programMemory.pendingMemory[key] = value
|
||||
value.then((resolvedValue) => {
|
||||
_programMemory.root[key] = resolvedValue
|
||||
delete _programMemory.pendingMemory[key]
|
||||
})
|
||||
} else {
|
||||
_programMemory.root[key] = value
|
||||
}
|
||||
return _programMemory
|
||||
}
|
||||
|
||||
const promisifyMemoryItem = async (obj: MemoryItem) => {
|
||||
if (obj.value instanceof Promise) {
|
||||
const resolvedGuy = await obj.value
|
||||
return {
|
||||
...obj,
|
||||
value: resolvedGuy,
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
const getMemoryItem = async (
|
||||
programMemory: ProgramMemory,
|
||||
key: string,
|
||||
sourceRanges: [number, number][]
|
||||
): Promise<MemoryItem> => {
|
||||
if (programMemory.root[key]) {
|
||||
return programMemory.root[key]
|
||||
}
|
||||
if (programMemory.pendingMemory[key]) {
|
||||
return programMemory.pendingMemory[key] as Promise<MemoryItem>
|
||||
}
|
||||
throw new KCLUndefinedValueError(`Memory item ${key} not found`, sourceRanges)
|
||||
return?: ProgramReturn
|
||||
}
|
||||
|
||||
export const executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} },
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
engineCommandManager: EngineCommandManager,
|
||||
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' },
|
||||
previousPathToNode: PathToNode = [],
|
||||
// work around while the gemotry is still be stored on the frontend
|
||||
// will be removed when the stream UI is added.
|
||||
tempMapCallback: (a: {
|
||||
@ -192,9 +120,7 @@ export const executor = async (
|
||||
const _programMemory = await _executor(
|
||||
node,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
options,
|
||||
previousPathToNode
|
||||
engineCommandManager
|
||||
)
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
@ -206,840 +132,27 @@ export const executor = async (
|
||||
|
||||
export const _executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} },
|
||||
engineCommandManager: EngineCommandManager,
|
||||
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' },
|
||||
previousPathToNode: PathToNode = []
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
engineCommandManager: EngineCommandManager
|
||||
): Promise<ProgramMemory> => {
|
||||
let _programMemory: ProgramMemory = {
|
||||
root: {
|
||||
...programMemory.root,
|
||||
},
|
||||
pendingMemory: {
|
||||
...programMemory.pendingMemory,
|
||||
},
|
||||
return: programMemory.return,
|
||||
}
|
||||
const { body } = node
|
||||
const proms: Promise<any>[] = []
|
||||
for (let bodyIndex = 0; bodyIndex < body.length; bodyIndex++) {
|
||||
const statement = body[bodyIndex]
|
||||
if (statement.type === 'VariableDeclaration') {
|
||||
for (let index = 0; index < statement.declarations.length; index++) {
|
||||
const declaration = statement.declarations[index]
|
||||
const variableName = declaration.id.name
|
||||
const pathToNode: PathToNode = [
|
||||
...previousPathToNode,
|
||||
['body', ''],
|
||||
[bodyIndex, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[index, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
]
|
||||
const sourceRange: SourceRange = [
|
||||
declaration.init.start,
|
||||
declaration.init.end,
|
||||
]
|
||||
const __meta: Metadata[] = [
|
||||
{
|
||||
pathToNode,
|
||||
sourceRange,
|
||||
},
|
||||
]
|
||||
|
||||
if (declaration.init.type === 'PipeExpression') {
|
||||
const prom = getPipeExpressionResult(
|
||||
declaration.init,
|
||||
_programMemory,
|
||||
engineCommandManager,
|
||||
pathToNode
|
||||
)
|
||||
proms.push(prom)
|
||||
const value = await prom
|
||||
if (value?.type === 'sketchGroup' || value?.type === 'extrudeGroup') {
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
value
|
||||
)
|
||||
} else {
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
{
|
||||
type: 'userVal',
|
||||
value,
|
||||
__meta,
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (declaration.init.type === 'Identifier') {
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
{
|
||||
type: 'userVal',
|
||||
value: _programMemory.root[declaration.init.name].value,
|
||||
__meta,
|
||||
}
|
||||
)
|
||||
} else if (declaration.init.type === 'Literal') {
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
{
|
||||
type: 'userVal',
|
||||
value: declaration.init.value,
|
||||
__meta,
|
||||
}
|
||||
)
|
||||
} else if (declaration.init.type === 'BinaryExpression') {
|
||||
const prom = getBinaryExpressionResult(
|
||||
declaration.init,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
promisifyMemoryItem({
|
||||
type: 'userVal',
|
||||
value: prom,
|
||||
__meta,
|
||||
})
|
||||
)
|
||||
} else if (declaration.init.type === 'UnaryExpression') {
|
||||
const prom = getUnaryExpressionResult(
|
||||
declaration.init,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
promisifyMemoryItem({
|
||||
type: 'userVal',
|
||||
value: prom,
|
||||
__meta,
|
||||
})
|
||||
)
|
||||
} else if (declaration.init.type === 'ArrayExpression') {
|
||||
const valueInfo: Promise<{ value: any; __meta?: Metadata }>[] =
|
||||
declaration.init.elements.map(
|
||||
async (element): Promise<{ value: any; __meta?: Metadata }> => {
|
||||
if (element.type === 'Literal') {
|
||||
return {
|
||||
value: element.value,
|
||||
}
|
||||
} else if (element.type === 'BinaryExpression') {
|
||||
const prom = getBinaryExpressionResult(
|
||||
element,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
return {
|
||||
value: await prom,
|
||||
}
|
||||
} else if (element.type === 'PipeExpression') {
|
||||
const prom = getPipeExpressionResult(
|
||||
element,
|
||||
_programMemory,
|
||||
engineCommandManager,
|
||||
pathToNode
|
||||
)
|
||||
proms.push(prom)
|
||||
return {
|
||||
value: await prom,
|
||||
}
|
||||
} else if (element.type === 'Identifier') {
|
||||
const node = await getMemoryItem(
|
||||
_programMemory,
|
||||
element.name,
|
||||
[[element.start, element.end]]
|
||||
)
|
||||
return {
|
||||
value: node.value,
|
||||
__meta: node.__meta[node.__meta.length - 1],
|
||||
}
|
||||
} else if (element.type === 'UnaryExpression') {
|
||||
const prom = getUnaryExpressionResult(
|
||||
element,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
return {
|
||||
value: await prom,
|
||||
}
|
||||
} else {
|
||||
throw new KCLSyntaxError(
|
||||
`Unexpected element type ${element.type} in array expression`,
|
||||
// TODO: Refactor this whole block into a `switch` so that we have a specific
|
||||
// type here and can put a sourceRange.
|
||||
[]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
const awaitedValueInfo = await Promise.all(valueInfo)
|
||||
const meta = awaitedValueInfo
|
||||
.filter(({ __meta }) => __meta)
|
||||
.map(({ __meta }) => __meta) as Metadata[]
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
{
|
||||
type: 'userVal',
|
||||
value: awaitedValueInfo.map(({ value }) => value),
|
||||
__meta: [...__meta, ...meta],
|
||||
}
|
||||
)
|
||||
} else if (declaration.init.type === 'ObjectExpression') {
|
||||
const prom = executeObjectExpression(
|
||||
_programMemory,
|
||||
declaration.init,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
promisifyMemoryItem({
|
||||
type: 'userVal',
|
||||
value: prom,
|
||||
__meta,
|
||||
})
|
||||
)
|
||||
} else if (declaration.init.type === 'FunctionExpression') {
|
||||
const fnInit = declaration.init
|
||||
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
declaration.id.name,
|
||||
[sourceRange],
|
||||
{
|
||||
type: 'userVal',
|
||||
value: async (...args: any[]) => {
|
||||
let fnMemory: ProgramMemory = {
|
||||
root: {
|
||||
..._programMemory.root,
|
||||
},
|
||||
pendingMemory: {
|
||||
..._programMemory.pendingMemory,
|
||||
},
|
||||
}
|
||||
if (args.length > fnInit.params.length) {
|
||||
throw new KCLSyntaxError(
|
||||
`Too many arguments passed to function ${declaration.id.name}`,
|
||||
[[declaration.start, declaration.end]]
|
||||
)
|
||||
} else if (args.length < fnInit.params.length) {
|
||||
throw new KCLSyntaxError(
|
||||
`Too few arguments passed to function ${declaration.id.name}`,
|
||||
[[declaration.start, declaration.end]]
|
||||
)
|
||||
}
|
||||
fnInit.params.forEach((param, index) => {
|
||||
fnMemory = addItemToMemory(
|
||||
fnMemory,
|
||||
param.name,
|
||||
[sourceRange],
|
||||
{
|
||||
type: 'userVal',
|
||||
value: args[index],
|
||||
__meta,
|
||||
}
|
||||
)
|
||||
})
|
||||
const prom = _executor(
|
||||
fnInit.body,
|
||||
fnMemory,
|
||||
engineCommandManager,
|
||||
{
|
||||
bodyType: 'block',
|
||||
}
|
||||
)
|
||||
proms.push(prom)
|
||||
const result = (await prom).return
|
||||
return result
|
||||
},
|
||||
__meta,
|
||||
}
|
||||
)
|
||||
} else if (declaration.init.type === 'MemberExpression') {
|
||||
await Promise.all([...proms]) // TODO wait for previous promises, does that makes sense?
|
||||
const prom = getMemberExpressionResult(
|
||||
declaration.init,
|
||||
_programMemory
|
||||
)
|
||||
proms.push(prom)
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
promisifyMemoryItem({
|
||||
type: 'userVal',
|
||||
value: prom,
|
||||
__meta,
|
||||
})
|
||||
)
|
||||
} else if (declaration.init.type === 'CallExpression') {
|
||||
const prom = executeCallExpression(
|
||||
_programMemory,
|
||||
declaration.init,
|
||||
engineCommandManager,
|
||||
previousPathToNode
|
||||
)
|
||||
proms.push(prom)
|
||||
_programMemory = addItemToMemory(
|
||||
_programMemory,
|
||||
variableName,
|
||||
[sourceRange],
|
||||
prom.then((a) => {
|
||||
return a?.type === 'sketchGroup' || a?.type === 'extrudeGroup'
|
||||
? a
|
||||
: {
|
||||
type: 'userVal',
|
||||
value: a,
|
||||
__meta,
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
throw new KCLSyntaxError(
|
||||
'Unsupported declaration type: ' + declaration.init.type,
|
||||
[[declaration.start, declaration.end]]
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'ExpressionStatement') {
|
||||
const expression = statement.expression
|
||||
if (expression.type === 'CallExpression') {
|
||||
const functionName = expression.callee.name
|
||||
const args = expression.arguments.map((arg) => {
|
||||
if (arg.type === 'Literal') {
|
||||
return arg.value
|
||||
} else if (arg.type === 'Identifier') {
|
||||
return _programMemory.root[arg.name]?.value
|
||||
}
|
||||
})
|
||||
if ('show' === functionName) {
|
||||
if (options.bodyType !== 'root') {
|
||||
throw new KCLSemanticError(
|
||||
`Cannot call ${functionName} outside of a root`,
|
||||
[[statement.start, statement.end]]
|
||||
)
|
||||
}
|
||||
_programMemory.return = expression.arguments as any // todo memory redo
|
||||
} else {
|
||||
if (_programMemory.root[functionName] === undefined) {
|
||||
throw new KCLSemanticError(`No such name ${functionName} defined`, [
|
||||
[statement.start, statement.end],
|
||||
])
|
||||
}
|
||||
_programMemory.root[functionName].value(...args)
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'ReturnStatement') {
|
||||
if (statement.argument.type === 'BinaryExpression') {
|
||||
const prom = getBinaryExpressionResult(
|
||||
statement.argument,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
_programMemory.return = await prom
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(proms)
|
||||
return _programMemory
|
||||
}
|
||||
|
||||
function getMemberExpressionResult(
|
||||
expression: MemberExpression,
|
||||
programMemory: ProgramMemory
|
||||
) {
|
||||
const propertyName = (
|
||||
expression.property.type === 'Identifier'
|
||||
? expression.property.name
|
||||
: expression.property.value
|
||||
) as any
|
||||
const object: any =
|
||||
expression.object.type === 'MemberExpression'
|
||||
? getMemberExpressionResult(expression.object, programMemory)
|
||||
: programMemory.root[expression.object.name]?.value
|
||||
return object?.[propertyName]
|
||||
}
|
||||
|
||||
async function getBinaryExpressionResult(
|
||||
expression: BinaryExpression,
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
pipeInfo: {
|
||||
isInPipe: boolean
|
||||
previousResults: any[]
|
||||
expressionIndex: number
|
||||
body: PipeExpression['body']
|
||||
sourceRangeOverride?: SourceRange
|
||||
} = {
|
||||
isInPipe: false,
|
||||
previousResults: [],
|
||||
expressionIndex: 0,
|
||||
body: [],
|
||||
}
|
||||
) {
|
||||
const _pipeInfo = {
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
const left = await getBinaryPartResult(
|
||||
expression.left,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
const right = await getBinaryPartResult(
|
||||
expression.right,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
if (expression.operator === '+') return left + right
|
||||
if (expression.operator === '-') return left - right
|
||||
if (expression.operator === '*') return left * right
|
||||
if (expression.operator === '/') return left / right
|
||||
if (expression.operator === '%') return left % right
|
||||
}
|
||||
|
||||
async function getBinaryPartResult(
|
||||
part: BinaryPart,
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
pipeInfo: {
|
||||
isInPipe: boolean
|
||||
previousResults: any[]
|
||||
expressionIndex: number
|
||||
body: PipeExpression['body']
|
||||
sourceRangeOverride?: SourceRange
|
||||
} = {
|
||||
isInPipe: false,
|
||||
previousResults: [],
|
||||
expressionIndex: 0,
|
||||
body: [],
|
||||
}
|
||||
): Promise<any> {
|
||||
const _pipeInfo = {
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
if (part.type === 'Literal') {
|
||||
return part.value
|
||||
} else if (part.type === 'Identifier') {
|
||||
return programMemory.root[part.name].value
|
||||
} else if (part.type === 'BinaryExpression') {
|
||||
const prom = getBinaryExpressionResult(
|
||||
part,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
const result = await prom
|
||||
return result
|
||||
} else if (part.type === 'CallExpression') {
|
||||
const result = await executeCallExpression(
|
||||
programMemory,
|
||||
part,
|
||||
engineCommandManager,
|
||||
[],
|
||||
_pipeInfo
|
||||
)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
async function getUnaryExpressionResult(
|
||||
expression: UnaryExpression,
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
pipeInfo: {
|
||||
isInPipe: boolean
|
||||
previousResults: any[]
|
||||
expressionIndex: number
|
||||
body: PipeExpression['body']
|
||||
sourceRangeOverride?: SourceRange
|
||||
} = {
|
||||
isInPipe: false,
|
||||
previousResults: [],
|
||||
expressionIndex: 0,
|
||||
body: [],
|
||||
}
|
||||
) {
|
||||
return -(await getBinaryPartResult(
|
||||
expression.argument,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
{
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
async function getPipeExpressionResult(
|
||||
expression: PipeExpression,
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
previousPathToNode: PathToNode = []
|
||||
) {
|
||||
const executedBody = await executePipeBody(
|
||||
expression.body,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
previousPathToNode
|
||||
)
|
||||
const result = executedBody[executedBody.length - 1]
|
||||
return result
|
||||
}
|
||||
|
||||
async function executePipeBody(
|
||||
body: PipeExpression['body'],
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
previousPathToNode: PathToNode = [],
|
||||
expressionIndex = 0,
|
||||
previousResults: any[] = []
|
||||
): Promise<any[]> {
|
||||
if (expressionIndex === body.length) {
|
||||
return previousResults
|
||||
}
|
||||
const expression = body[expressionIndex]
|
||||
if (expression.type === 'BinaryExpression') {
|
||||
const result = await getBinaryExpressionResult(
|
||||
expression,
|
||||
programMemory,
|
||||
try {
|
||||
const memory: ProgramMemory = await execute_wasm(
|
||||
JSON.stringify(node),
|
||||
JSON.stringify(programMemory),
|
||||
engineCommandManager
|
||||
)
|
||||
return executePipeBody(
|
||||
body,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
previousPathToNode,
|
||||
expressionIndex + 1,
|
||||
[...previousResults, result]
|
||||
return memory
|
||||
} catch (e: any) {
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
|
||||
parsed.kind === 'invalid_expression'
|
||||
? [[parsed.start, parsed.end]]
|
||||
: rangeTypeFix(parsed.sourceRanges)
|
||||
)
|
||||
} else if (expression.type === 'CallExpression') {
|
||||
return await executeCallExpression(
|
||||
programMemory,
|
||||
expression,
|
||||
engineCommandManager,
|
||||
previousPathToNode,
|
||||
{
|
||||
isInPipe: true,
|
||||
previousResults,
|
||||
expressionIndex,
|
||||
body,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
throw new KCLSyntaxError('Invalid pipe expression', [
|
||||
[expression.start, expression.end],
|
||||
])
|
||||
}
|
||||
|
||||
async function executeObjectExpression(
|
||||
_programMemory: ProgramMemory,
|
||||
objExp: ObjectExpression,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
pipeInfo: {
|
||||
isInPipe: boolean
|
||||
previousResults: any[]
|
||||
expressionIndex: number
|
||||
body: PipeExpression['body']
|
||||
sourceRangeOverride?: SourceRange
|
||||
} = {
|
||||
isInPipe: false,
|
||||
previousResults: [],
|
||||
expressionIndex: 0,
|
||||
body: [],
|
||||
}
|
||||
) {
|
||||
const _pipeInfo = {
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
const obj: { [key: string]: any } = {}
|
||||
const proms: Promise<any>[] = []
|
||||
objExp.properties.forEach(async (property) => {
|
||||
if (property.type === 'ObjectProperty') {
|
||||
if (property.value.type === 'Literal') {
|
||||
obj[property.key.name] = property.value.value
|
||||
} else if (property.value.type === 'BinaryExpression') {
|
||||
const prom = getBinaryExpressionResult(
|
||||
property.value,
|
||||
_programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
proms.push(prom)
|
||||
obj[property.key.name] = await prom
|
||||
} else if (property.value.type === 'PipeExpression') {
|
||||
const prom = getPipeExpressionResult(
|
||||
property.value,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
obj[property.key.name] = await prom
|
||||
} else if (property.value.type === 'Identifier') {
|
||||
obj[property.key.name] = (
|
||||
await getMemoryItem(_programMemory, property.value.name, [
|
||||
[property.value.start, property.value.end],
|
||||
])
|
||||
).value
|
||||
} else if (property.value.type === 'ObjectExpression') {
|
||||
const prom = executeObjectExpression(
|
||||
_programMemory,
|
||||
property.value,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
obj[property.key.name] = await prom
|
||||
} else if (property.value.type === 'ArrayExpression') {
|
||||
const prom = executeArrayExpression(
|
||||
_programMemory,
|
||||
property.value,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
obj[property.key.name] = await prom
|
||||
} else if (property.value.type === 'CallExpression') {
|
||||
const prom = executeCallExpression(
|
||||
_programMemory,
|
||||
property.value,
|
||||
engineCommandManager,
|
||||
[],
|
||||
_pipeInfo
|
||||
)
|
||||
proms.push(prom)
|
||||
const result = await prom
|
||||
obj[property.key.name] = result
|
||||
} else if (property.value.type === 'UnaryExpression') {
|
||||
const prom = getUnaryExpressionResult(
|
||||
property.value,
|
||||
_programMemory,
|
||||
engineCommandManager
|
||||
)
|
||||
proms.push(prom)
|
||||
obj[property.key.name] = await prom
|
||||
} else {
|
||||
throw new KCLSyntaxError(
|
||||
`Unexpected property type ${property.value.type} in object expression`,
|
||||
[[property.value.start, property.value.end]]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw new KCLSyntaxError(
|
||||
`Unexpected property type ${property.type} in object expression`,
|
||||
[[property.value.start, property.value.end]]
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(proms)
|
||||
return obj
|
||||
}
|
||||
|
||||
async function executeArrayExpression(
|
||||
_programMemory: ProgramMemory,
|
||||
arrExp: ArrayExpression,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
pipeInfo: {
|
||||
isInPipe: boolean
|
||||
previousResults: any[]
|
||||
expressionIndex: number
|
||||
body: PipeExpression['body']
|
||||
sourceRangeOverride?: SourceRange
|
||||
} = {
|
||||
isInPipe: false,
|
||||
previousResults: [],
|
||||
expressionIndex: 0,
|
||||
body: [],
|
||||
}
|
||||
) {
|
||||
const _pipeInfo = {
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
return await Promise.all(
|
||||
arrExp.elements.map((el) => {
|
||||
if (el.type === 'Literal') {
|
||||
return el.value
|
||||
} else if (el.type === 'Identifier') {
|
||||
return _programMemory.root?.[el.name]?.value
|
||||
} else if (el.type === 'BinaryExpression') {
|
||||
return getBinaryExpressionResult(
|
||||
el,
|
||||
_programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
} else if (el.type === 'ObjectExpression') {
|
||||
return executeObjectExpression(_programMemory, el, engineCommandManager)
|
||||
} else if (el.type === 'CallExpression') {
|
||||
const result: any = executeCallExpression(
|
||||
_programMemory,
|
||||
el,
|
||||
engineCommandManager,
|
||||
[],
|
||||
_pipeInfo
|
||||
)
|
||||
return result
|
||||
} else if (el.type === 'UnaryExpression') {
|
||||
return getUnaryExpressionResult(
|
||||
el,
|
||||
_programMemory,
|
||||
engineCommandManager,
|
||||
{
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
throw new KCLTypeError('Invalid argument type', [[el.start, el.end]])
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function executeCallExpression(
|
||||
programMemory: ProgramMemory,
|
||||
expression: CallExpression,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
previousPathToNode: PathToNode = [],
|
||||
pipeInfo: {
|
||||
isInPipe: boolean
|
||||
previousResults: any[]
|
||||
expressionIndex: number
|
||||
body: PipeExpression['body']
|
||||
sourceRangeOverride?: SourceRange
|
||||
} = {
|
||||
isInPipe: false,
|
||||
previousResults: [],
|
||||
expressionIndex: 0,
|
||||
body: [],
|
||||
}
|
||||
) {
|
||||
const {
|
||||
isInPipe,
|
||||
previousResults,
|
||||
expressionIndex,
|
||||
body,
|
||||
sourceRangeOverride,
|
||||
} = pipeInfo
|
||||
const functionName = expression?.callee?.name
|
||||
const _pipeInfo = {
|
||||
...pipeInfo,
|
||||
isInPipe: false,
|
||||
}
|
||||
const fnArgs = await Promise.all(
|
||||
expression?.arguments?.map(async (arg) => {
|
||||
if (arg.type === 'Literal') {
|
||||
return arg.value
|
||||
} else if (arg.type === 'Identifier') {
|
||||
await new Promise((r) => setTimeout(r)) // push into next even loop, but also probably should fix this
|
||||
const temp = await getMemoryItem(programMemory, arg.name, [
|
||||
[arg.start, arg.end],
|
||||
])
|
||||
return temp?.type === 'userVal' ? temp.value : temp
|
||||
} else if (arg.type === 'PipeSubstitution') {
|
||||
return previousResults[expressionIndex - 1]
|
||||
} else if (arg.type === 'ArrayExpression') {
|
||||
return await executeArrayExpression(
|
||||
programMemory,
|
||||
arg,
|
||||
engineCommandManager,
|
||||
pipeInfo
|
||||
)
|
||||
} else if (arg.type === 'CallExpression') {
|
||||
const result: any = await executeCallExpression(
|
||||
programMemory,
|
||||
arg,
|
||||
engineCommandManager,
|
||||
previousPathToNode,
|
||||
_pipeInfo
|
||||
)
|
||||
return result
|
||||
} else if (arg.type === 'ObjectExpression') {
|
||||
return await executeObjectExpression(
|
||||
programMemory,
|
||||
arg,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
} else if (arg.type === 'UnaryExpression') {
|
||||
return getUnaryExpressionResult(
|
||||
arg,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
} else if (arg.type === 'BinaryExpression') {
|
||||
return getBinaryExpressionResult(
|
||||
arg,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
_pipeInfo
|
||||
)
|
||||
}
|
||||
throw new KCLSyntaxError('Invalid argument type in function call', [
|
||||
[arg.start, arg.end],
|
||||
])
|
||||
})
|
||||
)
|
||||
if (functionName in internalFns) {
|
||||
const fnNameWithSketchOrExtrude = functionName as InternalFnNames
|
||||
const result = await internalFns[fnNameWithSketchOrExtrude](
|
||||
{
|
||||
programMemory,
|
||||
sourceRange: sourceRangeOverride || [expression.start, expression.end],
|
||||
engineCommandManager,
|
||||
code: JSON.stringify(expression),
|
||||
},
|
||||
fnArgs[0],
|
||||
fnArgs[1],
|
||||
fnArgs[2]
|
||||
)
|
||||
return isInPipe
|
||||
? await executePipeBody(
|
||||
body,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
previousPathToNode,
|
||||
expressionIndex + 1,
|
||||
[...previousResults, result]
|
||||
)
|
||||
: result
|
||||
}
|
||||
const result = await programMemory.root[functionName].value(...fnArgs)
|
||||
return isInPipe
|
||||
? await executePipeBody(
|
||||
body,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
previousPathToNode,
|
||||
expressionIndex + 1,
|
||||
[...previousResults, result]
|
||||
)
|
||||
: result
|
||||
console.log(kclError)
|
||||
throw kclError
|
||||
}
|
||||
}
|
||||
|
@ -182,14 +182,14 @@ describe('Testing moveValueIntoNewVariable', () => {
|
||||
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
|
||||
const abc = 3
|
||||
const identifierGuy = 5
|
||||
const yo = 5 + 6
|
||||
const part001 = startSketchAt([-1.2, 4.83])
|
||||
|> line([2.8, 0], %)
|
||||
|> angledLine([100 + 100, 3.09], %)
|
||||
|> angledLine([abc, 3.09], %)
|
||||
|> angledLine([def('yo'), 3.09], %)
|
||||
|> angledLine([def(yo), 3.09], %)
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||
const yo = 5 + 6
|
||||
|> angledLine([jkl(yo) + 2, 3.09], %)
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
it('should move a binary expression into a new variable', async () => {
|
||||
@ -231,7 +231,7 @@ show(part001)`
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = def('yo')`)
|
||||
expect(newCode).toContain(`const newVar = def(yo)`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a binary expression with call expression into a new variable', async () => {
|
||||
@ -245,7 +245,7 @@ show(part001)`
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = jkl('yo') + 2`)
|
||||
expect(newCode).toContain(`const newVar = jkl(yo) + 2`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a identifier into a new variable', async () => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Program } from './abstractSyntaxTreeTypes'
|
||||
import { recast_js } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { recast_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||
|
||||
export const recast = (ast: Program): string => {
|
||||
try {
|
||||
const s: string = recast_js(JSON.stringify(ast))
|
||||
const s: string = recast_wasm(JSON.stringify(ast))
|
||||
return s
|
||||
} catch (e) {
|
||||
// TODO: do something real with the error.
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { SourceRange } from '../executor'
|
||||
import { Selections } from '../../useStore'
|
||||
import { VITE_KC_API_WS_MODELING_URL } from '../../env'
|
||||
import { SourceRange } from 'lang/executor'
|
||||
import { Selections } from 'useStore'
|
||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from '../../lib/exportSave'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
interface ResultCommand {
|
||||
@ -32,11 +32,12 @@ interface CursorSelectionsArgs {
|
||||
idBasedSelections: { type: string; id: string }[]
|
||||
}
|
||||
|
||||
export type EngineCommand = Models['WebSocketMessages_type']
|
||||
interface NewTrackArgs {
|
||||
conn: EngineConnection
|
||||
mediaStream: MediaStream
|
||||
}
|
||||
|
||||
type OkResponse = Models['OkModelingCmdResponse_type']
|
||||
|
||||
type WebSocketResponse = Models['WebSocketResponses_type']
|
||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||
|
||||
// EngineConnection encapsulates the connection(s) to the Engine
|
||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||
@ -46,42 +47,76 @@ export class EngineConnection {
|
||||
pc?: RTCPeerConnection
|
||||
lossyDataChannel?: RTCDataChannel
|
||||
|
||||
onConnectionStarted: (conn: EngineConnection) => void = () => {}
|
||||
|
||||
waitForReady: Promise<void> = new Promise(() => {})
|
||||
private resolveReady = () => {}
|
||||
private ready: boolean
|
||||
|
||||
readonly url: string
|
||||
private readonly token?: string
|
||||
private onWebsocketOpen: (engineConnection: EngineConnection) => void
|
||||
private onDataChannelOpen: (engineConnection: EngineConnection) => void
|
||||
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
|
||||
private onConnectionStarted: (engineConnection: EngineConnection) => void
|
||||
private onClose: (engineConnection: EngineConnection) => void
|
||||
private onNewTrack: (track: NewTrackArgs) => void
|
||||
|
||||
constructor({
|
||||
url,
|
||||
token,
|
||||
onConnectionStarted,
|
||||
onWebsocketOpen = () => {},
|
||||
onNewTrack = () => {},
|
||||
onEngineConnectionOpen = () => {},
|
||||
onConnectionStarted = () => {},
|
||||
onClose = () => {},
|
||||
onDataChannelOpen = () => {},
|
||||
}: {
|
||||
url: string
|
||||
token?: string
|
||||
onConnectionStarted: (conn: EngineConnection) => void
|
||||
onWebsocketOpen?: (engineConnection: EngineConnection) => void
|
||||
onDataChannelOpen?: (engineConnection: EngineConnection) => void
|
||||
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
|
||||
onConnectionStarted?: (engineConnection: EngineConnection) => void
|
||||
onClose?: (engineConnection: EngineConnection) => void
|
||||
onNewTrack?: (track: NewTrackArgs) => void
|
||||
}) {
|
||||
this.url = url
|
||||
this.token = token
|
||||
this.ready = false
|
||||
this.onWebsocketOpen = onWebsocketOpen
|
||||
this.onDataChannelOpen = onDataChannelOpen
|
||||
this.onEngineConnectionOpen = onEngineConnectionOpen
|
||||
this.onConnectionStarted = onConnectionStarted
|
||||
this.onClose = onClose
|
||||
this.onNewTrack = onNewTrack
|
||||
|
||||
// TODO(paultag): This isn't right; this should be when the
|
||||
// connection is in a good place, and tied to the connect() method,
|
||||
// but this is part of a larger refactor to untangle logic. Once the
|
||||
// Connection is pulled apart, we can rework how ready is represented.
|
||||
// This was just the easiest way to ensure some level of parity between
|
||||
// the CommandManager and the Connection until I send a rework for
|
||||
// retry logic.
|
||||
this.waitForReady = new Promise((resolve) => {
|
||||
this.resolveReady = resolve
|
||||
})
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 10000
|
||||
|
||||
setInterval(() => {
|
||||
if (this.isReady()) {
|
||||
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
|
||||
// command through the WebSocket connection. This will help both ends
|
||||
// of the connection maintain the TCP connection without hitting a
|
||||
// timeout condition.
|
||||
this.send({ type: 'ping' })
|
||||
}
|
||||
}, pingIntervalMs)
|
||||
}
|
||||
|
||||
// isReady will return true only when the WebRTC *and* WebSocket connection
|
||||
// are connected. During setup, the WebSocket connection comes online first,
|
||||
// which is used to establish the WebRTC connection. The EngineConnection
|
||||
// is not "Ready" until both are connected.
|
||||
isReady() {
|
||||
return this.ready
|
||||
}
|
||||
// connect will attempt to connect to the Engine over a WebSocket, and
|
||||
// establish the WebRTC connections.
|
||||
//
|
||||
// This will attempt the full handshake, and retry if the connection
|
||||
// did not establish.
|
||||
connect() {
|
||||
this.websocket = new WebSocket(this.url, [])
|
||||
// TODO(paultag): make this safe to call multiple times, and figure out
|
||||
// when a connection is in progress (state: connecting or something).
|
||||
|
||||
this.websocket = new WebSocket(this.url, [])
|
||||
this.websocket.binaryType = 'arraybuffer'
|
||||
|
||||
this.pc = new RTCPeerConnection()
|
||||
@ -89,18 +124,22 @@ export class EngineConnection {
|
||||
this.websocket.addEventListener('open', (event) => {
|
||||
console.log('Connected to websocket, waiting for ICE servers')
|
||||
if (this.token) {
|
||||
this.websocket?.send(
|
||||
JSON.stringify({ headers: { Authorization: `Bearer ${this.token}` } })
|
||||
)
|
||||
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
||||
}
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('open', (event) => {
|
||||
this.onWebsocketOpen(this)
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('close', (event) => {
|
||||
console.log('websocket connection closed', event)
|
||||
this.close()
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('error', (event) => {
|
||||
console.log('websocket connection error', event)
|
||||
this.close()
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('message', (event) => {
|
||||
@ -115,31 +154,59 @@ export class EngineConnection {
|
||||
return
|
||||
}
|
||||
|
||||
const message: WebSocketResponse = JSON.parse(event.data)
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
||||
|
||||
if (
|
||||
message.type === 'sdp_answer' &&
|
||||
message.answer.type !== 'unspecified'
|
||||
) {
|
||||
this.pc?.setRemoteDescription(
|
||||
new RTCSessionDescription({
|
||||
type: message.answer.type,
|
||||
sdp: message.answer.sdp,
|
||||
})
|
||||
)
|
||||
} else if (message.type === 'trickle_ice') {
|
||||
this.pc?.addIceCandidate(message.candidate as RTCIceCandidateInit)
|
||||
} else if (message.type === 'ice_server_info' && this.pc) {
|
||||
if (!message.success) {
|
||||
if (message.request_id) {
|
||||
console.error(`Error in response to request ${message.request_id}:`)
|
||||
} else {
|
||||
console.error(`Error from server:`)
|
||||
}
|
||||
message.errors.forEach((error) => {
|
||||
console.error(` - ${error.error_code}: ${error.message}`)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let resp = message.resp
|
||||
if (!resp) {
|
||||
// If there's no body to the response, we can bail here.
|
||||
return
|
||||
}
|
||||
|
||||
if (resp.type === 'sdp_answer') {
|
||||
let answer = resp.data?.answer
|
||||
if (!answer || answer.type === 'unspecified') {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.pc?.signalingState !== 'stable') {
|
||||
// If the connection is stable, we shouldn't bother updating the
|
||||
// SDP, since we have a stable connection to the backend. If we
|
||||
// need to renegotiate, the whole PeerConnection needs to get
|
||||
// tore down.
|
||||
this.pc?.setRemoteDescription(
|
||||
new RTCSessionDescription({
|
||||
type: answer.type,
|
||||
sdp: answer.sdp,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (resp.type === 'trickle_ice') {
|
||||
let candidate = resp.data?.candidate
|
||||
this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
|
||||
} else if (resp.type === 'ice_server_info' && this.pc) {
|
||||
console.log('received ice_server_info')
|
||||
let ice_servers = resp.data?.ice_servers
|
||||
|
||||
if (message.ice_servers.length > 0) {
|
||||
if (ice_servers?.length > 0) {
|
||||
// When we set the Configuration, we want to always force
|
||||
// iceTransportPolicy to 'relay', since we know the topology
|
||||
// of the ICE/STUN/TUN server and the engine. We don't wish to
|
||||
// talk to the engine in any configuration /other/ than relay
|
||||
// from a infra POV.
|
||||
this.pc.setConfiguration({
|
||||
iceServers: message.ice_servers,
|
||||
iceServers: ice_servers,
|
||||
iceTransportPolicy: 'relay',
|
||||
})
|
||||
} else {
|
||||
@ -152,29 +219,21 @@ export class EngineConnection {
|
||||
// until the end of this function is setup of our end of the
|
||||
// PeerConnection and waiting for events to fire our callbacks.
|
||||
|
||||
this.pc.addEventListener('connectionstatechange', (e) =>
|
||||
console.log(this.pc?.iceConnectionState)
|
||||
)
|
||||
this.pc.addEventListener('connectionstatechange', (event) => {
|
||||
// if (this.pc?.iceConnectionState === 'disconnected') {
|
||||
// this.close()
|
||||
// }
|
||||
})
|
||||
|
||||
this.pc.addEventListener('icecandidate', (event) => {
|
||||
if (!this.pc || !this.websocket) return
|
||||
if (event.candidate === null) {
|
||||
console.log('sent sdp_offer')
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
type: 'sdp_offer',
|
||||
offer: this.pc.localDescription,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
if (event.candidate !== null) {
|
||||
console.log('sending trickle ice candidate')
|
||||
const { candidate } = event
|
||||
this.websocket?.send(
|
||||
JSON.stringify({
|
||||
type: 'trickle_ice',
|
||||
candidate: candidate.toJSON(),
|
||||
})
|
||||
)
|
||||
this.send({
|
||||
type: 'trickle_ice',
|
||||
candidate: candidate.toJSON(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -189,43 +248,90 @@ export class EngineConnection {
|
||||
.then(async (descriptionInit) => {
|
||||
await this?.pc?.setLocalDescription(descriptionInit)
|
||||
console.log('sent sdp_offer begin')
|
||||
const msg = JSON.stringify({
|
||||
this.send({
|
||||
type: 'sdp_offer',
|
||||
offer: this.pc?.localDescription,
|
||||
})
|
||||
this.websocket?.send(msg)
|
||||
})
|
||||
.catch(console.log)
|
||||
}
|
||||
|
||||
// TODO(paultag): This ought to be both controllable, as well as something
|
||||
// like exponential backoff to have some grace on the backend, as well as
|
||||
// fix responsiveness for clients that had a weird network hiccup.
|
||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.isReady()) {
|
||||
return
|
||||
}
|
||||
console.log('engine connection timeout on connection, retrying')
|
||||
this.close()
|
||||
this.connect()
|
||||
}, connectionTimeoutMs)
|
||||
})
|
||||
|
||||
this.pc.addEventListener('track', (event) => {
|
||||
console.log('received track', event)
|
||||
const mediaStream = event.streams[0]
|
||||
this.onNewTrack({
|
||||
conn: this,
|
||||
mediaStream: mediaStream,
|
||||
})
|
||||
})
|
||||
|
||||
// During startup, we'll track the time from `connect` being called
|
||||
// until the 'done' event fires.
|
||||
let connectionStarted = new Date()
|
||||
|
||||
this.pc.addEventListener('datachannel', (event) => {
|
||||
this.lossyDataChannel = event.channel
|
||||
|
||||
console.log('accepted lossy data channel', event.channel.label)
|
||||
this.lossyDataChannel.addEventListener('open', (event) => {
|
||||
this.resolveReady()
|
||||
console.log('lossy data channel opened', event)
|
||||
|
||||
this.onDataChannelOpen(this)
|
||||
|
||||
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
|
||||
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
|
||||
this.onEngineConnectionOpen(this)
|
||||
this.ready = true
|
||||
})
|
||||
|
||||
this.lossyDataChannel.addEventListener('close', (event) => {
|
||||
console.log('lossy data channel closed')
|
||||
this.close()
|
||||
})
|
||||
|
||||
this.lossyDataChannel.addEventListener('error', (event) => {
|
||||
console.log('lossy data channel error')
|
||||
this.close()
|
||||
})
|
||||
})
|
||||
|
||||
if (this.onConnectionStarted) this.onConnectionStarted(this)
|
||||
this.onConnectionStarted(this)
|
||||
}
|
||||
send(message: object) {
|
||||
// TODO(paultag): Add in logic to determine the connection state and
|
||||
// take actions if needed?
|
||||
this.websocket?.send(JSON.stringify(message))
|
||||
}
|
||||
close() {
|
||||
this.websocket?.close()
|
||||
this.pc?.close()
|
||||
this.lossyDataChannel?.close()
|
||||
this.websocket = undefined
|
||||
this.pc = undefined
|
||||
this.lossyDataChannel = undefined
|
||||
|
||||
this.onClose(this)
|
||||
this.ready = false
|
||||
}
|
||||
}
|
||||
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
|
||||
export class EngineCommandManager {
|
||||
artifactMap: ArtifactMap = {}
|
||||
sourceRangeMap: SourceRangeMap = {}
|
||||
@ -258,97 +364,106 @@ export class EngineCommandManager {
|
||||
this.engineConnection = new EngineConnection({
|
||||
url,
|
||||
token,
|
||||
onConnectionStarted: (conn) => {
|
||||
this.engineConnection?.pc?.addEventListener('track', (event) => {
|
||||
console.log('received track', event)
|
||||
const mediaStream = event.streams[0]
|
||||
setMediaStream(mediaStream)
|
||||
})
|
||||
|
||||
this.engineConnection?.pc?.addEventListener('datachannel', (event) => {
|
||||
onEngineConnectionOpen: () => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
},
|
||||
onClose: () => {
|
||||
setIsStreamReady(false)
|
||||
},
|
||||
onConnectionStarted: (engineConnection) => {
|
||||
engineConnection?.pc?.addEventListener('datachannel', (event) => {
|
||||
let lossyDataChannel = event.channel
|
||||
|
||||
lossyDataChannel.addEventListener('message', (event) => {
|
||||
const result: OkResponse = JSON.parse(event.data)
|
||||
const result: Models['OkModelingCmdResponse_type'] = JSON.parse(
|
||||
event.data
|
||||
)
|
||||
if (
|
||||
result.type === 'highlight_set_entity' &&
|
||||
result.sequence &&
|
||||
result.sequence > this.inSequence
|
||||
result?.data?.sequence &&
|
||||
result.data.sequence > this.inSequence
|
||||
) {
|
||||
this.onHoverCallback(result.entity_id)
|
||||
this.inSequence = result.sequence
|
||||
this.onHoverCallback(result.data.entity_id)
|
||||
this.inSequence = result.data.sequence
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// When the EngineConnection starts a connection, we want to register
|
||||
// callbacks into the WebSocket/PeerConnection.
|
||||
conn.websocket?.addEventListener('message', (event) => {
|
||||
engineConnection.websocket?.addEventListener('message', (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
// If the data is an ArrayBuffer, it's the result of an export command,
|
||||
// because in all other cases we send JSON strings. But in the case of
|
||||
// export we send a binary blob.
|
||||
// Pass this to our export function.
|
||||
exportSave(event.data)
|
||||
} else if (
|
||||
typeof event.data === 'string' &&
|
||||
event.data.toLocaleLowerCase().startsWith('error')
|
||||
) {
|
||||
console.warn('something went wrong: ', event.data)
|
||||
} else {
|
||||
const message: WebSocketResponse = JSON.parse(event.data)
|
||||
|
||||
if (message.type === 'modeling') {
|
||||
const id = message.cmd_id
|
||||
const command = this.artifactMap[id]
|
||||
if ('ok' in message.result) {
|
||||
const result: OkResponse = message.result.ok
|
||||
if (result.type === 'select_with_point') {
|
||||
if (result.entity_id) {
|
||||
this.onClickCallback({
|
||||
id: result.entity_id,
|
||||
type: 'default',
|
||||
})
|
||||
} else {
|
||||
this.onClickCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (command && command.type === 'pending') {
|
||||
const resolve = command.resolve
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
data: message.result,
|
||||
}
|
||||
resolve({
|
||||
id,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
data: message.result,
|
||||
}
|
||||
}
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
||||
event.data
|
||||
)
|
||||
if (
|
||||
message.success &&
|
||||
message.resp.type === 'modeling' &&
|
||||
message.request_id
|
||||
) {
|
||||
this.handleModelingCommand(message.resp, message.request_id)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
onNewTrack: ({ mediaStream }) => {
|
||||
console.log('received track', mediaStream)
|
||||
|
||||
// TODO(paultag): this isn't quite right, and the double promises is
|
||||
// pretty grim.
|
||||
this.engineConnection?.waitForReady.then(this.resolveReady)
|
||||
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
||||
console.log('peer is not sending video to us')
|
||||
this.engineConnection?.close()
|
||||
this.engineConnection?.connect()
|
||||
})
|
||||
|
||||
this.waitForReady.then(() => {
|
||||
setIsStreamReady(true)
|
||||
setMediaStream(mediaStream)
|
||||
},
|
||||
})
|
||||
|
||||
this.engineConnection?.connect()
|
||||
}
|
||||
handleModelingCommand(message: WebSocketResponse, id: string) {
|
||||
if (message.type !== 'modeling') {
|
||||
return
|
||||
}
|
||||
const modelingResponse = message.data.modeling_response
|
||||
|
||||
const command = this.artifactMap[id]
|
||||
if (modelingResponse.type === 'select_with_point') {
|
||||
if (modelingResponse?.data?.entity_id) {
|
||||
this.onClickCallback({
|
||||
id: modelingResponse?.data?.entity_id,
|
||||
type: 'default',
|
||||
})
|
||||
} else {
|
||||
this.onClickCallback()
|
||||
}
|
||||
}
|
||||
if (command && command.type === 'pending') {
|
||||
const resolve = command.resolve
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
data: modelingResponse,
|
||||
}
|
||||
resolve({
|
||||
id,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
data: modelingResponse,
|
||||
}
|
||||
}
|
||||
}
|
||||
tearDown() {
|
||||
// close all channels, sockets and WebRTC connections
|
||||
this.engineConnection?.close()
|
||||
}
|
||||
|
||||
startNewSession() {
|
||||
this.artifactMap = {}
|
||||
this.sourceRangeMap = {}
|
||||
@ -372,8 +487,8 @@ export class EngineCommandManager {
|
||||
otherSelections: Selections['otherSelections']
|
||||
idBasedSelections: { type: string; id: string }[]
|
||||
}) {
|
||||
if (this.engineConnection?.websocket?.readyState === 0) {
|
||||
console.log('socket not open')
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('engine connection isnt ready')
|
||||
return
|
||||
}
|
||||
this.sendSceneCommand({
|
||||
@ -393,7 +508,7 @@ export class EngineCommandManager {
|
||||
})
|
||||
}
|
||||
sendSceneCommand(command: EngineCommand) {
|
||||
if (this.engineConnection?.websocket?.readyState === 0) {
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('socket not ready')
|
||||
return
|
||||
}
|
||||
@ -417,26 +532,24 @@ export class EngineCommandManager {
|
||||
return
|
||||
}
|
||||
console.log('sending command', command)
|
||||
this.engineConnection?.websocket?.send(JSON.stringify(command))
|
||||
this.engineConnection?.send(command)
|
||||
}
|
||||
sendModellingCommand({
|
||||
sendModelingCommand({
|
||||
id,
|
||||
params,
|
||||
range,
|
||||
command,
|
||||
}: {
|
||||
id: string
|
||||
params: any
|
||||
range: SourceRange
|
||||
command: EngineCommand
|
||||
}): Promise<any> {
|
||||
this.sourceRangeMap[id] = range
|
||||
|
||||
if (this.engineConnection?.websocket?.readyState === 0) {
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('socket not ready')
|
||||
return new Promise(() => {})
|
||||
}
|
||||
this.engineConnection?.websocket?.send(JSON.stringify(command))
|
||||
this.engineConnection?.send(command)
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
@ -448,6 +561,25 @@ export class EngineCommandManager {
|
||||
}
|
||||
return promise
|
||||
}
|
||||
sendModelingCommandFromWasm(
|
||||
id: string,
|
||||
rangeStr: string,
|
||||
commandStr: string
|
||||
): Promise<any> {
|
||||
if (id === undefined) {
|
||||
throw new Error('id is undefined')
|
||||
}
|
||||
if (rangeStr === undefined) {
|
||||
throw new Error('rangeStr is undefined')
|
||||
}
|
||||
if (commandStr === undefined) {
|
||||
throw new Error('commandStr is undefined')
|
||||
}
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
const range: SourceRange = JSON.parse(rangeStr)
|
||||
|
||||
return this.sendModelingCommand({ id, range, command })
|
||||
}
|
||||
commandResult(id: string): Promise<any> {
|
||||
const command = this.artifactMap[id]
|
||||
if (!command) {
|
||||
|
@ -1,88 +0,0 @@
|
||||
import { InternalFn } from './stdTypes'
|
||||
import {
|
||||
ExtrudeGroup,
|
||||
ExtrudeSurface,
|
||||
SketchGroup,
|
||||
Position,
|
||||
Rotation,
|
||||
} from '../executor'
|
||||
import { clockwiseSign } from './std'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
export const extrude: InternalFn = (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
length: number,
|
||||
sketchVal: SketchGroup
|
||||
): ExtrudeGroup => {
|
||||
const sketch = sketchVal
|
||||
const { position, rotation } = sketchVal
|
||||
|
||||
const id = generateUuidFromHashSeed(
|
||||
JSON.stringify({
|
||||
code,
|
||||
sourceRange,
|
||||
data: {
|
||||
length,
|
||||
sketchVal,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const extrudeSurfaces: ExtrudeSurface[] = []
|
||||
const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to))
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id,
|
||||
params: [
|
||||
{
|
||||
length,
|
||||
extrusionDirection: extrusionDirection,
|
||||
},
|
||||
],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'extrude',
|
||||
target: sketch.id,
|
||||
distance: length,
|
||||
cap: true,
|
||||
},
|
||||
cmd_id: id,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
type: 'extrudeGroup',
|
||||
id,
|
||||
value: extrudeSurfaces, // TODO, this is just an empty array now, should be deleted.
|
||||
height: length,
|
||||
position,
|
||||
rotation,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
{
|
||||
sourceRange: sketchVal.__meta[0].sourceRange,
|
||||
pathToNode: sketchVal.__meta[0].pathToNode,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export const getExtrudeWallTransform: InternalFn = (
|
||||
_,
|
||||
pathName: string,
|
||||
extrudeGroup: ExtrudeGroup
|
||||
): {
|
||||
position: Position
|
||||
quaternion: Rotation
|
||||
} => {
|
||||
const path = extrudeGroup?.value.find((path) => path.name === pathName)
|
||||
if (!path) throw new Error(`Could not find path with name ${pathName}`)
|
||||
return {
|
||||
position: path.position,
|
||||
quaternion: path.rotation,
|
||||
}
|
||||
}
|
@ -23,12 +23,7 @@ import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
import {
|
||||
SketchLineHelper,
|
||||
ModifyAstBase,
|
||||
InternalFn,
|
||||
TransformCallback,
|
||||
} from './stdTypes'
|
||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||
|
||||
import {
|
||||
createLiteral,
|
||||
@ -42,10 +37,7 @@ import {
|
||||
} from '../modifyAst'
|
||||
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import {
|
||||
intersectionWithParallelLine,
|
||||
perpendicularDistance,
|
||||
} from 'sketch-helpers'
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
|
||||
export type Coords2d = [number, number]
|
||||
|
||||
@ -115,45 +107,6 @@ function makeId(seed: string | any) {
|
||||
}
|
||||
|
||||
export const lineTo: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
to: [number, number]
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
): SketchGroup => {
|
||||
if (!previousSketch)
|
||||
throw new Error('lineTo must be called after startSketchAt')
|
||||
const sketchGroup = { ...previousSketch }
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
const to = 'to' in data ? data.to : data
|
||||
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
sourceRange,
|
||||
id,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if ('tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: [...sketchGroup.value, currentPath],
|
||||
}
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -221,77 +174,6 @@ export const lineTo: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const line: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| 'default'
|
||||
| {
|
||||
to: [number, number] | 'default'
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
): SketchGroup => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const sketchGroup = { ...previousSketch }
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
let args: [number, number] = [0.2, 1]
|
||||
if (data !== 'default' && 'to' in data && data.to !== 'default') {
|
||||
args = data.to
|
||||
} else if (data !== 'default' && !('to' in data)) {
|
||||
args = data
|
||||
}
|
||||
|
||||
const to: [number, number] = [from[0] + args[0], from[1] + args[1]]
|
||||
const lineData: LineData = {
|
||||
from: [...from, 0],
|
||||
to: [...to, 0],
|
||||
}
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id,
|
||||
params: [lineData, previousSketch],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'extend_path',
|
||||
path: sketchGroup.id,
|
||||
segment: {
|
||||
type: 'line',
|
||||
end: {
|
||||
x: lineData.to[0],
|
||||
y: lineData.to[1],
|
||||
z: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
cmd_id: id,
|
||||
},
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if (data !== 'default' && 'tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: [...sketchGroup.value, currentPath],
|
||||
}
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -385,25 +267,6 @@ export const line: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const xLineTo: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
to: number
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [xVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
|
||||
return lineTo.fn(meta, { to: [xVal, from[1]], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -452,25 +315,6 @@ export const xLineTo: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const yLineTo: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
to: number
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [yVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
|
||||
return lineTo.fn(meta, { to: [from[0], yVal], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -519,21 +363,6 @@ export const yLineTo: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const xLine: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const [xVal, tag] =
|
||||
typeof data !== 'number' ? [data.length, data.tag] : [data]
|
||||
return line.fn(meta, { to: [xVal, 0], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -584,22 +413,6 @@ export const xLine: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const yLine: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
length: number
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const [yVal, tag] =
|
||||
typeof data !== 'number' ? [data.length, data.tag] : [data]
|
||||
return line.fn(meta, { to: [0, yVal], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -644,48 +457,6 @@ export const yLine: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLine: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const sketchGroup = { ...previousSketch }
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
const [angle, length] = 'angle' in data ? [data.angle, data.length] : data
|
||||
const to: [number, number] = [
|
||||
from[0] + length * Math.cos((angle * Math.PI) / 180),
|
||||
from[1] + length * Math.sin((angle * Math.PI) / 180),
|
||||
]
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if ('tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: [...sketchGroup.value, currentPath],
|
||||
}
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -753,26 +524,6 @@ export const angledLine: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineOfXLength: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const [angle, length, tag] =
|
||||
'angle' in data ? [data.angle, data.length, data.tag] : data
|
||||
return line.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: getYComponent(angle, length), tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -846,26 +597,6 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineOfYLength: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const [angle, length, tag] =
|
||||
'angle' in data ? [data.angle, data.length, data.tag] : data
|
||||
return line.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: getXComponent(angle, length), tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -940,33 +671,6 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineToX: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
to: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [angle, xTo, tag] =
|
||||
'angle' in data ? [data.angle, data.to, data.tag] : data
|
||||
const xComponent = xTo - from[0]
|
||||
const yComponent = xComponent * Math.tan((angle * Math.PI) / 180)
|
||||
const yTo = from[1] + yComponent
|
||||
return lineTo.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: [xTo, yTo], tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -1036,33 +740,6 @@ export const angledLineToX: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineToY: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
to: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [angle, yTo, tag] =
|
||||
'angle' in data ? [data.angle, data.to, data.tag] : data
|
||||
const yComponent = yTo - from[1]
|
||||
const xComponent = yComponent / Math.tan((angle * Math.PI) / 180)
|
||||
const xTo = from[0] + xComponent
|
||||
return lineTo.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: [xTo, yTo], tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -1133,37 +810,6 @@ export const angledLineToY: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineThatIntersects: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data: {
|
||||
angle: number
|
||||
intersectTag: string
|
||||
offset?: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const intersectPath = previousSketch.value.find(
|
||||
({ name }) => name === data.intersectTag
|
||||
)
|
||||
if (!intersectPath) throw new Error('intersectTag must match a line')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const to = intersectionWithParallelLine({
|
||||
line1: [intersectPath.from, intersectPath.to],
|
||||
line1Offset: data.offset || 0,
|
||||
line2Point: from,
|
||||
line2Angle: data.angle,
|
||||
})
|
||||
return lineTo.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to, tag: data.tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -1526,142 +1172,6 @@ function addTagWithTo(
|
||||
}
|
||||
}
|
||||
|
||||
export const close: InternalFn = (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
sketchGroup: SketchGroup
|
||||
): SketchGroup => {
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
const to = sketchGroup.start
|
||||
? sketchGroup.start.from
|
||||
: getCoordsFromPaths(sketchGroup, 0)
|
||||
|
||||
const lineData: LineData = {
|
||||
from: [...from, 0],
|
||||
to: [...to, 0],
|
||||
}
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data: sketchGroup,
|
||||
})
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id,
|
||||
params: [lineData],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'close_path',
|
||||
path_id: sketchGroup.id,
|
||||
},
|
||||
cmd_id: id,
|
||||
},
|
||||
})
|
||||
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
const newValue = [...sketchGroup.value]
|
||||
newValue.push(currentPath)
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: newValue,
|
||||
}
|
||||
}
|
||||
|
||||
export const startSketchAt: InternalFn = (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| 'default'
|
||||
| {
|
||||
to: [number, number] | 'default'
|
||||
// name?: string
|
||||
tag?: string
|
||||
}
|
||||
): SketchGroup => {
|
||||
let to: [number, number] = [0, 0]
|
||||
if (data !== 'default' && 'to' in data && data.to !== 'default') {
|
||||
to = data.to
|
||||
} else if (data !== 'default' && !('to' in data)) {
|
||||
to = data
|
||||
}
|
||||
|
||||
const lineData: { to: [number, number, number] } = {
|
||||
to: [...to, 0],
|
||||
}
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
const pathId = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
isPath: true,
|
||||
})
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id: pathId,
|
||||
params: [lineData],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
cmd_id: pathId,
|
||||
},
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'move_path_pen',
|
||||
path: pathId,
|
||||
to: {
|
||||
x: lineData.to[0],
|
||||
y: lineData.to[1],
|
||||
z: 0,
|
||||
},
|
||||
},
|
||||
cmd_id: id,
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'base',
|
||||
to,
|
||||
from: to,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if (data !== 'default' && 'tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
type: 'sketchGroup',
|
||||
start: currentPath,
|
||||
value: [],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
id: pathId,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function getYComponent(
|
||||
angleDegree: number,
|
||||
xComponent: number
|
||||
|
@ -391,6 +391,7 @@ show(part001)`
|
||||
type: 'toPoint',
|
||||
to: [5.62, 1.79],
|
||||
from: [3.48, 0.44],
|
||||
name: '',
|
||||
})
|
||||
})
|
||||
it('verify it works when the segment is in the `start` property', async () => {
|
||||
@ -400,6 +401,6 @@ show(part001)`
|
||||
programMemory.root['part001'] as SketchGroup,
|
||||
[index, index]
|
||||
).segment
|
||||
expect(segment).toEqual({ type: 'base', to: [0, 0.04], from: [0, 0.04] })
|
||||
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
|
||||
})
|
||||
})
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { getAngle } from '../../lib/utils'
|
||||
import { TooTip, toolTips } from '../../useStore'
|
||||
import {
|
||||
Program,
|
||||
@ -6,7 +5,6 @@ import {
|
||||
CallExpression,
|
||||
} from '../abstractSyntaxTreeTypes'
|
||||
import { SketchGroup, SourceRange } from '../executor'
|
||||
import { InternalFn } from './stdTypes'
|
||||
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
sketchGroup: SketchGroup,
|
||||
@ -36,79 +34,6 @@ export function getSketchSegmentFromSourceRange(
|
||||
}
|
||||
}
|
||||
|
||||
export const segLen: InternalFn = (
|
||||
_,
|
||||
segName: string,
|
||||
sketchGroup: SketchGroup
|
||||
): number => {
|
||||
const line = sketchGroup?.value.find((seg) => seg?.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!line) return 0
|
||||
|
||||
return Math.sqrt(
|
||||
(line.from[1] - line.to[1]) ** 2 + (line.from[0] - line.to[0]) ** 2
|
||||
)
|
||||
}
|
||||
|
||||
export const segAng: InternalFn = (
|
||||
_,
|
||||
segName: string,
|
||||
sketchGroup: SketchGroup
|
||||
): number => {
|
||||
const line = sketchGroup?.value.find((seg) => seg.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!line) return 0
|
||||
return getAngle(line.from, line.to)
|
||||
}
|
||||
|
||||
function segEndFactory(which: 'x' | 'y'): InternalFn {
|
||||
return (_, segName: string, sketchGroup: SketchGroup): number => {
|
||||
const line =
|
||||
sketchGroup?.start?.name === segName
|
||||
? sketchGroup?.start
|
||||
: sketchGroup?.value.find((seg) => seg.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!line) return 0
|
||||
return which === 'x' ? line.to[0] : line.to[1]
|
||||
}
|
||||
}
|
||||
|
||||
export const segEndX: InternalFn = segEndFactory('x')
|
||||
export const segEndY: InternalFn = segEndFactory('y')
|
||||
|
||||
function lastSegFactory(which: 'x' | 'y'): InternalFn {
|
||||
return (_, sketchGroup: SketchGroup): number => {
|
||||
const lastLine = sketchGroup?.value[sketchGroup.value.length - 1]
|
||||
return which === 'x' ? lastLine.to[0] : lastLine.to[1]
|
||||
}
|
||||
}
|
||||
|
||||
export const lastSegX: InternalFn = lastSegFactory('x')
|
||||
export const lastSegY: InternalFn = lastSegFactory('y')
|
||||
|
||||
function angleToMatchLengthFactory(which: 'x' | 'y'): InternalFn {
|
||||
return (_, segName: string, to: number, sketchGroup: SketchGroup): number => {
|
||||
const isX = which === 'x'
|
||||
const lineToMatch = sketchGroup?.value.find((seg) => seg.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!lineToMatch) return 0
|
||||
const lengthToMatch = Math.sqrt(
|
||||
(lineToMatch.from[1] - lineToMatch.to[1]) ** 2 +
|
||||
(lineToMatch.from[0] - lineToMatch.to[0]) ** 2
|
||||
)
|
||||
|
||||
const lastLine = sketchGroup?.value[sketchGroup.value.length - 1]
|
||||
const diff = Math.abs(to - (isX ? lastLine.to[0] : lastLine.to[1]))
|
||||
|
||||
const angleR = Math[isX ? 'acos' : 'asin'](diff / lengthToMatch)
|
||||
|
||||
return diff > lengthToMatch ? 0 : (angleR * 180) / Math.PI
|
||||
}
|
||||
}
|
||||
|
||||
export const angleToMatchLengthX: InternalFn = angleToMatchLengthFactory('x')
|
||||
export const angleToMatchLengthY: InternalFn = angleToMatchLengthFactory('y')
|
||||
|
||||
export function isSketchVariablesLinked(
|
||||
secondaryVarDec: VariableDeclarator,
|
||||
primaryVarDec: VariableDeclarator,
|
||||
|
@ -1,170 +0,0 @@
|
||||
import {
|
||||
lineTo,
|
||||
xLineTo,
|
||||
yLineTo,
|
||||
line,
|
||||
xLine,
|
||||
yLine,
|
||||
angledLine,
|
||||
angledLineOfXLength,
|
||||
angledLineToX,
|
||||
angledLineOfYLength,
|
||||
angledLineToY,
|
||||
close,
|
||||
startSketchAt,
|
||||
angledLineThatIntersects,
|
||||
} from './sketch'
|
||||
import {
|
||||
segLen,
|
||||
segAng,
|
||||
angleToMatchLengthX,
|
||||
angleToMatchLengthY,
|
||||
segEndX,
|
||||
segEndY,
|
||||
lastSegX,
|
||||
lastSegY,
|
||||
} from './sketchConstraints'
|
||||
import { getExtrudeWallTransform, extrude } from './extrude'
|
||||
|
||||
import { InternalFn, InternalFnNames } from './stdTypes'
|
||||
|
||||
// const transform: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
|
||||
// { sourceRange }: InternalFirstArg,
|
||||
// transformInfo: {
|
||||
// position: Position
|
||||
// quaternion: Rotation
|
||||
// },
|
||||
// sketch: T
|
||||
// ): T => {
|
||||
// const quaternionToApply = new Quaternion(...transformInfo?.quaternion)
|
||||
// const newQuaternion = new Quaternion(...sketch.rotation).multiply(
|
||||
// quaternionToApply.invert()
|
||||
// )
|
||||
|
||||
// const oldPosition = new Vector3(...sketch?.position)
|
||||
// const newPosition = oldPosition
|
||||
// .applyQuaternion(quaternionToApply)
|
||||
// .add(new Vector3(...transformInfo?.position))
|
||||
// return {
|
||||
// ...sketch,
|
||||
// position: newPosition.toArray(),
|
||||
// rotation: newQuaternion.toArray(),
|
||||
// __meta: [
|
||||
// ...sketch.__meta,
|
||||
// {
|
||||
// sourceRange,
|
||||
// pathToNode: [], // TODO
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
|
||||
// const translate: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
|
||||
// { sourceRange }: InternalFirstArg,
|
||||
// vec3: [number, number, number],
|
||||
// sketch: T
|
||||
// ): T => {
|
||||
// const oldPosition = new Vector3(...sketch.position)
|
||||
// const newPosition = oldPosition.add(new Vector3(...vec3))
|
||||
// return {
|
||||
// ...sketch,
|
||||
// position: newPosition.toArray(),
|
||||
// __meta: [
|
||||
// ...sketch.__meta,
|
||||
// {
|
||||
// sourceRange,
|
||||
// pathToNode: [], // TODO
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
|
||||
const min: InternalFn = (_, a: number, b: number): number => Math.min(a, b)
|
||||
|
||||
const legLen: InternalFn = (_, hypotenuse: number, leg: number): number =>
|
||||
Math.sqrt(
|
||||
hypotenuse ** 2 - Math.min(Math.abs(leg), Math.abs(hypotenuse)) ** 2
|
||||
)
|
||||
|
||||
const legAngX: InternalFn = (_, hypotenuse: number, leg: number): number =>
|
||||
(Math.acos(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI
|
||||
|
||||
const legAngY: InternalFn = (_, hypotenuse: number, leg: number): number =>
|
||||
(Math.asin(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI
|
||||
|
||||
export const internalFns: { [key in InternalFnNames]: InternalFn } = {
|
||||
// TODO - re-enable these
|
||||
// rx: rotateOnAxis([1, 0, 0]), // Enable rotations #152
|
||||
// ry: rotateOnAxis([0, 1, 0]),
|
||||
// rz: rotateOnAxis([0, 0, 1]),
|
||||
extrude,
|
||||
// translate,
|
||||
// transform,
|
||||
getExtrudeWallTransform,
|
||||
min,
|
||||
legLen,
|
||||
legAngX,
|
||||
legAngY,
|
||||
segEndX,
|
||||
segEndY,
|
||||
lastSegX,
|
||||
lastSegY,
|
||||
segLen,
|
||||
segAng,
|
||||
angleToMatchLengthX,
|
||||
angleToMatchLengthY,
|
||||
lineTo: lineTo.fn,
|
||||
xLineTo: xLineTo.fn,
|
||||
yLineTo: yLineTo.fn,
|
||||
line: line.fn,
|
||||
xLine: xLine.fn,
|
||||
yLine: yLine.fn,
|
||||
angledLine: angledLine.fn,
|
||||
angledLineOfXLength: angledLineOfXLength.fn,
|
||||
angledLineToX: angledLineToX.fn,
|
||||
angledLineOfYLength: angledLineOfYLength.fn,
|
||||
angledLineToY: angledLineToY.fn,
|
||||
angledLineThatIntersects: angledLineThatIntersects.fn,
|
||||
startSketchAt,
|
||||
close,
|
||||
}
|
||||
|
||||
// function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>(
|
||||
// axisMultiplier: [number, number, number]
|
||||
// ): InternalFn {
|
||||
// return ({ sourceRange }, rotationD: number, sketch: T): T => {
|
||||
// const rotationR = rotationD * (Math.PI / 180)
|
||||
// const rotateVec = new Vector3(...axisMultiplier)
|
||||
// const quaternion = new Quaternion()
|
||||
// quaternion.setFromAxisAngle(rotateVec, rotationR)
|
||||
|
||||
// const position = new Vector3(...sketch.position)
|
||||
// .applyQuaternion(quaternion)
|
||||
// .toArray()
|
||||
|
||||
// const existingQuat = new Quaternion(...sketch.rotation)
|
||||
// const rotation = quaternion.multiply(existingQuat).toArray()
|
||||
// return {
|
||||
// ...sketch,
|
||||
// rotation,
|
||||
// position,
|
||||
// __meta: [
|
||||
// ...sketch.__meta,
|
||||
// {
|
||||
// sourceRange,
|
||||
// pathToNode: [], // TODO
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export function clockwiseSign(points: [number, number][]): number {
|
||||
let sum = 0
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const currentPoint = points[i]
|
||||
const nextPoint = points[(i + 1) % points.length]
|
||||
sum += (nextPoint[0] - currentPoint[0]) * (nextPoint[1] + currentPoint[1])
|
||||
}
|
||||
return sum >= 0 ? 1 : -1
|
||||
}
|
@ -17,44 +17,6 @@ export interface PathReturn {
|
||||
currentPath: Path
|
||||
}
|
||||
|
||||
export type InternalFn = (internals: InternalFirstArg, ...args: any[]) => any
|
||||
|
||||
export type InternalFnNames =
|
||||
// TODO re-enable these
|
||||
// | 'translate'
|
||||
// | 'transform'
|
||||
// | 'rx' // Enable rotations #152
|
||||
// | 'ry'
|
||||
// | 'rz'
|
||||
| 'extrude'
|
||||
| 'getExtrudeWallTransform'
|
||||
| 'min'
|
||||
| 'legLen'
|
||||
| 'legAngX'
|
||||
| 'legAngY'
|
||||
| 'segEndX'
|
||||
| 'segEndY'
|
||||
| 'lastSegX'
|
||||
| 'lastSegY'
|
||||
| 'segLen'
|
||||
| 'segAng'
|
||||
| 'angleToMatchLengthX'
|
||||
| 'angleToMatchLengthY'
|
||||
| 'lineTo'
|
||||
| 'yLineTo'
|
||||
| 'xLineTo'
|
||||
| 'line'
|
||||
| 'yLine'
|
||||
| 'xLine'
|
||||
| 'angledLine'
|
||||
| 'angledLineOfXLength'
|
||||
| 'angledLineToX'
|
||||
| 'angledLineOfYLength'
|
||||
| 'angledLineToY'
|
||||
| 'startSketchAt'
|
||||
| 'close'
|
||||
| 'angledLineThatIntersects'
|
||||
|
||||
export interface ModifyAstBase {
|
||||
node: Program
|
||||
previousProgramMemory: ProgramMemory
|
||||
@ -87,7 +49,6 @@ export type SketchCallTransfromMap = {
|
||||
}
|
||||
|
||||
export interface SketchLineHelper {
|
||||
fn: InternalFn
|
||||
add: (a: addCall) => {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
|
@ -26,3 +26,12 @@ export function updateCursors(
|
||||
setCursor(newSelections)
|
||||
}
|
||||
}
|
||||
|
||||
export function isReducedMotion(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia &&
|
||||
// TODO/Note I (Kurt) think '(prefers-reduced-motion: reduce)' and '(prefers-reduced-motion)' are equivalent, but not 100% sure
|
||||
window.matchMedia('(prefers-reduced-motion)').matches
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { Program } from '../lang/abstractSyntaxTreeTypes'
|
||||
import { ProgramMemory, _executor } from '../lang/executor'
|
||||
import { EngineCommandManager } from '../lang/std/engineConnection'
|
||||
import {
|
||||
EngineCommandManager,
|
||||
EngineCommand,
|
||||
} from '../lang/std/engineConnection'
|
||||
import { SourceRange } from 'lang/executor'
|
||||
|
||||
class MockEngineCommandManager {
|
||||
constructor(mockParams: {
|
||||
@ -10,13 +14,43 @@ class MockEngineCommandManager {
|
||||
startNewSession() {}
|
||||
waitForAllCommands() {}
|
||||
waitForReady = new Promise<void>((resolve) => resolve())
|
||||
sendModellingCommand() {}
|
||||
sendModelingCommand({
|
||||
id,
|
||||
range,
|
||||
command,
|
||||
}: {
|
||||
id: string
|
||||
range: SourceRange
|
||||
command: EngineCommand
|
||||
}): Promise<any> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
sendModelingCommandFromWasm(
|
||||
id: string,
|
||||
rangeStr: string,
|
||||
commandStr: string
|
||||
): Promise<any> {
|
||||
if (id === undefined) {
|
||||
throw new Error('id is undefined')
|
||||
}
|
||||
if (rangeStr === undefined) {
|
||||
throw new Error('rangeStr is undefined')
|
||||
}
|
||||
if (commandStr === undefined) {
|
||||
throw new Error('commandStr is undefined')
|
||||
}
|
||||
console.log('sendModelingCommandFromWasm', id, rangeStr, commandStr)
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
const range: SourceRange = JSON.parse(rangeStr)
|
||||
|
||||
return this.sendModelingCommand({ id, range, command })
|
||||
}
|
||||
sendSceneCommand() {}
|
||||
}
|
||||
|
||||
export async function enginelessExecutor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {}, pendingMemory: {} }
|
||||
pm: ProgramMemory = { root: {} }
|
||||
): Promise<ProgramMemory> {
|
||||
const mockEngineCommandManager = new MockEngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
@ -31,7 +65,7 @@ export async function enginelessExecutor(
|
||||
|
||||
export async function executor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {}, pendingMemory: {} }
|
||||
pm: ProgramMemory = { root: {} }
|
||||
): Promise<ProgramMemory> {
|
||||
const engineCommandManager = new EngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
|
1348
src/wasm-lib/Cargo.lock
generated
1348
src/wasm-lib/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,20 +11,43 @@ crate-type = ["cdylib"]
|
||||
anyhow = "1.0.75"
|
||||
backtrace = "0.3"
|
||||
bincode = "1.3.3"
|
||||
derive-docs = { path = "derive-docs" }
|
||||
futures = { version = "0.3.28", optional = true }
|
||||
gloo-file = { version = "0.3.0", optional = true }
|
||||
gloo-utils = "0.2.0"
|
||||
http = "0.2.9"
|
||||
httparse = { version = "1.8.0", optional = true }
|
||||
js-sys = { version = "0.3.64", optional = true }
|
||||
kittycad = { version = "0.2.15", default-features = false, features = ["js"] }
|
||||
lazy_static = "1.4.0"
|
||||
parse-display = "0.8.2"
|
||||
regex = "1.7.1"
|
||||
schemars = { version = "0.8", features = ["url", "uuid1"] }
|
||||
serde = {version = "1.0.152", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.3.0"
|
||||
serde_json = "1.0.93"
|
||||
thiserror = "1.0.47"
|
||||
tokio = { version = "1.32.0", features = ["full"], optional = true }
|
||||
tokio-tungstenite = { version = "0.20.0", optional = true }
|
||||
ts-rs = { git = "https://github.com/kittycad/ts-rs.git", branch = "serde_json", features = ["serde-json-impl", "uuid-impl"] }
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-futures = "0.4.37"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
debug = true
|
||||
|
||||
[dev-dependencies]
|
||||
expectorate = "1.0.7"
|
||||
pretty_assertions = "1.4.0"
|
||||
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
web = ["dep:gloo-file", "dep:js-sys"]
|
||||
noweb = ["dep:futures", "dep:httparse", "dep:tokio", "dep:tokio-tungstenite"]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"derive-docs"
|
||||
]
|
||||
|
23
src/wasm-lib/derive-docs/Cargo.toml
Normal file
23
src/wasm-lib/derive-docs/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = "0.6.0"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.29", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
expectorate = "1.0.7"
|
||||
openapitor = "0.0.5"
|
||||
pretty_assertions = "1.4.0"
|
548
src/wasm-lib/derive-docs/src/lib.rs
Normal file
548
src/wasm-lib/derive-docs/src/lib.rs
Normal file
@ -0,0 +1,548 @@
|
||||
// Copyright 2023 Oxide Computer Company
|
||||
|
||||
//! This package defines macro attributes associated with HTTP handlers. These
|
||||
//! attributes are used both to define an HTTP API and to generate an OpenAPI
|
||||
//! Spec (OAS) v3 document that describes the API.
|
||||
|
||||
// Clippy's style advice is definitely valuable, but not worth the trouble for
|
||||
// automated enforcement.
|
||||
#![allow(clippy::style)]
|
||||
|
||||
use convert_case::Casing;
|
||||
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
||||
use serde::Deserialize;
|
||||
use serde_tokenstream::{from_tokenstream, Error};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Attribute, Signature, Visibility,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StdlibMetadata {
|
||||
/// The name of the function in the API.
|
||||
name: String,
|
||||
/// Tags for the function.
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
/// Whether the function is unpublished.
|
||||
/// Then docs will not be generated.
|
||||
#[serde(default)]
|
||||
unpublished: bool,
|
||||
/// Whether the function is deprecated.
|
||||
/// Then specific docs detailing that this is deprecated will be generated.
|
||||
#[serde(default)]
|
||||
deprecated: bool,
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn stdlib(
|
||||
attr: proc_macro::TokenStream,
|
||||
item: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
do_output(do_stdlib(attr.into(), item.into()))
|
||||
}
|
||||
|
||||
fn do_stdlib(
|
||||
attr: proc_macro2::TokenStream,
|
||||
item: proc_macro2::TokenStream,
|
||||
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
|
||||
let metadata = from_tokenstream(&attr)?;
|
||||
do_stdlib_inner(metadata, attr, item)
|
||||
}
|
||||
|
||||
fn do_output(
|
||||
res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>,
|
||||
) -> proc_macro::TokenStream {
|
||||
match res {
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
Ok((stdlib_docs, errors)) => {
|
||||
let compiler_errors = errors.iter().map(|err| err.to_compile_error());
|
||||
|
||||
let output = quote! {
|
||||
#stdlib_docs
|
||||
#( #compiler_errors )*
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_stdlib_inner(
|
||||
metadata: StdlibMetadata,
|
||||
_attr: proc_macro2::TokenStream,
|
||||
item: proc_macro2::TokenStream,
|
||||
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
|
||||
let ast: ItemFnForSignature = syn::parse2(item.clone())?;
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if ast.sig.constness.is_some() {
|
||||
errors.push(Error::new_spanned(
|
||||
&ast.sig.constness,
|
||||
"stdlib functions may not be const functions",
|
||||
));
|
||||
}
|
||||
|
||||
if ast.sig.asyncness.is_some() {
|
||||
errors.push(Error::new_spanned(
|
||||
&ast.sig.fn_token,
|
||||
"stdlib functions must not be async",
|
||||
));
|
||||
}
|
||||
|
||||
if ast.sig.unsafety.is_some() {
|
||||
errors.push(Error::new_spanned(
|
||||
&ast.sig.unsafety,
|
||||
"stdlib functions may not be unsafe",
|
||||
));
|
||||
}
|
||||
|
||||
if ast.sig.abi.is_some() {
|
||||
errors.push(Error::new_spanned(
|
||||
&ast.sig.abi,
|
||||
"stdlib functions may not use an alternate ABI",
|
||||
));
|
||||
}
|
||||
|
||||
if !ast.sig.generics.params.is_empty() {
|
||||
errors.push(Error::new_spanned(
|
||||
&ast.sig.generics,
|
||||
"generics are not permitted for stdlib functions",
|
||||
));
|
||||
}
|
||||
|
||||
if ast.sig.variadic.is_some() {
|
||||
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
|
||||
}
|
||||
|
||||
let name = metadata.name;
|
||||
let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
|
||||
let name_str = name.to_string();
|
||||
|
||||
let fn_name = &ast.sig.ident;
|
||||
let fn_name_str = fn_name.to_string().replace("inner_", "");
|
||||
let fn_name_ident = format_ident!("{}", fn_name_str);
|
||||
let _visibility = &ast.vis;
|
||||
|
||||
let (summary_text, description_text) = extract_doc_from_attrs(&ast.attrs);
|
||||
let comment_text = {
|
||||
let mut buf = String::new();
|
||||
buf.push_str("Std lib function: ");
|
||||
buf.push_str(&name_str);
|
||||
if let Some(s) = &summary_text {
|
||||
buf.push_str("\n");
|
||||
buf.push_str(&s);
|
||||
}
|
||||
if let Some(s) = &description_text {
|
||||
buf.push_str("\n");
|
||||
buf.push_str(&s);
|
||||
}
|
||||
buf
|
||||
};
|
||||
let description_doc_comment = quote! {
|
||||
#[doc = #comment_text]
|
||||
};
|
||||
|
||||
let summary = if let Some(summary) = summary_text {
|
||||
quote! { #summary }
|
||||
} else {
|
||||
quote! { "" }
|
||||
};
|
||||
let description = if let Some(description) = description_text {
|
||||
quote! { #description }
|
||||
} else {
|
||||
quote! { "" }
|
||||
};
|
||||
|
||||
let tags = metadata
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| {
|
||||
quote! { #tag.to_string() }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let deprecated = if metadata.deprecated {
|
||||
quote! { true }
|
||||
} else {
|
||||
quote! { false }
|
||||
};
|
||||
|
||||
let unpublished = if metadata.unpublished {
|
||||
quote! { true }
|
||||
} else {
|
||||
quote! { false }
|
||||
};
|
||||
|
||||
let docs_crate = get_crate(None);
|
||||
|
||||
// When the user attaches this proc macro to a function with the wrong type
|
||||
// signature, the resulting errors can be deeply inscrutable. To attempt to
|
||||
// make failures easier to understand, we inject code that asserts the types
|
||||
// of the various parameters. We do this by calling dummy functions that
|
||||
// require a type that satisfies SharedExtractor or ExclusiveExtractor.
|
||||
let mut arg_types = Vec::new();
|
||||
for arg in ast.sig.inputs.iter() {
|
||||
// Get the name of the argument.
|
||||
let arg_name = match arg {
|
||||
syn::FnArg::Receiver(pat) => {
|
||||
let span = pat.self_token.span.unwrap();
|
||||
span.source_text().unwrap().to_string()
|
||||
}
|
||||
syn::FnArg::Typed(pat) => match &*pat.pat {
|
||||
syn::Pat::Ident(ident) => ident.ident.to_string(),
|
||||
_ => {
|
||||
errors.push(Error::new_spanned(
|
||||
&pat.pat,
|
||||
"stdlib functions may not use destructuring patterns",
|
||||
));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let ty = match arg {
|
||||
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
|
||||
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
|
||||
};
|
||||
|
||||
let ty_string = ty
|
||||
.to_string()
|
||||
.replace('&', "")
|
||||
.replace("mut", "")
|
||||
.replace(' ', "");
|
||||
let ty_string = ty_string.trim().to_string();
|
||||
let ty_ident = if ty_string.starts_with("Vec<") {
|
||||
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
|
||||
let ty_ident = format_ident!("{}", ty_string);
|
||||
quote! {
|
||||
Vec<#ty_ident>
|
||||
}
|
||||
} else {
|
||||
let ty_ident = format_ident!("{}", ty_string);
|
||||
quote! {
|
||||
#ty_ident
|
||||
}
|
||||
};
|
||||
|
||||
let ty_string = clean_type(&ty_string);
|
||||
|
||||
if ty_string != "Args" {
|
||||
let schema = if ty_ident.to_string().starts_with("Vec < ") {
|
||||
quote! {
|
||||
<#ty_ident>::json_schema(&mut generator)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#ty_ident::json_schema(&mut generator)
|
||||
}
|
||||
};
|
||||
arg_types.push(quote! {
|
||||
#docs_crate::StdLibFnArg {
|
||||
name: #arg_name.to_string(),
|
||||
type_: #ty_string.to_string(),
|
||||
schema: #schema,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let ret_ty = ast.sig.output.clone();
|
||||
let ret_ty_string = ret_ty
|
||||
.into_token_stream()
|
||||
.to_string()
|
||||
.replace("-> ", "")
|
||||
.replace("Result < ", "")
|
||||
.replace(", KclError >", "");
|
||||
let ret_ty_string = ret_ty_string.trim().to_string();
|
||||
let ret_ty_ident = format_ident!("{}", ret_ty_string);
|
||||
let ret_ty_string = clean_type(&ret_ty_string);
|
||||
let return_type = quote! {
|
||||
#docs_crate::StdLibFnArg {
|
||||
name: "".to_string(),
|
||||
type_: #ret_ty_string.to_string(),
|
||||
schema: #ret_ty_ident::json_schema(&mut generator),
|
||||
required: true,
|
||||
}
|
||||
};
|
||||
|
||||
// For reasons that are not well understood unused constants that use the
|
||||
// (default) call_site() Span do not trigger the dead_code lint. Because
|
||||
// defining but not using an endpoint is likely a programming error, we
|
||||
// want to be sure to have the compiler flag this. We force this by using
|
||||
// the span from the name of the function to which this macro was applied.
|
||||
let span = ast.sig.ident.span();
|
||||
let const_struct = quote_spanned! {span=>
|
||||
pub(crate) const #name_ident: #name_ident = #name_ident {};
|
||||
};
|
||||
|
||||
// The final TokenStream returned will have a few components that reference
|
||||
// `#name_ident`, the name of the function to which this macro was applied...
|
||||
let stream = quote! {
|
||||
// ... a struct type called `#name_ident` that has no members
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
#description_doc_comment
|
||||
pub(crate) struct #name_ident {}
|
||||
// ... a constant of type `#name` whose identifier is also #name_ident
|
||||
#[allow(non_upper_case_globals, missing_docs)]
|
||||
#description_doc_comment
|
||||
#const_struct
|
||||
|
||||
impl #docs_crate::StdLibFn for #name_ident
|
||||
{
|
||||
fn name(&self) -> String {
|
||||
#name_str.to_string()
|
||||
}
|
||||
|
||||
fn summary(&self) -> String {
|
||||
#summary.to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
#description.to_string()
|
||||
}
|
||||
|
||||
fn tags(&self) -> Vec<String> {
|
||||
vec![#(#tags),*]
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<#docs_crate::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
|
||||
vec![#(#arg_types),*]
|
||||
}
|
||||
|
||||
fn return_value(&self) -> #docs_crate::StdLibFnArg {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
|
||||
#return_type
|
||||
}
|
||||
|
||||
fn unpublished(&self) -> bool {
|
||||
#unpublished
|
||||
}
|
||||
|
||||
fn deprecated(&self) -> bool {
|
||||
#deprecated
|
||||
}
|
||||
|
||||
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||
#fn_name_ident
|
||||
}
|
||||
}
|
||||
|
||||
#item
|
||||
};
|
||||
|
||||
// Prepend the usage message if any errors were detected.
|
||||
if !errors.is_empty() {
|
||||
errors.insert(0, Error::new_spanned(&ast.sig, ""));
|
||||
}
|
||||
|
||||
Ok((stream, errors))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
|
||||
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
|
||||
quote!(#(#compile_errors)*)
|
||||
}
|
||||
|
||||
fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
|
||||
if let Some(s) = var {
|
||||
if let Ok(ts) = syn::parse_str(s.as_str()) {
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
quote!(crate::docs)
|
||||
}
|
||||
|
||||
fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> (Option<String>, Option<String>) {
|
||||
let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
|
||||
|
||||
let mut lines = attrs.iter().flat_map(|attr| {
|
||||
if let syn::Meta::NameValue(nv) = &attr.meta {
|
||||
if nv.path.is_ident(&doc) {
|
||||
if let syn::Expr::Lit(syn::ExprLit {
|
||||
lit: syn::Lit::Str(s),
|
||||
..
|
||||
}) = &nv.value
|
||||
{
|
||||
return normalize_comment_string(s.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
// Skip initial blank lines; they make for excessively terse summaries.
|
||||
let summary = loop {
|
||||
match lines.next() {
|
||||
Some(s) if s.is_empty() => (),
|
||||
next => break next,
|
||||
}
|
||||
};
|
||||
// Skip initial blank description lines.
|
||||
let first = loop {
|
||||
match lines.next() {
|
||||
Some(s) if s.is_empty() => (),
|
||||
next => break next,
|
||||
}
|
||||
};
|
||||
|
||||
match (summary, first) {
|
||||
(None, _) => (None, None),
|
||||
(summary, None) => (summary, None),
|
||||
(Some(summary), Some(first)) => (
|
||||
Some(summary),
|
||||
Some(
|
||||
lines
|
||||
.fold(first, |acc, comment| {
|
||||
if acc.ends_with('-') || acc.ends_with('\n') || acc.is_empty() {
|
||||
// Continuation lines and newlines.
|
||||
format!("{}{}", acc, comment)
|
||||
} else if comment.is_empty() {
|
||||
// Handle fully blank comments as newlines we keep.
|
||||
format!("{}\n", acc)
|
||||
} else {
|
||||
// Default to space-separating comment fragments.
|
||||
format!("{} {}", acc, comment)
|
||||
}
|
||||
})
|
||||
.trim_end()
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_comment_string(s: String) -> Vec<String> {
|
||||
s.split('\n')
|
||||
.enumerate()
|
||||
.map(|(idx, s)| {
|
||||
// Rust-style comments are intrinsically single-line. We don't want
|
||||
// to trim away formatting such as an initial '*'.
|
||||
if idx == 0 {
|
||||
s.trim_start().trim_end()
|
||||
} else {
|
||||
let trimmed = s.trim_start().trim_end();
|
||||
trimmed
|
||||
.strip_prefix("* ")
|
||||
.unwrap_or_else(|| trimmed.strip_prefix('*').unwrap_or(trimmed))
|
||||
}
|
||||
})
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represent an item without concern for its body which may (or may not)
|
||||
/// contain syntax errors.
|
||||
struct ItemFnForSignature {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: Visibility,
|
||||
pub sig: Signature,
|
||||
pub _block: proc_macro2::TokenStream,
|
||||
}
|
||||
|
||||
impl Parse for ItemFnForSignature {
|
||||
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let vis: Visibility = input.parse()?;
|
||||
let sig: Signature = input.parse()?;
|
||||
let block = input.parse()?;
|
||||
Ok(ItemFnForSignature {
|
||||
attrs,
|
||||
vis,
|
||||
sig,
|
||||
_block: block,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_type(t: &str) -> String {
|
||||
let mut t = t.to_string();
|
||||
// Turn vecs into arrays.
|
||||
if t.starts_with("Vec<") {
|
||||
t = t.replace("Vec<", "[").replace('>', "]");
|
||||
}
|
||||
|
||||
if t == "f64" {
|
||||
return "number".to_string();
|
||||
} else if t == "str" {
|
||||
return "string".to_string();
|
||||
} else {
|
||||
return t.replace("f64", "number").to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use quote::quote;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_stdlib_line_to() {
|
||||
let (item, errors) = do_stdlib(
|
||||
quote! {
|
||||
name = "lineTo",
|
||||
},
|
||||
quote! {
|
||||
fn inner_line_to(
|
||||
data: LineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _expected = quote! {};
|
||||
|
||||
assert!(errors.is_empty());
|
||||
expectorate::assert_contents(
|
||||
"tests/lineTo.gen",
|
||||
&openapitor::types::get_text_fmt(&item).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stdlib_min() {
|
||||
let (item, errors) = do_stdlib(
|
||||
quote! {
|
||||
name = "min",
|
||||
},
|
||||
quote! {
|
||||
fn inner_min(
|
||||
/// The args to do shit to.
|
||||
args: Vec<f64>
|
||||
) -> f64 {
|
||||
let mut min = std::f64::MAX;
|
||||
for arg in args.iter() {
|
||||
if *arg < min {
|
||||
min = *arg;
|
||||
}
|
||||
}
|
||||
|
||||
min
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _expected = quote! {};
|
||||
|
||||
assert!(errors.is_empty());
|
||||
expectorate::assert_contents(
|
||||
"tests/min.gen",
|
||||
&openapitor::types::get_text_fmt(&item).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
76
src/wasm-lib/derive-docs/tests/lineTo.gen
Normal file
76
src/wasm-lib/derive-docs/tests/lineTo.gen
Normal file
@ -0,0 +1,76 @@
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
#[doc = "Std lib function: lineTo"]
|
||||
pub(crate) struct LineTo {}
|
||||
|
||||
#[allow(non_upper_case_globals, missing_docs)]
|
||||
#[doc = "Std lib function: lineTo"]
|
||||
pub(crate) const LineTo: LineTo = LineTo {};
|
||||
impl crate::docs::StdLibFn for LineTo {
|
||||
fn name(&self) -> String {
|
||||
"lineTo".to_string()
|
||||
}
|
||||
|
||||
fn summary(&self) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn tags(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
vec![
|
||||
crate::docs::StdLibFnArg {
|
||||
name: "data".to_string(),
|
||||
type_: "LineToData".to_string(),
|
||||
schema: LineToData::json_schema(&mut generator),
|
||||
required: true,
|
||||
},
|
||||
crate::docs::StdLibFnArg {
|
||||
name: "sketch_group".to_string(),
|
||||
type_: "SketchGroup".to_string(),
|
||||
schema: SketchGroup::json_schema(&mut generator),
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn return_value(&self) -> crate::docs::StdLibFnArg {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
crate::docs::StdLibFnArg {
|
||||
name: "".to_string(),
|
||||
type_: "SketchGroup".to_string(),
|
||||
schema: SketchGroup::json_schema(&mut generator),
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn unpublished(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn deprecated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||
line_to
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_line_to(
|
||||
data: LineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
Ok(())
|
||||
}
|
71
src/wasm-lib/derive-docs/tests/min.gen
Normal file
71
src/wasm-lib/derive-docs/tests/min.gen
Normal file
@ -0,0 +1,71 @@
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
#[doc = "Std lib function: min"]
|
||||
pub(crate) struct Min {}
|
||||
|
||||
#[allow(non_upper_case_globals, missing_docs)]
|
||||
#[doc = "Std lib function: min"]
|
||||
pub(crate) const Min: Min = Min {};
|
||||
impl crate::docs::StdLibFn for Min {
|
||||
fn name(&self) -> String {
|
||||
"min".to_string()
|
||||
}
|
||||
|
||||
fn summary(&self) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn tags(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
vec![crate::docs::StdLibFnArg {
|
||||
name: "args".to_string(),
|
||||
type_: "[number]".to_string(),
|
||||
schema: <Vec<f64>>::json_schema(&mut generator),
|
||||
required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
fn return_value(&self) -> crate::docs::StdLibFnArg {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
crate::docs::StdLibFnArg {
|
||||
name: "".to_string(),
|
||||
type_: "number".to_string(),
|
||||
schema: f64::json_schema(&mut generator),
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn unpublished(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn deprecated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||
min
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_min(#[doc = r" The args to do shit to."] args: Vec<f64>) -> f64 {
|
||||
let mut min = std::f64::MAX;
|
||||
for arg in args.iter() {
|
||||
if *arg < min {
|
||||
min = *arg;
|
||||
}
|
||||
}
|
||||
|
||||
min
|
||||
}
|
@ -2,9 +2,17 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Map;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
use crate::{
|
||||
engine::EngineConnection,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Program {
|
||||
@ -14,7 +22,7 @@ pub struct Program {
|
||||
pub non_code_meta: NoneCodeMeta,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum BodyItem {
|
||||
@ -23,7 +31,7 @@ pub enum BodyItem {
|
||||
ReturnStatement(ReturnStatement),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Value {
|
||||
@ -40,7 +48,91 @@ pub enum Value {
|
||||
UnaryExpression(Box<UnaryExpression>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
pub trait ValueMeta {
|
||||
fn start(&self) -> usize;
|
||||
|
||||
fn end(&self) -> usize;
|
||||
}
|
||||
|
||||
macro_rules! impl_value_meta {
|
||||
{$name:ident} => {
|
||||
impl ValueMeta for $name {
|
||||
fn start(&self) -> usize {
|
||||
self.start
|
||||
}
|
||||
|
||||
fn end(&self) -> usize {
|
||||
self.end
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for crate::executor::SourceRange {
|
||||
fn from(v: $name) -> Self {
|
||||
Self([v.start, v.end])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&$name> for crate::executor::SourceRange {
|
||||
fn from(v: &$name) -> Self {
|
||||
Self([v.start, v.end])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Box<$name>> for crate::executor::SourceRange {
|
||||
fn from(v: &Box<$name>) -> Self {
|
||||
Self([v.start, v.end])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn start(&self) -> usize {
|
||||
match self {
|
||||
Value::Literal(literal) => literal.start(),
|
||||
Value::Identifier(identifier) => identifier.start(),
|
||||
Value::BinaryExpression(binary_expression) => binary_expression.start(),
|
||||
Value::FunctionExpression(function_expression) => function_expression.start(),
|
||||
Value::CallExpression(call_expression) => call_expression.start(),
|
||||
Value::PipeExpression(pipe_expression) => pipe_expression.start(),
|
||||
Value::PipeSubstitution(pipe_substitution) => pipe_substitution.start(),
|
||||
Value::ArrayExpression(array_expression) => array_expression.start(),
|
||||
Value::ObjectExpression(object_expression) => object_expression.start(),
|
||||
Value::MemberExpression(member_expression) => member_expression.start(),
|
||||
Value::UnaryExpression(unary_expression) => unary_expression.start(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> usize {
|
||||
match self {
|
||||
Value::Literal(literal) => literal.end(),
|
||||
Value::Identifier(identifier) => identifier.end(),
|
||||
Value::BinaryExpression(binary_expression) => binary_expression.end(),
|
||||
Value::FunctionExpression(function_expression) => function_expression.end(),
|
||||
Value::CallExpression(call_expression) => call_expression.end(),
|
||||
Value::PipeExpression(pipe_expression) => pipe_expression.end(),
|
||||
Value::PipeSubstitution(pipe_substitution) => pipe_substitution.end(),
|
||||
Value::ArrayExpression(array_expression) => array_expression.end(),
|
||||
Value::ObjectExpression(object_expression) => object_expression.end(),
|
||||
Value::MemberExpression(member_expression) => member_expression.end(),
|
||||
Value::UnaryExpression(unary_expression) => unary_expression.end(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for crate::executor::SourceRange {
|
||||
fn from(value: Value) -> Self {
|
||||
Self([value.start(), value.end()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Value> for crate::executor::SourceRange {
|
||||
fn from(value: &Value) -> Self {
|
||||
Self([value.start(), value.end()])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum BinaryPart {
|
||||
@ -50,8 +142,74 @@ pub enum BinaryPart {
|
||||
CallExpression(Box<CallExpression>),
|
||||
UnaryExpression(Box<UnaryExpression>),
|
||||
}
|
||||
impl From<BinaryPart> for crate::executor::SourceRange {
|
||||
fn from(value: BinaryPart) -> Self {
|
||||
Self([value.start(), value.end()])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl From<&BinaryPart> for crate::executor::SourceRange {
|
||||
fn from(value: &BinaryPart) -> Self {
|
||||
Self([value.start(), value.end()])
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryPart {
|
||||
pub fn start(&self) -> usize {
|
||||
match self {
|
||||
BinaryPart::Literal(literal) => literal.start(),
|
||||
BinaryPart::Identifier(identifier) => identifier.start(),
|
||||
BinaryPart::BinaryExpression(binary_expression) => binary_expression.start(),
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.start(),
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.start(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> usize {
|
||||
match self {
|
||||
BinaryPart::Literal(literal) => literal.end(),
|
||||
BinaryPart::Identifier(identifier) => identifier.end(),
|
||||
BinaryPart::BinaryExpression(binary_expression) => binary_expression.end(),
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.end(),
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.end(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_result(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
pipe_info.is_in_pipe = false;
|
||||
match self {
|
||||
BinaryPart::Literal(literal) => Ok(literal.into()),
|
||||
BinaryPart::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?;
|
||||
Ok(value.clone())
|
||||
}
|
||||
BinaryPart::BinaryExpression(binary_expression) => {
|
||||
binary_expression.get_result(memory, pipe_info, stdlib, engine)
|
||||
}
|
||||
BinaryPart::CallExpression(call_expression) => {
|
||||
call_expression.execute(memory, pipe_info, stdlib, engine)
|
||||
}
|
||||
BinaryPart::UnaryExpression(unary_expression) => {
|
||||
// Return an error this should not happen.
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"UnaryExpression should not be a BinaryPart: {:?}",
|
||||
unary_expression
|
||||
),
|
||||
source_ranges: vec![unary_expression.into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct NoneCodeNode {
|
||||
@ -60,7 +218,7 @@ pub struct NoneCodeNode {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NoneCodeMeta {
|
||||
@ -94,7 +252,7 @@ impl<'de> Deserialize<'de> for NoneCodeMeta {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct ExpressionStatement {
|
||||
@ -103,7 +261,9 @@ pub struct ExpressionStatement {
|
||||
pub expression: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(ExpressionStatement);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct CallExpression {
|
||||
@ -114,7 +274,138 @@ pub struct CallExpression {
|
||||
pub optional: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(CallExpression);
|
||||
|
||||
impl CallExpression {
|
||||
pub fn execute(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
let fn_name = self.callee.name.clone();
|
||||
|
||||
let mut fn_args: Vec<MemoryItem> = Vec::with_capacity(self.arguments.len());
|
||||
|
||||
for arg in &self.arguments {
|
||||
let result: MemoryItem = match arg {
|
||||
Value::Literal(literal) => literal.into(),
|
||||
Value::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?;
|
||||
value.clone()
|
||||
}
|
||||
Value::BinaryExpression(binary_expression) => {
|
||||
binary_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::CallExpression(call_expression) => {
|
||||
pipe_info.is_in_pipe = false;
|
||||
call_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::UnaryExpression(unary_expression) => {
|
||||
unary_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::ObjectExpression(object_expression) => {
|
||||
object_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::ArrayExpression(array_expression) => {
|
||||
array_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::PipeExpression(pipe_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"PipeExpression not implemented here: {:?}",
|
||||
pipe_expression
|
||||
),
|
||||
source_ranges: vec![pipe_expression.into()],
|
||||
}));
|
||||
}
|
||||
Value::PipeSubstitution(pipe_substitution) => pipe_info
|
||||
.previous_results
|
||||
.get(&pipe_info.index - 1)
|
||||
.ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"PipeSubstitution index out of bounds: {:?}",
|
||||
pipe_info
|
||||
),
|
||||
source_ranges: vec![pipe_substitution.into()],
|
||||
})
|
||||
})?
|
||||
.clone(),
|
||||
Value::MemberExpression(member_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"MemberExpression not implemented here: {:?}",
|
||||
member_expression
|
||||
),
|
||||
source_ranges: vec![member_expression.into()],
|
||||
}));
|
||||
}
|
||||
Value::FunctionExpression(function_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"FunctionExpression not implemented here: {:?}",
|
||||
function_expression
|
||||
),
|
||||
source_ranges: vec![function_expression.into()],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
fn_args.push(result);
|
||||
}
|
||||
|
||||
if let Some(func) = stdlib.fns.get(&fn_name) {
|
||||
// Attempt to call the function.
|
||||
let mut args = crate::std::Args::new(fn_args, self.into(), engine);
|
||||
let result = func(&mut args)?;
|
||||
if pipe_info.is_in_pipe {
|
||||
pipe_info.index += 1;
|
||||
pipe_info.previous_results.push(result);
|
||||
execute_pipe_body(
|
||||
memory,
|
||||
&pipe_info.body.clone(),
|
||||
pipe_info,
|
||||
self.into(),
|
||||
stdlib,
|
||||
engine,
|
||||
)
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
} else {
|
||||
let mem = memory.clone();
|
||||
let func = mem.get(&fn_name, self.into())?;
|
||||
let result = func.call_fn(&fn_args, memory, engine)?.ok_or_else(|| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("Result of function {} is undefined", fn_name),
|
||||
source_ranges: vec![self.into()],
|
||||
})
|
||||
})?;
|
||||
|
||||
let result = result.get_value()?;
|
||||
|
||||
if pipe_info.is_in_pipe {
|
||||
pipe_info.index += 1;
|
||||
pipe_info.previous_results.push(result);
|
||||
|
||||
execute_pipe_body(
|
||||
memory,
|
||||
&pipe_info.body.clone(),
|
||||
pipe_info,
|
||||
self.into(),
|
||||
stdlib,
|
||||
engine,
|
||||
)
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct VariableDeclaration {
|
||||
@ -124,7 +415,9 @@ pub struct VariableDeclaration {
|
||||
pub kind: String, // Change to enum if there are specific values
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(VariableDeclaration);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct VariableDeclarator {
|
||||
@ -134,7 +427,9 @@ pub struct VariableDeclarator {
|
||||
pub init: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(VariableDeclarator);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct Literal {
|
||||
@ -144,7 +439,31 @@ pub struct Literal {
|
||||
pub raw: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(Literal);
|
||||
|
||||
impl From<Literal> for MemoryItem {
|
||||
fn from(literal: Literal) -> Self {
|
||||
MemoryItem::UserVal {
|
||||
value: literal.value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: literal.into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Box<Literal>> for MemoryItem {
|
||||
fn from(literal: &Box<Literal>) -> Self {
|
||||
MemoryItem::UserVal {
|
||||
value: literal.value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: literal.into(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct Identifier {
|
||||
@ -153,7 +472,9 @@ pub struct Identifier {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(Identifier);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct PipeSubstitution {
|
||||
@ -161,7 +482,9 @@ pub struct PipeSubstitution {
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(PipeSubstitution);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct ArrayExpression {
|
||||
@ -170,7 +493,87 @@ pub struct ArrayExpression {
|
||||
pub elements: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(ArrayExpression);
|
||||
|
||||
impl ArrayExpression {
|
||||
pub fn execute(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
let mut results = Vec::with_capacity(self.elements.len());
|
||||
|
||||
for element in &self.elements {
|
||||
let result = match element {
|
||||
Value::Literal(literal) => literal.into(),
|
||||
Value::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?;
|
||||
value.clone()
|
||||
}
|
||||
Value::BinaryExpression(binary_expression) => {
|
||||
binary_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::CallExpression(call_expression) => {
|
||||
pipe_info.is_in_pipe = false;
|
||||
call_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::UnaryExpression(unary_expression) => {
|
||||
unary_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::ObjectExpression(object_expression) => {
|
||||
object_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::ArrayExpression(array_expression) => {
|
||||
array_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::PipeExpression(pipe_expression) => {
|
||||
pipe_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::PipeSubstitution(pipe_substitution) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"PipeSubstitution not implemented here: {:?}",
|
||||
pipe_substitution
|
||||
),
|
||||
source_ranges: vec![pipe_substitution.into()],
|
||||
}));
|
||||
}
|
||||
Value::MemberExpression(member_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"MemberExpression not implemented here: {:?}",
|
||||
member_expression
|
||||
),
|
||||
source_ranges: vec![member_expression.into()],
|
||||
}));
|
||||
}
|
||||
Value::FunctionExpression(function_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"FunctionExpression not implemented here: {:?}",
|
||||
function_expression
|
||||
),
|
||||
source_ranges: vec![function_expression.into()],
|
||||
}));
|
||||
}
|
||||
}
|
||||
.get_json_value()?;
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
Ok(MemoryItem::UserVal {
|
||||
value: results.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct ObjectExpression {
|
||||
@ -179,7 +582,85 @@ pub struct ObjectExpression {
|
||||
pub properties: Vec<ObjectProperty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl ObjectExpression {
|
||||
pub fn execute(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
let mut object = Map::new();
|
||||
for property in &self.properties {
|
||||
let result = match &property.value {
|
||||
Value::Literal(literal) => literal.into(),
|
||||
Value::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?;
|
||||
value.clone()
|
||||
}
|
||||
Value::BinaryExpression(binary_expression) => {
|
||||
binary_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::CallExpression(call_expression) => {
|
||||
pipe_info.is_in_pipe = false;
|
||||
call_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::UnaryExpression(unary_expression) => {
|
||||
unary_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::ObjectExpression(object_expression) => {
|
||||
object_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::ArrayExpression(array_expression) => {
|
||||
array_expression.execute(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::PipeExpression(pipe_expression) => {
|
||||
pipe_expression.get_result(memory, pipe_info, stdlib, engine)?
|
||||
}
|
||||
Value::PipeSubstitution(pipe_substitution) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"PipeSubstitution not implemented here: {:?}",
|
||||
pipe_substitution
|
||||
),
|
||||
source_ranges: vec![pipe_substitution.into()],
|
||||
}));
|
||||
}
|
||||
Value::MemberExpression(member_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"MemberExpression not implemented here: {:?}",
|
||||
member_expression
|
||||
),
|
||||
source_ranges: vec![member_expression.into()],
|
||||
}));
|
||||
}
|
||||
Value::FunctionExpression(function_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"FunctionExpression not implemented here: {:?}",
|
||||
function_expression
|
||||
),
|
||||
source_ranges: vec![function_expression.into()],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
object.insert(property.key.name.clone(), result.get_json_value()?);
|
||||
}
|
||||
|
||||
Ok(MemoryItem::UserVal {
|
||||
value: object.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl_value_meta!(ObjectExpression);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct ObjectProperty {
|
||||
@ -189,7 +670,9 @@ pub struct ObjectProperty {
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(ObjectProperty);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum MemberObject {
|
||||
@ -197,7 +680,7 @@ pub enum MemberObject {
|
||||
Identifier(Box<Identifier>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum LiteralIdentifier {
|
||||
@ -205,7 +688,7 @@ pub enum LiteralIdentifier {
|
||||
Literal(Box<Literal>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct MemberExpression {
|
||||
@ -216,7 +699,62 @@ pub struct MemberExpression {
|
||||
pub computed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(MemberExpression);
|
||||
|
||||
impl MemberExpression {
|
||||
pub fn get_result(&self, memory: &mut ProgramMemory) -> Result<MemoryItem, KclError> {
|
||||
let property_name = match &self.property {
|
||||
LiteralIdentifier::Identifier(identifier) => identifier.name.to_string(),
|
||||
LiteralIdentifier::Literal(literal) => {
|
||||
let value = literal.value.clone();
|
||||
// Parse this as a string.
|
||||
if let serde_json::Value::String(string) = value {
|
||||
string
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected string literal for property name, found {:?}",
|
||||
value
|
||||
),
|
||||
source_ranges: vec![literal.into()],
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let object = match &self.object {
|
||||
MemberObject::MemberExpression(member_expr) => member_expr.get_result(memory)?,
|
||||
MemberObject::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?;
|
||||
value.clone()
|
||||
}
|
||||
}
|
||||
.get_json_value()?;
|
||||
|
||||
if let serde_json::Value::Object(map) = object {
|
||||
if let Some(value) = map.get(&property_name) {
|
||||
Ok(MemoryItem::UserVal {
|
||||
value: value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
} else {
|
||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("Property {} not found in object", property_name),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("MemberExpression object is not an object: {:?}", object),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct ObjectKeyInfo {
|
||||
pub key: LiteralIdentifier,
|
||||
@ -224,18 +762,109 @@ pub struct ObjectKeyInfo {
|
||||
pub computed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct BinaryExpression {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
// TODO: operator should be a type not a string.
|
||||
pub operator: String,
|
||||
pub left: BinaryPart,
|
||||
pub right: BinaryPart,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(BinaryExpression);
|
||||
|
||||
impl BinaryExpression {
|
||||
pub fn get_result(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
pipe_info.is_in_pipe = false;
|
||||
|
||||
let left_json_value = self
|
||||
.left
|
||||
.get_result(memory, pipe_info, stdlib, engine)?
|
||||
.get_json_value()?;
|
||||
let right_json_value = self
|
||||
.right
|
||||
.get_result(memory, pipe_info, stdlib, engine)?
|
||||
.get_json_value()?;
|
||||
|
||||
// First check if we are doing string concatenation.
|
||||
if self.operator == "+" {
|
||||
if let (Some(left), Some(right)) = (
|
||||
parse_json_value_as_string(&left_json_value),
|
||||
parse_json_value_as_string(&right_json_value),
|
||||
) {
|
||||
let value = serde_json::Value::String(format!("{}{}", left, right));
|
||||
return Ok(MemoryItem::UserVal {
|
||||
value,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
|
||||
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
|
||||
|
||||
let value: serde_json::Value = match self.operator.as_str() {
|
||||
"+" => (left + right).into(),
|
||||
"-" => (left - right).into(),
|
||||
"*" => (left * right).into(),
|
||||
"/" => (left / right).into(),
|
||||
"%" => (left % right).into(),
|
||||
_ => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![self.into()],
|
||||
message: format!("Invalid operator: {}", self.operator),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(MemoryItem::UserVal {
|
||||
value,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_number_as_f64(
|
||||
j: &serde_json::Value,
|
||||
source_range: SourceRange,
|
||||
) -> Result<f64, KclError> {
|
||||
if let serde_json::Value::Number(n) = &j {
|
||||
n.as_f64().ok_or_else(|| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid number: {}", j),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid number: {}", j),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
|
||||
if let serde_json::Value::String(n) = &j {
|
||||
Some(n.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct UnaryExpression {
|
||||
@ -245,7 +874,35 @@ pub struct UnaryExpression {
|
||||
pub argument: BinaryPart,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(UnaryExpression);
|
||||
|
||||
impl UnaryExpression {
|
||||
pub fn get_result(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
pipe_info.is_in_pipe = false;
|
||||
|
||||
let num = parse_json_number_as_f64(
|
||||
&self
|
||||
.argument
|
||||
.get_result(memory, pipe_info, stdlib, engine)?
|
||||
.get_json_value()?,
|
||||
self.into(),
|
||||
)?;
|
||||
Ok(MemoryItem::UserVal {
|
||||
value: (-(num)).into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub struct PipeExpression {
|
||||
@ -255,7 +912,75 @@ pub struct PipeExpression {
|
||||
pub non_code_meta: NoneCodeMeta,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
impl_value_meta!(PipeExpression);
|
||||
|
||||
impl PipeExpression {
|
||||
pub fn get_result(
|
||||
&self,
|
||||
memory: &mut ProgramMemory,
|
||||
pipe_info: &mut PipeInfo,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
// Reset the previous results.
|
||||
pipe_info.previous_results = vec![];
|
||||
pipe_info.index = 0;
|
||||
execute_pipe_body(memory, &self.body, pipe_info, self.into(), stdlib, engine)
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_pipe_body(
|
||||
memory: &mut ProgramMemory,
|
||||
body: &[Value],
|
||||
pipe_info: &mut PipeInfo,
|
||||
source_range: SourceRange,
|
||||
stdlib: &crate::std::StdLib,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<MemoryItem, KclError> {
|
||||
if pipe_info.index == body.len() {
|
||||
pipe_info.is_in_pipe = false;
|
||||
return Ok(pipe_info
|
||||
.previous_results
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: "Pipe body results should have at least one expression".to_string(),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?
|
||||
.clone());
|
||||
}
|
||||
|
||||
let expression = body.get(pipe_info.index).ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Invalid index for pipe: {}", pipe_info.index),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
match expression {
|
||||
Value::BinaryExpression(binary_expression) => {
|
||||
let result = binary_expression.get_result(memory, pipe_info, stdlib, engine)?;
|
||||
pipe_info.previous_results.push(result);
|
||||
pipe_info.index += 1;
|
||||
execute_pipe_body(memory, body, pipe_info, source_range, stdlib, engine)
|
||||
}
|
||||
Value::CallExpression(call_expression) => {
|
||||
pipe_info.is_in_pipe = true;
|
||||
pipe_info.body = body.to_vec();
|
||||
call_expression.execute(memory, pipe_info, stdlib, engine)
|
||||
}
|
||||
_ => {
|
||||
// Return an error this should not happen.
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("PipeExpression not implemented here: {:?}", expression),
|
||||
source_ranges: vec![expression.into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct FunctionExpression {
|
||||
@ -263,20 +988,12 @@ pub struct FunctionExpression {
|
||||
pub end: usize,
|
||||
pub id: Option<Identifier>,
|
||||
pub params: Vec<Identifier>,
|
||||
pub body: BlockStatement,
|
||||
pub body: Program,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub struct BlockStatement {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub body: Vec<BodyItem>,
|
||||
pub non_code_meta: NoneCodeMeta,
|
||||
}
|
||||
impl_value_meta!(FunctionExpression);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct ReturnStatement {
|
||||
@ -284,3 +1001,5 @@ pub struct ReturnStatement {
|
||||
pub end: usize,
|
||||
pub argument: Value,
|
||||
}
|
||||
|
||||
impl_value_meta!(ReturnStatement);
|
||||
|
195
src/wasm-lib/src/docs.rs
Normal file
195
src/wasm-lib/src/docs.rs
Normal file
@ -0,0 +1,195 @@
|
||||
//! Functions for generating docs for our stdlib functions.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::std::Primitive;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct StdLibFnData {
|
||||
/// The name of the function.
|
||||
pub name: String,
|
||||
/// The summary of the function.
|
||||
pub summary: String,
|
||||
/// The description of the function.
|
||||
pub description: String,
|
||||
/// The tags of the function.
|
||||
pub tags: Vec<String>,
|
||||
/// The args of the function.
|
||||
pub args: Vec<StdLibFnArg>,
|
||||
/// The return value of the function.
|
||||
pub return_value: StdLibFnArg,
|
||||
/// If the function is unpublished.
|
||||
pub unpublished: bool,
|
||||
/// If the function is deprecated.
|
||||
pub deprecated: bool,
|
||||
}
|
||||
|
||||
/// This struct defines a single argument to a stdlib function.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct StdLibFnArg {
|
||||
/// The name of the argument.
|
||||
pub name: String,
|
||||
/// The type of the argument.
|
||||
pub type_: String,
|
||||
/// The schema of the argument.
|
||||
pub schema: schemars::schema::Schema,
|
||||
/// If the argument is required.
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
impl StdLibFnArg {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_type_string(&self) -> Result<(String, bool)> {
|
||||
get_type_string_from_schema(&self.schema)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn description(&self) -> Option<String> {
|
||||
get_description_string_from_schema(&self.schema)
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait defines functions called upon stdlib functions to generate
|
||||
/// documentation for them.
|
||||
pub trait StdLibFn {
|
||||
/// The name of the function.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// The summary of the function.
|
||||
fn summary(&self) -> String;
|
||||
|
||||
/// The description of the function.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// The tags of the function.
|
||||
fn tags(&self) -> Vec<String>;
|
||||
|
||||
/// The args of the function.
|
||||
fn args(&self) -> Vec<StdLibFnArg>;
|
||||
|
||||
/// The return value of the function.
|
||||
fn return_value(&self) -> StdLibFnArg;
|
||||
|
||||
/// If the function is unpublished.
|
||||
fn unpublished(&self) -> bool;
|
||||
|
||||
/// If the function is deprecated.
|
||||
fn deprecated(&self) -> bool;
|
||||
|
||||
/// The function itself.
|
||||
fn std_lib_fn(&self) -> crate::std::StdFn;
|
||||
|
||||
/// Return a JSON struct representing the function.
|
||||
fn to_json(&self) -> Result<StdLibFnData> {
|
||||
Ok(StdLibFnData {
|
||||
name: self.name(),
|
||||
summary: self.summary(),
|
||||
description: self.description(),
|
||||
tags: self.tags(),
|
||||
args: self.args(),
|
||||
return_value: self.return_value(),
|
||||
unpublished: self.unpublished(),
|
||||
deprecated: self.deprecated(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
|
||||
if let schemars::schema::Schema::Object(o) = schema {
|
||||
if let Some(metadata) = &o.metadata {
|
||||
if let Some(description) = &metadata.description {
|
||||
return Some(description.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> {
|
||||
match schema {
|
||||
schemars::schema::Schema::Object(o) => {
|
||||
if let Some(format) = &o.format {
|
||||
if format == "uuid" {
|
||||
return Ok((Primitive::Uuid.to_string(), false));
|
||||
} else if format == "double" || format == "uint" {
|
||||
return Ok((Primitive::Number.to_string(), false));
|
||||
} else {
|
||||
anyhow::bail!("unknown format: {}", format);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(obj_val) = &o.object {
|
||||
let mut fn_docs = String::new();
|
||||
fn_docs.push_str("{\n");
|
||||
// Let's print out the object's properties.
|
||||
for (prop_name, prop) in obj_val.properties.iter() {
|
||||
if prop_name.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(description) = get_description_string_from_schema(prop) {
|
||||
fn_docs.push_str(&format!("\t// {}\n", description));
|
||||
}
|
||||
fn_docs.push_str(&format!(
|
||||
"\t\"{}\": {},\n",
|
||||
prop_name,
|
||||
get_type_string_from_schema(prop)?.0,
|
||||
));
|
||||
}
|
||||
|
||||
fn_docs.push('}');
|
||||
|
||||
return Ok((fn_docs, true));
|
||||
}
|
||||
|
||||
if let Some(array_val) = &o.array {
|
||||
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
|
||||
// Let's print out the object's properties.
|
||||
return Ok((
|
||||
format!("[{}]", get_type_string_from_schema(items)?.0),
|
||||
false,
|
||||
));
|
||||
} else if let Some(items) = &array_val.contains {
|
||||
return Ok((
|
||||
format!("[{}]", get_type_string_from_schema(items)?.0),
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(subschemas) = &o.subschemas {
|
||||
let mut fn_docs = String::new();
|
||||
if let Some(items) = &subschemas.one_of {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
// Let's print out the object's properties.
|
||||
fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string());
|
||||
if i < items.len() - 1 {
|
||||
fn_docs.push_str(" |\n");
|
||||
}
|
||||
}
|
||||
} else if let Some(items) = &subschemas.any_of {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
// Let's print out the object's properties.
|
||||
fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string());
|
||||
if i < items.len() - 1 {
|
||||
fn_docs.push_str(" |\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("unknown subschemas: {:#?}", subschemas);
|
||||
}
|
||||
|
||||
return Ok((fn_docs, true));
|
||||
}
|
||||
|
||||
if let Some(schemars::schema::SingleOrVec::Single(_string)) = &o.instance_type {
|
||||
return Ok((Primitive::String.to_string(), false));
|
||||
}
|
||||
|
||||
anyhow::bail!("unknown type: {:#?}", o)
|
||||
}
|
||||
schemars::schema::Schema::Bool(_) => Ok((Primitive::Bool.to_string(), false)),
|
||||
}
|
||||
}
|
28
src/wasm-lib/src/engine/conn_mock.rs
Normal file
28
src/wasm-lib/src/engine/conn_mock.rs
Normal file
@ -0,0 +1,28 @@
|
||||
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
|
||||
//! engine.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::errors::KclError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EngineConnection {}
|
||||
|
||||
impl EngineConnection {
|
||||
pub async fn new(
|
||||
_conn_str: &str,
|
||||
_auth_token: &str,
|
||||
_origin: &str,
|
||||
) -> Result<EngineConnection> {
|
||||
Ok(EngineConnection {})
|
||||
}
|
||||
|
||||
pub fn send_modeling_cmd(
|
||||
&mut self,
|
||||
_id: uuid::Uuid,
|
||||
_source_range: crate::executor::SourceRange,
|
||||
_cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<(), KclError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
159
src/wasm-lib/src/engine/conn_noweb.rs
Normal file
159
src/wasm-lib/src/engine/conn_noweb.rs
Normal file
@ -0,0 +1,159 @@
|
||||
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
|
||||
//! engine.
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
|
||||
use tokio_tungstenite::tungstenite::Message as WsMsg;
|
||||
|
||||
use crate::errors::{KclError, KclErrorDetails};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EngineConnection {
|
||||
tcp_write: futures::stream::SplitSink<
|
||||
tokio_tungstenite::WebSocketStream<
|
||||
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
|
||||
>,
|
||||
WsMsg,
|
||||
>,
|
||||
tcp_read_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for EngineConnection {
|
||||
fn drop(&mut self) {
|
||||
// Drop the read handle.
|
||||
self.tcp_read_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TcpRead {
|
||||
stream: futures::stream::SplitStream<
|
||||
tokio_tungstenite::WebSocketStream<
|
||||
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl TcpRead {
|
||||
pub async fn read(&mut self) -> Result<WebSocketResponse> {
|
||||
let msg = self.stream.next().await.unwrap()?;
|
||||
let msg = match msg {
|
||||
WsMsg::Text(text) => text,
|
||||
WsMsg::Binary(bin) => bincode::deserialize(&bin)?,
|
||||
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
|
||||
};
|
||||
let msg = serde_json::from_str::<WebSocketResponse>(&msg)?;
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl EngineConnection {
|
||||
pub async fn new(conn_str: &str, auth_token: &str, origin: &str) -> Result<EngineConnection> {
|
||||
let method = http::Method::GET.to_string();
|
||||
let key = tokio_tungstenite::tungstenite::handshake::client::generate_key();
|
||||
|
||||
// Establish a websocket connection.
|
||||
let (ws_stream, _) = tokio_tungstenite::connect_async(httparse::Request {
|
||||
method: Some(&method),
|
||||
path: Some(conn_str),
|
||||
// TODO pass in the origin from elsewhere.
|
||||
headers: &mut websocket_headers(auth_token, &key, origin),
|
||||
version: Some(1), // HTTP/1.1
|
||||
})
|
||||
.await?;
|
||||
|
||||
let (tcp_write, tcp_read) = ws_stream.split();
|
||||
|
||||
let mut tcp_read = TcpRead { stream: tcp_read };
|
||||
|
||||
let tcp_read_handle = tokio::spawn(async move {
|
||||
// Get Websocket messages from API server
|
||||
while let Ok(ws_resp) = tcp_read.read().await {
|
||||
if !ws_resp.success {
|
||||
println!("got ws errors: {:?}", ws_resp.errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(msg) = ws_resp.resp {
|
||||
match msg {
|
||||
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
|
||||
println!("got ice server info: {:?}", ice_servers);
|
||||
}
|
||||
OkWebSocketResponseData::SdpAnswer { answer } => {
|
||||
println!("got sdp answer: {:?}", answer);
|
||||
}
|
||||
OkWebSocketResponseData::TrickleIce { candidate } => {
|
||||
println!("got trickle ice: {:?}", candidate);
|
||||
}
|
||||
OkWebSocketResponseData::Modeling { .. } => {}
|
||||
OkWebSocketResponseData::Export { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(EngineConnection {
|
||||
tcp_write,
|
||||
tcp_read_handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn tcp_send(&mut self, msg: WebSocketRequest) -> Result<()> {
|
||||
let msg = serde_json::to_string(&msg)?;
|
||||
self.tcp_write.send(WsMsg::Text(msg)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_modeling_cmd(
|
||||
&mut self,
|
||||
id: uuid::Uuid,
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<(), KclError> {
|
||||
futures::executor::block_on(
|
||||
self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id }),
|
||||
)
|
||||
.map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to send modeling command: {}", e),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Headers for starting a websocket session with api-deux.
|
||||
fn websocket_headers<'a>(
|
||||
token: &'a str,
|
||||
key: &'a str,
|
||||
origin: &'a str,
|
||||
) -> [httparse::Header<'a>; 6] {
|
||||
[
|
||||
httparse::Header {
|
||||
name: "Authorization",
|
||||
value: token.as_bytes(),
|
||||
},
|
||||
httparse::Header {
|
||||
name: "Connection",
|
||||
value: b"Upgrade",
|
||||
},
|
||||
httparse::Header {
|
||||
name: "Upgrade",
|
||||
value: b"websocket",
|
||||
},
|
||||
httparse::Header {
|
||||
name: "Sec-WebSocket-Version",
|
||||
value: b"13",
|
||||
},
|
||||
httparse::Header {
|
||||
name: "Sec-WebSocket-Key",
|
||||
value: key.as_bytes(),
|
||||
},
|
||||
httparse::Header {
|
||||
name: "Host",
|
||||
value: origin.as_bytes(),
|
||||
},
|
||||
]
|
||||
}
|
58
src/wasm-lib/src/engine/conn_web.rs
Normal file
58
src/wasm-lib/src/engine/conn_web.rs
Normal file
@ -0,0 +1,58 @@
|
||||
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
|
||||
//! engine.
|
||||
|
||||
use anyhow::Result;
|
||||
use kittycad::types::WebSocketRequest;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::errors::{KclError, KclErrorDetails};
|
||||
|
||||
#[wasm_bindgen(module = "/../lang/std/engineConnection.ts")]
|
||||
extern "C" {
|
||||
#[derive(Debug, Clone)]
|
||||
pub type EngineCommandManager;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
fn sendModelingCommandFromWasm(
|
||||
this: &EngineCommandManager,
|
||||
id: String,
|
||||
rangeStr: String,
|
||||
cmdStr: String,
|
||||
) -> js_sys::Promise;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EngineConnection {
|
||||
manager: EngineCommandManager,
|
||||
}
|
||||
|
||||
impl EngineConnection {
|
||||
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
|
||||
Ok(EngineConnection { manager })
|
||||
}
|
||||
|
||||
pub fn send_modeling_cmd(
|
||||
&mut self,
|
||||
id: uuid::Uuid,
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<(), 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 _ = self
|
||||
.manager
|
||||
.sendModelingCommandFromWasm(id.to_string(), source_range_str, cmd_str);
|
||||
Ok(())
|
||||
}
|
||||
}
|
64
src/wasm-lib/src/engine/mod.rs
Normal file
64
src/wasm-lib/src/engine/mod.rs
Normal file
@ -0,0 +1,64 @@
|
||||
//! Functions for managing engine communications.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(feature = "noweb")]
|
||||
#[cfg(not(test))]
|
||||
pub mod conn_noweb;
|
||||
#[cfg(feature = "noweb")]
|
||||
#[cfg(not(test))]
|
||||
pub use conn_noweb::EngineConnection;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg(not(test))]
|
||||
pub mod conn_web;
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg(not(test))]
|
||||
pub use conn_web::EngineConnection;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod conn_mock;
|
||||
#[cfg(test)]
|
||||
pub use conn_mock::EngineConnection;
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[wasm_bindgen]
|
||||
pub struct EngineManager {
|
||||
connection: EngineConnection,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EngineManager {
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg(not(test))]
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub async fn new(manager: conn_web::EngineCommandManager) -> EngineManager {
|
||||
EngineManager {
|
||||
// This unwrap is safe because the connection is always created.
|
||||
connection: EngineConnection::new(manager).await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "web"))]
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub async fn new(conn_str: &str, auth_token: &str, origin: &str) -> EngineManager {
|
||||
EngineManager {
|
||||
// TODO: fix unwrap.
|
||||
connection: EngineConnection::new(conn_str, auth_token, origin)
|
||||
.await
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
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, SourceRange::default(), cmd)
|
||||
.map_err(String::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -13,19 +13,23 @@ pub enum KclError {
|
||||
Type(KclErrorDetails),
|
||||
#[error("unimplemented: {0:?}")]
|
||||
Unimplemented(KclErrorDetails),
|
||||
#[error("unexpected: {0:?}")]
|
||||
Unexpected(KclErrorDetails),
|
||||
#[error("value already defined: {0:?}")]
|
||||
ValueAlreadyDefined(KclErrorDetails),
|
||||
#[error("undefined value: {0:?}")]
|
||||
UndefinedValue(KclErrorDetails),
|
||||
#[error("invalid expression: {0:?}")]
|
||||
InvalidExpression(crate::math_parser::MathExpression),
|
||||
#[error("engine: {0:?}")]
|
||||
Engine(KclErrorDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
pub struct KclErrorDetails {
|
||||
#[serde(rename = "sourceRanges")]
|
||||
pub source_ranges: Vec<[i32; 2]>,
|
||||
pub source_ranges: Vec<crate::executor::SourceRange>,
|
||||
#[serde(rename = "msg")]
|
||||
pub message: String,
|
||||
}
|
||||
@ -37,3 +41,9 @@ impl From<KclError> for String {
|
||||
serde_json::to_string(&error).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for KclError {
|
||||
fn from(error: String) -> Self {
|
||||
serde_json::from_str(&error).unwrap()
|
||||
}
|
||||
}
|
||||
|
865
src/wasm-lib/src/executor.rs
Normal file
865
src/wasm-lib/src/executor.rs
Normal file
@ -0,0 +1,865 @@
|
||||
//! The executor for the AST.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(not(test))]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
abstract_syntax_tree_types::{BodyItem, FunctionExpression, Value},
|
||||
engine::EngineConnection,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProgramMemory {
|
||||
pub root: HashMap<String, MemoryItem>,
|
||||
#[serde(rename = "return")]
|
||||
pub return_: Option<ProgramReturn>,
|
||||
}
|
||||
|
||||
impl ProgramMemory {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
root: HashMap::new(),
|
||||
return_: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add to the program memory.
|
||||
pub fn add(
|
||||
&mut self,
|
||||
key: &str,
|
||||
value: MemoryItem,
|
||||
source_range: SourceRange,
|
||||
) -> Result<(), KclError> {
|
||||
if self.root.get(key).is_some() {
|
||||
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
|
||||
message: format!("Cannot redefine {}", key),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
self.root.insert(key.to_string(), value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a value from the program memory.
|
||||
pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
|
||||
self.root.get(key).ok_or_else(|| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("memory item key `{}` is not defined", key),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProgramMemory {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum ProgramReturn {
|
||||
Arguments(Vec<Value>),
|
||||
Value(MemoryItem),
|
||||
}
|
||||
|
||||
impl From<ProgramReturn> for Vec<SourceRange> {
|
||||
fn from(item: ProgramReturn) -> Self {
|
||||
match item {
|
||||
ProgramReturn::Arguments(args) => args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
let r: SourceRange = arg.into();
|
||||
r
|
||||
})
|
||||
.collect(),
|
||||
ProgramReturn::Value(v) => v.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgramReturn {
|
||||
pub fn get_value(&self) -> Result<MemoryItem, KclError> {
|
||||
match self {
|
||||
ProgramReturn::Value(v) => Ok(v.clone()),
|
||||
ProgramReturn::Arguments(args) => Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Cannot get value from arguments: {:?}", args),
|
||||
source_ranges: self.clone().into(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum MemoryItem {
|
||||
UserVal {
|
||||
value: serde_json::Value,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
SketchGroup(SketchGroup),
|
||||
ExtrudeGroup(ExtrudeGroup),
|
||||
ExtrudeTransform(ExtrudeTransform),
|
||||
Function {
|
||||
#[serde(skip)]
|
||||
func: Option<MemoryFunction>,
|
||||
expression: Box<FunctionExpression>,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtrudeTransform {
|
||||
pub position: Position,
|
||||
pub rotation: Rotation,
|
||||
#[serde(rename = "__meta")]
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
pub type MemoryFunction = fn(
|
||||
s: &[MemoryItem],
|
||||
memory: &ProgramMemory,
|
||||
expression: &FunctionExpression,
|
||||
metadata: &[Metadata],
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<Option<ProgramReturn>, KclError>;
|
||||
|
||||
impl From<MemoryItem> for Vec<SourceRange> {
|
||||
fn from(item: MemoryItem) -> Self {
|
||||
match item {
|
||||
MemoryItem::UserVal { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryItem {
|
||||
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
|
||||
if let MemoryItem::UserVal { value, .. } = self {
|
||||
Ok(value.clone())
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Not a user value: {:?}", self),
|
||||
source_ranges: self.clone().into(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_fn(
|
||||
&self,
|
||||
args: &[MemoryItem],
|
||||
memory: &ProgramMemory,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<Option<ProgramReturn>, KclError> {
|
||||
if let MemoryItem::Function {
|
||||
func,
|
||||
expression,
|
||||
meta,
|
||||
} = self
|
||||
{
|
||||
if let Some(func) = func {
|
||||
func(args, memory, expression, meta, engine)
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Not a function: {:?}", self),
|
||||
source_ranges: vec![],
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("not a function: {:?}", self),
|
||||
source_ranges: vec![],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sketch group is a collection of paths.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SketchGroup {
|
||||
/// The id of the sketch group.
|
||||
pub id: uuid::Uuid,
|
||||
/// The paths in the sketch group.
|
||||
pub value: Vec<Path>,
|
||||
/// The starting path.
|
||||
pub start: BasePath,
|
||||
/// The position of the sketch group.
|
||||
pub position: Position,
|
||||
/// The rotation of the sketch group.
|
||||
pub rotation: Rotation,
|
||||
/// Metadata.
|
||||
#[serde(rename = "__meta")]
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
impl SketchGroup {
|
||||
pub fn get_path_by_id(&self, id: &uuid::Uuid) -> Option<&Path> {
|
||||
self.value.iter().find(|p| p.get_id() == *id)
|
||||
}
|
||||
|
||||
pub fn get_path_by_name(&self, name: &str) -> Option<&Path> {
|
||||
self.value.iter().find(|p| p.get_name() == name)
|
||||
}
|
||||
|
||||
pub fn get_base_by_name_or_start(&self, name: &str) -> Option<&BasePath> {
|
||||
if self.start.name == name {
|
||||
Some(&self.start)
|
||||
} else {
|
||||
self.value
|
||||
.iter()
|
||||
.find(|p| p.get_name() == name)
|
||||
.map(|p| p.get_base())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_coords_from_paths(&self) -> Result<Point2d, KclError> {
|
||||
if self.value.is_empty() {
|
||||
return Ok(self.start.to.into());
|
||||
}
|
||||
|
||||
let index = self.value.len() - 1;
|
||||
if let Some(path) = self.value.get(index) {
|
||||
let base = path.get_base();
|
||||
Ok(base.to.into())
|
||||
} else {
|
||||
Ok(self.start.to.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An extrude group is a collection of extrude surfaces.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtrudeGroup {
|
||||
/// The id of the extrude group.
|
||||
pub id: uuid::Uuid,
|
||||
/// The extrude surfaces.
|
||||
pub value: Vec<ExtrudeSurface>,
|
||||
/// The height of the extrude group.
|
||||
pub height: f64,
|
||||
/// The position of the extrude group.
|
||||
pub position: Position,
|
||||
/// The rotation of the extrude group.
|
||||
pub rotation: Rotation,
|
||||
/// Metadata.
|
||||
#[serde(rename = "__meta")]
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
impl ExtrudeGroup {
|
||||
pub fn get_path_by_id(&self, id: &uuid::Uuid) -> Option<&ExtrudeSurface> {
|
||||
self.value.iter().find(|p| p.get_id() == *id)
|
||||
}
|
||||
|
||||
pub fn get_path_by_name(&self, name: &str) -> Option<&ExtrudeSurface> {
|
||||
self.value.iter().find(|p| p.get_name() == name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum BodyType {
|
||||
Root,
|
||||
Sketch,
|
||||
Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Position(pub [f64; 3]);
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Rotation(pub [f64; 4]);
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct SourceRange(pub [usize; 2]);
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Point2d {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl From<[f64; 2]> for Point2d {
|
||||
fn from(p: [f64; 2]) -> Self {
|
||||
Self { x: p[0], y: p[1] }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point2d> for [f64; 2] {
|
||||
fn from(p: Point2d) -> Self {
|
||||
[p.x, p.y]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Point3d {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub z: f64,
|
||||
}
|
||||
|
||||
/// Metadata.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Metadata {
|
||||
/// The source range.
|
||||
pub source_range: SourceRange,
|
||||
}
|
||||
|
||||
impl From<SourceRange> for Metadata {
|
||||
fn from(source_range: SourceRange) -> Self {
|
||||
Self { source_range }
|
||||
}
|
||||
}
|
||||
|
||||
/// A base path.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasePath {
|
||||
/// The from point.
|
||||
pub from: [f64; 2],
|
||||
/// The to point.
|
||||
pub to: [f64; 2],
|
||||
/// The name of the path.
|
||||
pub name: String,
|
||||
/// Metadata.
|
||||
#[serde(rename = "__geoMeta")]
|
||||
pub geo_meta: GeoMeta,
|
||||
}
|
||||
|
||||
/// Geometry metadata.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeoMeta {
|
||||
/// The id of the geometry.
|
||||
pub id: uuid::Uuid,
|
||||
/// Metadata.
|
||||
#[serde(flatten)]
|
||||
pub metadata: Metadata,
|
||||
}
|
||||
|
||||
/// A path.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Path {
|
||||
/// A path that goes to a point.
|
||||
ToPoint {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
},
|
||||
/// A path that is horizontal.
|
||||
Horizontal {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
/// The x coordinate.
|
||||
x: f64,
|
||||
},
|
||||
/// An angled line to.
|
||||
AngledLineTo {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
/// The x coordinate.
|
||||
x: Option<f64>,
|
||||
/// The y coordinate.
|
||||
y: Option<f64>,
|
||||
},
|
||||
/// A base path.
|
||||
Base {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
},
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn get_id(&self) -> uuid::Uuid {
|
||||
match self {
|
||||
Path::ToPoint { base } => base.geo_meta.id,
|
||||
Path::Horizontal { base, .. } => base.geo_meta.id,
|
||||
Path::AngledLineTo { base, .. } => base.geo_meta.id,
|
||||
Path::Base { base } => base.geo_meta.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> String {
|
||||
match self {
|
||||
Path::ToPoint { base } => base.name.clone(),
|
||||
Path::Horizontal { base, .. } => base.name.clone(),
|
||||
Path::AngledLineTo { base, .. } => base.name.clone(),
|
||||
Path::Base { base } => base.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_base(&self) -> &BasePath {
|
||||
match self {
|
||||
Path::ToPoint { base } => base,
|
||||
Path::Horizontal { base, .. } => base,
|
||||
Path::AngledLineTo { base, .. } => base,
|
||||
Path::Base { base } => base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An extrude surface.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum ExtrudeSurface {
|
||||
/// An extrude plane.
|
||||
ExtrudePlane {
|
||||
/// The position.
|
||||
position: Position,
|
||||
/// The rotation.
|
||||
rotation: Rotation,
|
||||
/// The name.
|
||||
name: String,
|
||||
/// Metadata.
|
||||
#[serde(flatten)]
|
||||
geo_meta: GeoMeta,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExtrudeSurface {
|
||||
pub fn get_id(&self) -> uuid::Uuid {
|
||||
match self {
|
||||
ExtrudeSurface::ExtrudePlane { geo_meta, .. } => geo_meta.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> String {
|
||||
match self {
|
||||
ExtrudeSurface::ExtrudePlane { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> Position {
|
||||
match self {
|
||||
ExtrudeSurface::ExtrudePlane { position, .. } => *position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rotation(&self) -> Rotation {
|
||||
match self {
|
||||
ExtrudeSurface::ExtrudePlane { rotation, .. } => *rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipeInfo {
|
||||
pub previous_results: Vec<MemoryItem>,
|
||||
pub is_in_pipe: bool,
|
||||
pub index: usize,
|
||||
pub body: Vec<Value>,
|
||||
}
|
||||
|
||||
impl PipeInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
previous_results: Vec::new(),
|
||||
is_in_pipe: false,
|
||||
index: 0,
|
||||
body: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PipeInfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a AST's program.
|
||||
fn execute(
|
||||
program: crate::abstract_syntax_tree_types::Program,
|
||||
memory: &mut ProgramMemory,
|
||||
options: BodyType,
|
||||
engine: &mut EngineConnection,
|
||||
) -> Result<ProgramMemory, KclError> {
|
||||
let mut pipe_info = PipeInfo::default();
|
||||
let stdlib = crate::std::StdLib::new();
|
||||
|
||||
// Iterate over the body of the program.
|
||||
for statement in &program.body {
|
||||
match statement {
|
||||
BodyItem::ExpressionStatement(expression_statement) => {
|
||||
if let Value::CallExpression(call_expr) = &expression_statement.expression {
|
||||
let fn_name = call_expr.callee.name.to_string();
|
||||
let mut args: Vec<MemoryItem> = Vec::new();
|
||||
for arg in &call_expr.arguments {
|
||||
match arg {
|
||||
Value::Literal(literal) => args.push(literal.into()),
|
||||
Value::Identifier(identifier) => {
|
||||
let memory_item =
|
||||
memory.get(&identifier.name, identifier.into())?;
|
||||
args.push(memory_item.clone());
|
||||
}
|
||||
// We do nothing for the rest.
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
if fn_name == "show" {
|
||||
if options != BodyType::Root {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Cannot call show outside of a root".to_string(),
|
||||
source_ranges: vec![call_expr.into()],
|
||||
}));
|
||||
}
|
||||
|
||||
memory.return_ =
|
||||
Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
|
||||
} else if let Some(func) = memory.clone().root.get(&fn_name) {
|
||||
func.call_fn(&args, memory, engine)?;
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("No such name {} defined", fn_name),
|
||||
source_ranges: vec![call_expr.into()],
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
BodyItem::VariableDeclaration(variable_declaration) => {
|
||||
for declaration in &variable_declaration.declarations {
|
||||
let var_name = declaration.id.name.to_string();
|
||||
let source_range: SourceRange = declaration.init.clone().into();
|
||||
let metadata = Metadata { source_range };
|
||||
|
||||
match &declaration.init {
|
||||
Value::Literal(literal) => {
|
||||
memory.add(&var_name, literal.into(), source_range)?;
|
||||
}
|
||||
Value::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?;
|
||||
memory.add(&var_name, value.clone(), source_range)?;
|
||||
}
|
||||
Value::BinaryExpression(binary_expression) => {
|
||||
let result = binary_expression.get_result(
|
||||
memory,
|
||||
&mut pipe_info,
|
||||
&stdlib,
|
||||
engine,
|
||||
)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
Value::FunctionExpression(function_expression) => {
|
||||
memory.add(
|
||||
&var_name,
|
||||
MemoryItem::Function{
|
||||
expression: function_expression.clone(),
|
||||
meta: vec![metadata],
|
||||
func: Some(|args: &[MemoryItem], memory: &ProgramMemory, function_expression: &FunctionExpression, _metadata: &[Metadata], engine: &mut EngineConnection| -> Result<Option<ProgramReturn>, KclError> {
|
||||
let mut fn_memory = memory.clone();
|
||||
|
||||
if args.len() != function_expression.params.len() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Expected {} arguments, got {}", function_expression.params.len(), args.len()),
|
||||
source_ranges: vec![function_expression.into()],
|
||||
}));
|
||||
}
|
||||
|
||||
// Add the arguments to the memory.
|
||||
for (index, param) in function_expression.params.iter().enumerate() {
|
||||
fn_memory.add(
|
||||
¶m.name,
|
||||
args.clone().get(index).unwrap().clone(),
|
||||
param.into(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let result = execute(function_expression.body.clone(), &mut fn_memory, BodyType::Block, engine)?;
|
||||
|
||||
Ok(result.return_)
|
||||
})
|
||||
},
|
||||
source_range,
|
||||
)?;
|
||||
}
|
||||
Value::CallExpression(call_expression) => {
|
||||
let result =
|
||||
call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
Value::PipeExpression(pipe_expression) => {
|
||||
let result = pipe_expression.get_result(
|
||||
memory,
|
||||
&mut pipe_info,
|
||||
&stdlib,
|
||||
engine,
|
||||
)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
Value::PipeSubstitution(pipe_substitution) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("pipe substitution not implemented for declaration of variable {}", var_name),
|
||||
source_ranges: vec![pipe_substitution.into()],
|
||||
}));
|
||||
}
|
||||
Value::ArrayExpression(array_expression) => {
|
||||
let result = array_expression.execute(
|
||||
memory,
|
||||
&mut pipe_info,
|
||||
&stdlib,
|
||||
engine,
|
||||
)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
Value::ObjectExpression(object_expression) => {
|
||||
let result = object_expression.execute(
|
||||
memory,
|
||||
&mut pipe_info,
|
||||
&stdlib,
|
||||
engine,
|
||||
)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
Value::MemberExpression(member_expression) => {
|
||||
let result = member_expression.get_result(memory)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
Value::UnaryExpression(unary_expression) => {
|
||||
let result = unary_expression.get_result(
|
||||
memory,
|
||||
&mut pipe_info,
|
||||
&stdlib,
|
||||
engine,
|
||||
)?;
|
||||
memory.add(&var_name, result, source_range)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(return_statement) => match &return_statement.argument {
|
||||
Value::BinaryExpression(bin_expr) => {
|
||||
let result = bin_expr.get_result(memory, &mut pipe_info, &stdlib, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?.clone();
|
||||
memory.return_ = Some(ProgramReturn::Value(value));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(memory.clone())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg(not(test))]
|
||||
#[wasm_bindgen]
|
||||
pub async fn execute_wasm(
|
||||
program_str: &str,
|
||||
memory_str: &str,
|
||||
manager: crate::engine::conn_web::EngineCommandManager,
|
||||
) -> Result<JsValue, String> {
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
|
||||
// deserialize the ast from a stringified json
|
||||
let program: crate::abstract_syntax_tree_types::Program =
|
||||
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
|
||||
let mut mem: ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut engine = EngineConnection::new(manager)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
let memory = execute(program, &mut mem, BodyType::Root, &mut engine).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(&memory).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
#[cfg(not(feature = "web"))]
|
||||
#[wasm_bindgen]
|
||||
pub async fn execute_wasm(program_str: &str, memory_str: &str) -> Result<JsValue, String> {
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
|
||||
// deserialize the ast from a stringified json
|
||||
let program: crate::abstract_syntax_tree_types::Program =
|
||||
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
|
||||
let mut mem: ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut engine = EngineConnection::new("dev.kittycad.io", "some-token", "")
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
let memory = execute(program, &mut mem, BodyType::Root, &mut engine).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(&memory).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
|
||||
let tokens = crate::tokeniser::lexer(code);
|
||||
let program = crate::parser::abstract_syntax_tree(&tokens)?;
|
||||
let mut mem: ProgramMemory = Default::default();
|
||||
let mut engine = EngineConnection::new("dev.kittycad.io", "some-token", "").await?;
|
||||
let memory = execute(program, &mut mem, BodyType::Root, &mut engine)?;
|
||||
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_assign_two_variables() {
|
||||
let ast = r#"const myVar = 5
|
||||
const newVar = myVar + 1"#;
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(5),
|
||||
memory.root.get("myVar").unwrap().get_json_value().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::json!(6.0),
|
||||
memory.root.get("newVar").unwrap().get_json_value().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_angled_line_that_intersects() {
|
||||
let ast_fn = |offset: &str| -> String {
|
||||
format!(
|
||||
r#"const part001 = startSketchAt([0, 0])
|
||||
|> lineTo({{to:[2, 2], tag: "yo"}}, %)
|
||||
|> lineTo([3, 1], %)
|
||||
|> angledLineThatIntersects({{
|
||||
angle: 180,
|
||||
intersectTag: 'yo',
|
||||
offset: {},
|
||||
tag: "yo2"
|
||||
}}, %)
|
||||
const intersect = segEndX('yo2', part001)
|
||||
show(part001)"#,
|
||||
offset
|
||||
)
|
||||
};
|
||||
|
||||
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(1.0 + 2.0f64.sqrt()),
|
||||
memory
|
||||
.root
|
||||
.get("intersect")
|
||||
.unwrap()
|
||||
.get_json_value()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let memory = parse_execute(&ast_fn("0")).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(1.0000000000000002),
|
||||
memory
|
||||
.root
|
||||
.get("intersect")
|
||||
.unwrap()
|
||||
.get_json_value()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_fn_definitions() {
|
||||
let ast = r#"const def = (x) => {
|
||||
return x
|
||||
}
|
||||
const ghi = (x) => {
|
||||
return x
|
||||
}
|
||||
const jkl = (x) => {
|
||||
return x
|
||||
}
|
||||
const hmm = (x) => {
|
||||
return x
|
||||
}
|
||||
|
||||
const yo = 5 + 6
|
||||
|
||||
const abc = 3
|
||||
const identifierGuy = 5
|
||||
const part001 = startSketchAt([-1.2, 4.83])
|
||||
|> line([2.8, 0], %)
|
||||
|> angledLine([100 + 100, 3.01], %)
|
||||
|> angledLine([abc, 3.02], %)
|
||||
|> angledLine([def(yo), 3.03], %)
|
||||
|> angledLine([ghi(2), 3.04], %)
|
||||
|> angledLine([jkl(yo) + 2, 3.05], %)
|
||||
|> close(%)
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_pipe_substitutions_unary() {
|
||||
let ast = r#"const myVar = 3
|
||||
const part001 = startSketchAt([0, 0])
|
||||
|> line({ to: [3, 4], tag: 'seg01' }, %)
|
||||
|> line([
|
||||
min(segLen('seg01', %), myVar),
|
||||
-legLen(segLen('seg01', %), myVar)
|
||||
], %)
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_pipe_substitutions() {
|
||||
let ast = r#"const myVar = 3
|
||||
const part001 = startSketchAt([0, 0])
|
||||
|> line({ to: [3, 4], tag: 'seg01' }, %)
|
||||
|> line([
|
||||
min(segLen('seg01', %), myVar),
|
||||
legLen(segLen('seg01', %), myVar)
|
||||
], %)
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
}
|
@ -1,13 +1,21 @@
|
||||
//! Functions for exported files from the server.
|
||||
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
|
||||
let ws_resp: kittycad::types::WebSocketResponses = bincode::deserialize(data)?;
|
||||
let ws_resp: kittycad::types::WebSocketResponse = bincode::deserialize(data)?;
|
||||
|
||||
if let kittycad::types::WebSocketResponses::Export { files } = ws_resp {
|
||||
return Ok(serde_wasm_bindgen::to_value(&files)?);
|
||||
if !ws_resp.success {
|
||||
return Err(JsError::new(&format!(
|
||||
"Server returned error: {:?}",
|
||||
ws_resp.errors
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(kittycad::types::OkWebSocketResponseData::Export { files }) = ws_resp.resp {
|
||||
return Ok(JsValue::from_serde(&files)?);
|
||||
}
|
||||
|
||||
Err(JsError::new(&format!(
|
||||
|
@ -1,7 +1,11 @@
|
||||
mod abstract_syntax_tree_types;
|
||||
mod docs;
|
||||
mod engine;
|
||||
mod errors;
|
||||
mod executor;
|
||||
mod export;
|
||||
mod math_parser;
|
||||
mod parser;
|
||||
mod recast;
|
||||
mod std;
|
||||
mod tokeniser;
|
||||
|
@ -147,7 +147,7 @@ pub fn reverse_polish_notation(
|
||||
}
|
||||
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!(
|
||||
"Unexpected token: {} {:?}",
|
||||
current_token.value, current_token.token_type
|
||||
@ -260,19 +260,13 @@ fn build_tree(
|
||||
serde_json::Value::Number(n)
|
||||
} else {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
current_token.start as i32,
|
||||
current_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("Invalid float: {}", current_token.value),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
current_token.start as i32,
|
||||
current_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("Invalid integer: {}", current_token.value),
|
||||
}));
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::abstract_syntax_tree_types::{
|
||||
ArrayExpression, BinaryExpression, BinaryPart, BlockStatement, BodyItem, CallExpression,
|
||||
ExpressionStatement, FunctionExpression, Identifier, Literal, LiteralIdentifier,
|
||||
MemberExpression, MemberObject, NoneCodeMeta, NoneCodeNode, ObjectExpression, ObjectKeyInfo,
|
||||
ObjectProperty, PipeExpression, PipeSubstitution, Program, ReturnStatement, UnaryExpression,
|
||||
Value, VariableDeclaration, VariableDeclarator,
|
||||
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
|
||||
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject,
|
||||
NoneCodeMeta, NoneCodeNode, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression,
|
||||
PipeSubstitution, Program, ReturnStatement, UnaryExpression, Value, VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
};
|
||||
use crate::errors::{KclError, KclErrorDetails};
|
||||
use crate::math_parser::parse_expression;
|
||||
@ -34,13 +34,13 @@ pub fn make_literal(tokens: &[Token], index: usize) -> Result<Literal, KclError>
|
||||
serde_json::Value::Number(n)
|
||||
} else {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[token.start as i32, token.end as i32]],
|
||||
source_ranges: vec![token.into()],
|
||||
message: format!("Invalid float: {}", token.value),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[token.start as i32, token.end as i32]],
|
||||
source_ranges: vec![token.into()],
|
||||
message: format!("Invalid integer: {}", token.value),
|
||||
}));
|
||||
}
|
||||
@ -165,7 +165,7 @@ pub fn find_closing_brace(
|
||||
search_opening_brace = ¤t_token.value;
|
||||
if !["(", "{", "["].contains(&search_opening_brace) {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!(
|
||||
"expected to be started on a opening brace ( {{ [, instead found '{}'",
|
||||
search_opening_brace
|
||||
@ -380,10 +380,7 @@ fn collect_object_keys(
|
||||
collect_object_keys(tokens, key_token.index, Some(new_previous_keys))
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
period_or_opening_bracket_token.start as i32,
|
||||
period_or_opening_bracket_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![period_or_opening_bracket_token.clone().into()],
|
||||
message: format!(
|
||||
"expression with token type {:?}",
|
||||
period_or_opening_bracket_token.token_type
|
||||
@ -508,7 +505,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
|
||||
}
|
||||
} else {
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!(
|
||||
"expression with token type {:?}",
|
||||
current_token.token_type
|
||||
@ -593,16 +590,16 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
|
||||
last_index: function_expression.last_index,
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "expression with braces".to_string(),
|
||||
}))
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "expression with braces".to_string(),
|
||||
}))
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@ -614,9 +611,9 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
|
||||
});
|
||||
}
|
||||
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
message: format!("expression with token type {:?}", current_token.token_type),
|
||||
Err(KclError::Unexpected(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("{:?}", current_token.token_type),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -645,7 +642,7 @@ fn make_array_elements(
|
||||
let is_comma = next_token_token.token_type == TokenType::Comma;
|
||||
if !is_closing_brace && !is_comma {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[next_token_token.start as i32, next_token_token.end as i32]],
|
||||
source_ranges: vec![next_token_token.clone().into()],
|
||||
message: format!(
|
||||
"Expected a comma or closing brace, found {:?}",
|
||||
next_token_token.value
|
||||
@ -662,10 +659,7 @@ fn make_array_elements(
|
||||
make_array_elements(tokens, next_call_index, _previous_elements)
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
first_element_token.start as i32,
|
||||
first_element_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![first_element_token.into()],
|
||||
message: "no next token".to_string(),
|
||||
}))
|
||||
}
|
||||
@ -720,7 +714,7 @@ fn make_pipe_body(
|
||||
last_index = val.last_index;
|
||||
} else {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!(
|
||||
"Expected a pipe value, found {:?}",
|
||||
current_token.token_type
|
||||
@ -899,13 +893,10 @@ fn make_arguments(
|
||||
make_arguments(tokens, next_comma_or_brace_token_index, _previous_args)
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
argument_token_token.start as i32,
|
||||
argument_token_token.end as i32,
|
||||
]],
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![argument_token_token.clone().into()],
|
||||
message: format!("Unexpected token {} ", argument_token_token.value),
|
||||
}))
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@ -929,27 +920,18 @@ fn make_arguments(
|
||||
}
|
||||
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
argument_token_token.start as i32,
|
||||
argument_token_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![argument_token_token.clone().into()],
|
||||
message: format!("Unexpected token {} ", argument_token_token.value),
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
brace_or_comma_token.start as i32,
|
||||
brace_or_comma_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {} ", brace_or_comma_token.value),
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
brace_or_comma_token.start as i32,
|
||||
brace_or_comma_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {} ", brace_or_comma_token.value),
|
||||
}))
|
||||
}
|
||||
@ -1045,7 +1027,7 @@ fn make_variable_declarators(
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.clone().into()],
|
||||
message: format!("Unexpected token {} ", current_token.value),
|
||||
}))
|
||||
}
|
||||
@ -1113,10 +1095,7 @@ fn make_params(
|
||||
make_params(tokens, next_brace_or_comma_token.index, _previous_params)
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
brace_or_comma_token.start as i32,
|
||||
brace_or_comma_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {} ", brace_or_comma_token.value),
|
||||
}))
|
||||
}
|
||||
@ -1158,7 +1137,7 @@ fn make_unary_expression(
|
||||
}
|
||||
_ => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "Invalid argument for unary expression".to_string(),
|
||||
}));
|
||||
}
|
||||
@ -1205,7 +1184,7 @@ fn make_expression_statement(
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[current_token.start as i32, current_token.end as i32]],
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "make_expression_statement".to_string(),
|
||||
}))
|
||||
}
|
||||
@ -1266,10 +1245,7 @@ fn make_object_properties(
|
||||
make_object_properties(tokens, next_key_index, _previous_properties)
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![[
|
||||
property_key_token.start as i32,
|
||||
property_key_token.end as i32,
|
||||
]],
|
||||
source_ranges: vec![property_key_token.into()],
|
||||
message: "make_object_properties".to_string(),
|
||||
}))
|
||||
}
|
||||
@ -1470,13 +1446,13 @@ fn make_body(
|
||||
}
|
||||
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![[token.start as i32, token.end as i32]],
|
||||
source_ranges: vec![token.into()],
|
||||
message: "unexpected token".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
struct BlockStatementResult {
|
||||
block: BlockStatement,
|
||||
block: Program,
|
||||
last_index: usize,
|
||||
}
|
||||
|
||||
@ -1505,7 +1481,7 @@ fn make_block_statement(tokens: &[Token], index: usize) -> Result<BlockStatement
|
||||
)?
|
||||
};
|
||||
Ok(BlockStatementResult {
|
||||
block: BlockStatement {
|
||||
block: Program {
|
||||
start: opening_curly.start,
|
||||
end: tokens[body.last_index].end,
|
||||
body: body.body,
|
||||
@ -1564,19 +1540,11 @@ pub fn abstract_syntax_tree(tokens: &[Token]) -> Result<Program, KclError> {
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
// Use `js_namespace` here to bind `console.log(..)` instead of just
|
||||
// `log(..)`
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_js(js: &str) -> Result<JsValue, String> {
|
||||
let tokens = lexer(js);
|
||||
let program = abstract_syntax_tree(&tokens).map_err(String::from)?;
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// 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())
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
//! Generates source code from the AST.
|
||||
//! The inverse of parsing (which generates an AST from the source code)
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::abstract_syntax_tree_types::{
|
||||
@ -403,10 +404,10 @@ pub fn recast_function(expression: FunctionExpression) -> String {
|
||||
// wasm_bindgen wrapper for recast
|
||||
// test for this function and by extension the recaster are done in javascript land src/lang/recast.test.ts
|
||||
#[wasm_bindgen]
|
||||
pub fn recast_js(json_str: &str) -> Result<JsValue, JsError> {
|
||||
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
|
||||
// deserialize the ast from a stringified json
|
||||
let program: Program = serde_json::from_str(json_str).map_err(JsError::from)?;
|
||||
|
||||
let result = recast(&program, "", false);
|
||||
Ok(serde_wasm_bindgen::to_value(&result)?)
|
||||
Ok(JsValue::from_serde(&result)?)
|
||||
}
|
||||
|
85
src/wasm-lib/src/std/extrude.rs
Normal file
85
src/wasm-lib/src/std/extrude.rs
Normal file
@ -0,0 +1,85 @@
|
||||
//! Functions related to extruding.
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, ExtrudeTransform, MemoryItem, SketchGroup},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
/// Extrudes by a given amount.
|
||||
pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (length, sketch_group) = args.get_number_sketch_group()?;
|
||||
|
||||
let result = inner_extrude(length, sketch_group, args)?;
|
||||
|
||||
Ok(MemoryItem::ExtrudeGroup(result))
|
||||
}
|
||||
|
||||
/// Extrudes by a given amount.
|
||||
#[stdlib {
|
||||
name = "extrude"
|
||||
}]
|
||||
fn inner_extrude(
|
||||
length: f64,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<ExtrudeGroup, KclError> {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
let cmd = kittycad::types::ModelingCmd::Extrude {
|
||||
target: sketch_group.id,
|
||||
distance: length,
|
||||
cap: true,
|
||||
};
|
||||
args.send_modeling_cmd(id, cmd)?;
|
||||
|
||||
Ok(ExtrudeGroup {
|
||||
id,
|
||||
// TODO, this is just an empty array now, should be deleted. This
|
||||
// comment was originally in the JS code.
|
||||
value: Default::default(),
|
||||
height: length,
|
||||
position: sketch_group.position,
|
||||
rotation: sketch_group.rotation,
|
||||
meta: sketch_group.meta,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the extrude wall transform.
|
||||
pub fn get_extrude_wall_transform(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (surface_name, extrude_group) = args.get_path_name_extrude_group()?;
|
||||
let result = inner_get_extrude_wall_transform(&surface_name, extrude_group, args)?;
|
||||
Ok(MemoryItem::ExtrudeTransform(result))
|
||||
}
|
||||
|
||||
/// Returns the extrude wall transform.
|
||||
#[stdlib {
|
||||
name = "getExtrudeWallTransform"
|
||||
}]
|
||||
fn inner_get_extrude_wall_transform(
|
||||
surface_name: &str,
|
||||
extrude_group: ExtrudeGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<ExtrudeTransform, KclError> {
|
||||
let surface = extrude_group
|
||||
.get_path_by_name(surface_name)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
|
||||
surface_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(ExtrudeTransform {
|
||||
position: surface.get_position(),
|
||||
rotation: surface.get_rotation(),
|
||||
meta: extrude_group.meta,
|
||||
})
|
||||
}
|
684
src/wasm-lib/src/std/mod.rs
Normal file
684
src/wasm-lib/src/std/mod.rs
Normal file
@ -0,0 +1,684 @@
|
||||
//! Functions implemented for language execution.
|
||||
|
||||
mod extrude;
|
||||
mod segment;
|
||||
mod sketch;
|
||||
mod utils;
|
||||
|
||||
// TODO: Something that would be nice is if we could generate docs for Kcl based on the
|
||||
// actual stdlib functions below.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
abstract_syntax_tree_types::parse_json_number_as_f64,
|
||||
engine::EngineConnection,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, MemoryItem, Metadata, SketchGroup, SourceRange},
|
||||
};
|
||||
|
||||
pub type FnMap = HashMap<String, StdFn>;
|
||||
pub type StdFn = fn(&mut Args) -> Result<MemoryItem, KclError>;
|
||||
|
||||
pub struct StdLib {
|
||||
#[allow(dead_code)]
|
||||
internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
|
||||
|
||||
pub fns: FnMap,
|
||||
}
|
||||
|
||||
impl StdLib {
|
||||
pub fn new() -> Self {
|
||||
let internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>> = vec![
|
||||
Box::new(Min),
|
||||
Box::new(LegLen),
|
||||
Box::new(LegAngX),
|
||||
Box::new(LegAngY),
|
||||
Box::new(crate::std::extrude::Extrude),
|
||||
Box::new(crate::std::extrude::GetExtrudeWallTransform),
|
||||
Box::new(crate::std::segment::SegEndX),
|
||||
Box::new(crate::std::segment::SegEndY),
|
||||
Box::new(crate::std::segment::LastSegX),
|
||||
Box::new(crate::std::segment::LastSegY),
|
||||
Box::new(crate::std::segment::SegLen),
|
||||
Box::new(crate::std::segment::SegAng),
|
||||
Box::new(crate::std::segment::AngleToMatchLengthX),
|
||||
Box::new(crate::std::segment::AngleToMatchLengthY),
|
||||
Box::new(crate::std::sketch::LineTo),
|
||||
Box::new(crate::std::sketch::Line),
|
||||
Box::new(crate::std::sketch::XLineTo),
|
||||
Box::new(crate::std::sketch::XLine),
|
||||
Box::new(crate::std::sketch::YLineTo),
|
||||
Box::new(crate::std::sketch::YLine),
|
||||
Box::new(crate::std::sketch::AngledLineToX),
|
||||
Box::new(crate::std::sketch::AngledLineToY),
|
||||
Box::new(crate::std::sketch::AngledLine),
|
||||
Box::new(crate::std::sketch::AngledLineOfXLength),
|
||||
Box::new(crate::std::sketch::AngledLineOfYLength),
|
||||
Box::new(crate::std::sketch::AngledLineThatIntersects),
|
||||
Box::new(crate::std::sketch::StartSketchAt),
|
||||
Box::new(crate::std::sketch::Close),
|
||||
];
|
||||
|
||||
let mut fns = HashMap::new();
|
||||
for internal_fn_name in &internal_fn_names {
|
||||
fns.insert(
|
||||
internal_fn_name.name().to_string(),
|
||||
internal_fn_name.std_lib_fn(),
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
internal_fn_names,
|
||||
fns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StdLib {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Args<'a> {
|
||||
pub args: Vec<MemoryItem>,
|
||||
pub source_range: SourceRange,
|
||||
engine: &'a mut EngineConnection,
|
||||
}
|
||||
|
||||
impl<'a> Args<'a> {
|
||||
pub fn new(
|
||||
args: Vec<MemoryItem>,
|
||||
source_range: SourceRange,
|
||||
engine: &'a mut EngineConnection,
|
||||
) -> Self {
|
||||
Self {
|
||||
args,
|
||||
source_range,
|
||||
engine,
|
||||
}
|
||||
}
|
||||
pub fn send_modeling_cmd(
|
||||
&mut self,
|
||||
id: uuid::Uuid,
|
||||
cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<(), KclError> {
|
||||
self.engine.send_modeling_cmd(id, self.source_range, cmd)
|
||||
}
|
||||
|
||||
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
|
||||
Ok(MemoryItem::UserVal {
|
||||
value: j,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.source_range,
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
|
||||
self.make_user_val_from_json(serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(f).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Failed to convert `{}` to a number", f),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
|
||||
let mut numbers: Vec<f64> = Vec::new();
|
||||
for arg in &self.args {
|
||||
let parsed = arg.get_json_value()?;
|
||||
numbers.push(parse_json_number_as_f64(&parsed, self.source_range)?);
|
||||
}
|
||||
Ok(numbers)
|
||||
}
|
||||
|
||||
fn get_hypotenuse_leg(&self) -> Result<(f64, f64), KclError> {
|
||||
let numbers = self.get_number_array()?;
|
||||
|
||||
if numbers.len() != 2 {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a number array of length 2, found `{:?}`", numbers),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
Ok((numbers[0], numbers[1]))
|
||||
}
|
||||
|
||||
fn get_segment_name_sketch_group(&self) -> Result<(String, SketchGroup), KclError> {
|
||||
// Iterate over our args, the first argument should be a UserVal with a string value.
|
||||
// The second argument should be a SketchGroup.
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a string as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let segment_name = if let serde_json::Value::String(s) = first_value {
|
||||
s.to_string()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a string as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((segment_name, sketch_group))
|
||||
}
|
||||
|
||||
fn get_sketch_group(&self) -> Result<SketchGroup, KclError> {
|
||||
let first_value = self.args.first().ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_group = if let MemoryItem::SketchGroup(sg) = first_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok(sketch_group)
|
||||
}
|
||||
|
||||
fn get_data<T: serde::de::DeserializeOwned>(&self) -> Result<T, KclError> {
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a struct as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let data: T = serde_json::from_value(first_value).map_err(|e| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Failed to deserialize struct from JSON: {}", e),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
) -> Result<(T, SketchGroup), KclError> {
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a struct as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let data: T = serde_json::from_value(first_value).map_err(|e| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Failed to deserialize struct from JSON: {}", e),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((data, sketch_group))
|
||||
}
|
||||
|
||||
fn get_segment_name_to_number_sketch_group(
|
||||
&self,
|
||||
) -> Result<(String, f64, SketchGroup), KclError> {
|
||||
// Iterate over our args, the first argument should be a UserVal with a string value.
|
||||
// The second argument should be a number.
|
||||
// The third argument should be a SketchGroup.
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a string as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let segment_name = if let serde_json::Value::String(s) = first_value {
|
||||
s.to_string()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a string as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
let second_value = self
|
||||
.args
|
||||
.get(1)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a number as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let to_number = parse_json_number_as_f64(&second_value, self.source_range)?;
|
||||
|
||||
let third_value = self.args.get(2).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the third argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_group = if let MemoryItem::SketchGroup(sg) = third_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the third argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((segment_name, to_number, sketch_group))
|
||||
}
|
||||
|
||||
fn get_number_sketch_group(&self) -> Result<(f64, SketchGroup), KclError> {
|
||||
// Iterate over our args, the first argument should be a number.
|
||||
// The second argument should be a SketchGroup.
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a number as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let number = parse_json_number_as_f64(&first_value, self.source_range)?;
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((number, sketch_group))
|
||||
}
|
||||
|
||||
fn get_path_name_extrude_group(&self) -> Result<(String, ExtrudeGroup), KclError> {
|
||||
// Iterate over our args, the first argument should be a UserVal with a string value.
|
||||
// The second argument should be a ExtrudeGroup.
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a string as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let path_name = if let serde_json::Value::String(s) = first_value {
|
||||
s.to_string()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a string as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a ExtrudeGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let extrude_group = if let MemoryItem::ExtrudeGroup(sg) = second_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a ExtrudeGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((path_name, extrude_group))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum of the given arguments.
|
||||
/// TODO fix min
|
||||
pub fn min(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let nums = args.get_number_array()?;
|
||||
let result = inner_min(nums);
|
||||
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the minimum of the given arguments.
|
||||
#[stdlib {
|
||||
name = "min",
|
||||
}]
|
||||
fn inner_min(args: Vec<f64>) -> f64 {
|
||||
let mut min = std::f64::MAX;
|
||||
for arg in args.iter() {
|
||||
if *arg < min {
|
||||
min = *arg;
|
||||
}
|
||||
}
|
||||
|
||||
min
|
||||
}
|
||||
|
||||
/// Returns the length of the given leg.
|
||||
pub fn leg_length(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||
let result = inner_leg_length(hypotenuse, leg);
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the length of the given leg.
|
||||
#[stdlib {
|
||||
name = "legLen",
|
||||
}]
|
||||
fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
|
||||
(hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt()
|
||||
}
|
||||
|
||||
/// Returns the angle of the given leg for x.
|
||||
pub fn leg_angle_x(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||
let result = inner_leg_angle_x(hypotenuse, leg);
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the angle of the given leg for x.
|
||||
#[stdlib {
|
||||
name = "legAngX",
|
||||
}]
|
||||
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
|
||||
(leg.min(hypotenuse) / hypotenuse).acos() * 180.0 / std::f64::consts::PI
|
||||
}
|
||||
|
||||
/// Returns the angle of the given leg for y.
|
||||
pub fn leg_angle_y(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||
let result = inner_leg_angle_y(hypotenuse, leg);
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the angle of the given leg for y.
|
||||
#[stdlib {
|
||||
name = "legAngY",
|
||||
}]
|
||||
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {
|
||||
(leg.min(hypotenuse) / hypotenuse).asin() * 180.0 / std::f64::consts::PI
|
||||
}
|
||||
|
||||
/// The primitive types that can be used in a KCL file.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Display, FromStr)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[display(style = "lowercase")]
|
||||
pub enum Primitive {
|
||||
/// A boolean value.
|
||||
Bool,
|
||||
/// A number value.
|
||||
Number,
|
||||
/// A string value.
|
||||
String,
|
||||
/// A uuid value.
|
||||
Uuid,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::std::StdLib;
|
||||
|
||||
#[test]
|
||||
fn test_generate_stdlib_markdown_docs() {
|
||||
let stdlib = StdLib::new();
|
||||
let mut buf = String::new();
|
||||
|
||||
buf.push_str("<!--- DO NOT EDIT THIS FILE. IT IS AUTOMATICALLY GENERATED. -->\n\n");
|
||||
|
||||
buf.push_str("# KCL Standard Library\n\n");
|
||||
|
||||
// Generate a table of contents.
|
||||
buf.push_str("## Table of Contents\n\n");
|
||||
|
||||
buf.push_str("* [Functions](#functions)\n");
|
||||
|
||||
for internal_fn in &stdlib.internal_fn_names {
|
||||
if internal_fn.unpublished() || internal_fn.deprecated() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.push_str(&format!(
|
||||
"\t* [`{}`](#{})\n",
|
||||
internal_fn.name(),
|
||||
internal_fn.name()
|
||||
));
|
||||
}
|
||||
|
||||
buf.push_str("\n\n");
|
||||
|
||||
buf.push_str("## Functions\n\n");
|
||||
|
||||
for internal_fn in &stdlib.internal_fn_names {
|
||||
if internal_fn.unpublished() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut fn_docs = String::new();
|
||||
|
||||
if internal_fn.deprecated() {
|
||||
fn_docs.push_str(&format!("### {} DEPRECATED\n\n", internal_fn.name()));
|
||||
} else {
|
||||
fn_docs.push_str(&format!("### {}\n\n", internal_fn.name()));
|
||||
}
|
||||
|
||||
fn_docs.push_str(&format!("{}\n\n", internal_fn.summary()));
|
||||
fn_docs.push_str(&format!("{}\n\n", internal_fn.description()));
|
||||
|
||||
fn_docs.push_str("```\n");
|
||||
fn_docs.push_str(&format!("{}(", internal_fn.name()));
|
||||
for (i, arg) in internal_fn.args().iter().enumerate() {
|
||||
if i > 0 {
|
||||
fn_docs.push_str(", ");
|
||||
}
|
||||
fn_docs.push_str(&format!("{}: {}", arg.name, arg.type_));
|
||||
}
|
||||
fn_docs.push_str(") -> ");
|
||||
fn_docs.push_str(&internal_fn.return_value().type_);
|
||||
fn_docs.push_str("\n```\n\n");
|
||||
|
||||
fn_docs.push_str("#### Arguments\n\n");
|
||||
for arg in internal_fn.args() {
|
||||
let (format, should_be_indented) = arg.get_type_string().unwrap();
|
||||
if let Some(description) = arg.description() {
|
||||
fn_docs.push_str(&format!(
|
||||
"* `{}`: `{}` - {}\n",
|
||||
arg.name, arg.type_, description
|
||||
));
|
||||
} else {
|
||||
fn_docs.push_str(&format!("* `{}`: `{}`\n", arg.name, arg.type_));
|
||||
}
|
||||
|
||||
if should_be_indented {
|
||||
fn_docs.push_str(&format!("```\n{}\n```\n", format));
|
||||
}
|
||||
}
|
||||
|
||||
fn_docs.push_str("\n#### Returns\n\n");
|
||||
let return_type = internal_fn.return_value();
|
||||
if let Some(description) = return_type.description() {
|
||||
fn_docs.push_str(&format!("* `{}` - {}\n", return_type.type_, description));
|
||||
} else {
|
||||
fn_docs.push_str(&format!("* `{}`\n", return_type.type_));
|
||||
}
|
||||
|
||||
let (format, should_be_indented) = return_type.get_type_string().unwrap();
|
||||
if should_be_indented {
|
||||
fn_docs.push_str(&format!("```\n{}\n```\n", format));
|
||||
}
|
||||
|
||||
fn_docs.push_str("\n\n\n");
|
||||
|
||||
buf.push_str(&fn_docs);
|
||||
}
|
||||
|
||||
expectorate::assert_contents("../../docs/kcl.md", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_stdlib_json_schema() {
|
||||
let stdlib = StdLib::new();
|
||||
|
||||
let mut json_data = vec![];
|
||||
|
||||
for internal_fn in &stdlib.internal_fn_names {
|
||||
json_data.push(internal_fn.to_json().unwrap());
|
||||
}
|
||||
|
||||
expectorate::assert_contents(
|
||||
"../../docs/kcl.json",
|
||||
&serde_json::to_string_pretty(&json_data).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
310
src/wasm-lib/src/std/segment.rs
Normal file
310
src/wasm-lib/src/std/segment.rs
Normal file
@ -0,0 +1,310 @@
|
||||
//! Functions related to line segments.
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{MemoryItem, SketchGroup},
|
||||
std::{utils::get_angle, Args},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
/// Returns the segment end of x.
|
||||
pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
|
||||
let result = inner_segment_end_x(&segment_name, sketch_group, args)?;
|
||||
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the segment end of x.
|
||||
#[stdlib {
|
||||
name = "segEndX",
|
||||
}]
|
||||
fn inner_segment_end_x(
|
||||
segment_name: &str,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<f64, KclError> {
|
||||
let line = sketch_group
|
||||
.get_base_by_name_or_start(segment_name)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a segment name that exists in the given SketchGroup, found `{}`",
|
||||
segment_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(line.to[0])
|
||||
}
|
||||
|
||||
/// Returns the segment end of y.
|
||||
pub fn segment_end_y(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
|
||||
let result = inner_segment_end_y(&segment_name, sketch_group, args)?;
|
||||
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the segment end of y.
|
||||
#[stdlib {
|
||||
name = "segEndY",
|
||||
}]
|
||||
fn inner_segment_end_y(
|
||||
segment_name: &str,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<f64, KclError> {
|
||||
let line = sketch_group
|
||||
.get_base_by_name_or_start(segment_name)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a segment name that exists in the given SketchGroup, found `{}`",
|
||||
segment_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(line.to[1])
|
||||
}
|
||||
|
||||
/// Returns the last segment of x.
|
||||
pub fn last_segment_x(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let sketch_group = args.get_sketch_group()?;
|
||||
let result = inner_last_segment_x(sketch_group, args)?;
|
||||
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the last segment of x.
|
||||
#[stdlib {
|
||||
name = "lastSegX",
|
||||
}]
|
||||
fn inner_last_segment_x(sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
|
||||
let last_line = sketch_group
|
||||
.value
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup with at least one segment, found `{:?}`",
|
||||
sketch_group
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base();
|
||||
|
||||
Ok(last_line.to[0])
|
||||
}
|
||||
|
||||
/// Returns the last segment of y.
|
||||
pub fn last_segment_y(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let sketch_group = args.get_sketch_group()?;
|
||||
let result = inner_last_segment_y(sketch_group, args)?;
|
||||
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the last segment of y.
|
||||
#[stdlib {
|
||||
name = "lastSegY",
|
||||
}]
|
||||
fn inner_last_segment_y(sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
|
||||
let last_line = sketch_group
|
||||
.value
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup with at least one segment, found `{:?}`",
|
||||
sketch_group
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base();
|
||||
|
||||
Ok(last_line.to[1])
|
||||
}
|
||||
|
||||
/// Returns the length of the segment.
|
||||
pub fn segment_length(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
|
||||
let result = inner_segment_length(&segment_name, sketch_group, args)?;
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the length of the segment.
|
||||
#[stdlib {
|
||||
name = "segLen",
|
||||
}]
|
||||
fn inner_segment_length(
|
||||
segment_name: &str,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<f64, KclError> {
|
||||
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a segment name that exists in the given SketchGroup, found `{}`",
|
||||
segment_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
let line = path.get_base();
|
||||
|
||||
let result = ((line.from[1] - line.to[1]).powi(2) + (line.from[0] - line.to[0]).powi(2)).sqrt();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns the angle of the segment.
|
||||
pub fn segment_angle(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
|
||||
|
||||
let result = inner_segment_angle(&segment_name, sketch_group, args)?;
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the angle of the segment.
|
||||
#[stdlib {
|
||||
name = "segAng",
|
||||
}]
|
||||
fn inner_segment_angle(
|
||||
segment_name: &str,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<f64, KclError> {
|
||||
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a segment name that exists in the given SketchGroup, found `{}`",
|
||||
segment_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
let line = path.get_base();
|
||||
|
||||
let result = get_angle(&line.from, &line.to);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns the angle to match the given length for x.
|
||||
pub fn angle_to_match_length_x(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, to, sketch_group) = args.get_segment_name_to_number_sketch_group()?;
|
||||
let result = inner_angle_to_match_length_x(&segment_name, to, sketch_group, args)?;
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the angle to match the given length for x.
|
||||
#[stdlib {
|
||||
name = "angleToMatchLengthX",
|
||||
}]
|
||||
fn inner_angle_to_match_length_x(
|
||||
segment_name: &str,
|
||||
to: f64,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<f64, KclError> {
|
||||
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a segment name that exists in the given SketchGroup, found `{}`",
|
||||
segment_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
let line = path.get_base();
|
||||
|
||||
let length = ((line.from[1] - line.to[1]).powi(2) + (line.from[0] - line.to[0]).powi(2)).sqrt();
|
||||
|
||||
let last_line = sketch_group
|
||||
.value
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup with at least one segment, found `{:?}`",
|
||||
sketch_group
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base();
|
||||
|
||||
let diff = (to - last_line.to[0]).abs();
|
||||
|
||||
let angle_r = diff / length.acos();
|
||||
|
||||
if diff > length {
|
||||
Ok(0.0)
|
||||
} else {
|
||||
Ok(angle_r * 180.0 / std::f64::consts::PI)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the angle to match the given length for y.
|
||||
pub fn angle_to_match_length_y(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, to, sketch_group) = args.get_segment_name_to_number_sketch_group()?;
|
||||
let result = inner_angle_to_match_length_y(&segment_name, to, sketch_group, args)?;
|
||||
args.make_user_val_from_f64(result)
|
||||
}
|
||||
|
||||
/// Returns the angle to match the given length for y.
|
||||
#[stdlib {
|
||||
name = "angleToMatchLengthY",
|
||||
}]
|
||||
fn inner_angle_to_match_length_y(
|
||||
segment_name: &str,
|
||||
to: f64,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<f64, KclError> {
|
||||
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a segment name that exists in the given SketchGroup, found `{}`",
|
||||
segment_name
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
let line = path.get_base();
|
||||
|
||||
let length = ((line.from[1] - line.to[1]).powi(2) + (line.from[0] - line.to[0]).powi(2)).sqrt();
|
||||
|
||||
let last_line = sketch_group
|
||||
.value
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup with at least one segment, found `{:?}`",
|
||||
sketch_group
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base();
|
||||
|
||||
let diff = (to - last_line.to[1]).abs();
|
||||
|
||||
let angle_r = diff / length.asin();
|
||||
|
||||
if diff > length {
|
||||
Ok(0.0)
|
||||
} else {
|
||||
Ok(angle_r * 180.0 / std::f64::consts::PI)
|
||||
}
|
||||
}
|
794
src/wasm-lib/src/std/sketch.rs
Normal file
794
src/wasm-lib/src/std/sketch.rs
Normal file
@ -0,0 +1,794 @@
|
||||
//! Functions related to sketching.
|
||||
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::{ModelingCmd, Point3D};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup},
|
||||
std::{
|
||||
utils::{get_x_component, get_y_component, intersection_with_parallel_line},
|
||||
Args,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Data to draw a line to a point.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum LineToData {
|
||||
/// A point with a tag.
|
||||
PointWithTag {
|
||||
/// The to point.
|
||||
to: [f64; 2],
|
||||
/// The tag.
|
||||
tag: String,
|
||||
},
|
||||
/// A point.
|
||||
Point([f64; 2]),
|
||||
}
|
||||
|
||||
/// Draw a line to a point.
|
||||
pub fn line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (LineToData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_line_to(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw a line to a point.
|
||||
#[stdlib {
|
||||
name = "lineTo",
|
||||
}]
|
||||
fn inner_line_to(
|
||||
data: LineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
let to = match data {
|
||||
LineToData::PointWithTag { to, .. } => to,
|
||||
LineToData::Point(to) => to,
|
||||
};
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let current_path = Path::ToPoint {
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to,
|
||||
name: if let LineToData::PointWithTag { tag, .. } = data {
|
||||
tag.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
geo_meta: GeoMeta {
|
||||
id,
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let mut new_sketch_group = sketch_group.clone();
|
||||
new_sketch_group.value.push(current_path);
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw a line to a point on an axis.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum AxisLineToData {
|
||||
/// A point with a tag.
|
||||
PointWithTag {
|
||||
/// The to point.
|
||||
to: f64,
|
||||
/// The tag.
|
||||
tag: String,
|
||||
},
|
||||
/// A point.
|
||||
Point(f64),
|
||||
}
|
||||
|
||||
/// Draw a line to a point on the x-axis.
|
||||
pub fn x_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AxisLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_x_line_to(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw a line to a point on the x-axis.
|
||||
#[stdlib {
|
||||
name = "xLineTo",
|
||||
}]
|
||||
fn inner_x_line_to(
|
||||
data: AxisLineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
|
||||
let line_to_data = match data {
|
||||
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag {
|
||||
to: [to, from.y],
|
||||
tag,
|
||||
},
|
||||
AxisLineToData::Point(data) => LineToData::Point([data, from.y]),
|
||||
};
|
||||
|
||||
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Draw a line to a point on the y-axis.
|
||||
pub fn y_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AxisLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_y_line_to(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw a line to a point on the y-axis.
|
||||
#[stdlib {
|
||||
name = "yLineTo",
|
||||
}]
|
||||
fn inner_y_line_to(
|
||||
data: AxisLineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
|
||||
let line_to_data = match data {
|
||||
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag {
|
||||
to: [from.x, to],
|
||||
tag,
|
||||
},
|
||||
AxisLineToData::Point(data) => LineToData::Point([from.x, data]),
|
||||
};
|
||||
|
||||
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw a line.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum LineData {
|
||||
/// A point with a tag.
|
||||
PointWithTag {
|
||||
/// The to point.
|
||||
to: PointOrDefault,
|
||||
/// The tag.
|
||||
tag: String,
|
||||
},
|
||||
/// A point.
|
||||
Point([f64; 2]),
|
||||
/// A string like `default`.
|
||||
Default(String),
|
||||
}
|
||||
|
||||
/// A point or a default value.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum PointOrDefault {
|
||||
/// A point.
|
||||
Point([f64; 2]),
|
||||
/// A string like `default`.
|
||||
Default(String),
|
||||
}
|
||||
|
||||
impl PointOrDefault {
|
||||
fn get_point_with_default(&self, default: [f64; 2]) -> [f64; 2] {
|
||||
match self {
|
||||
PointOrDefault::Point(point) => *point,
|
||||
PointOrDefault::Default(_) => default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a line.
|
||||
pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (LineData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_line(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw a line.
|
||||
#[stdlib {
|
||||
name = "line",
|
||||
}]
|
||||
fn inner_line(
|
||||
data: LineData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
|
||||
let default = [0.2, 1.0];
|
||||
let inner_args = match &data {
|
||||
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
|
||||
LineData::Point(to) => *to,
|
||||
LineData::Default(_) => default,
|
||||
};
|
||||
|
||||
let to = [from.x + inner_args[0], from.y + inner_args[1]];
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
segment: kittycad::types::PathSegment::Line {
|
||||
end: Point3D {
|
||||
x: to[0],
|
||||
y: to[1],
|
||||
z: 0.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
)?;
|
||||
|
||||
let current_path = Path::ToPoint {
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to,
|
||||
name: if let LineData::PointWithTag { tag, .. } = data {
|
||||
tag.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
geo_meta: GeoMeta {
|
||||
id,
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let mut new_sketch_group = sketch_group.clone();
|
||||
new_sketch_group.value.push(current_path);
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw a line on an axis.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum AxisLineData {
|
||||
/// The length with a tag.
|
||||
LengthWithTag {
|
||||
/// The length of the line.
|
||||
length: f64,
|
||||
/// The tag.
|
||||
tag: String,
|
||||
},
|
||||
/// The length.
|
||||
Length(f64),
|
||||
}
|
||||
|
||||
/// Draw a line on the x-axis.
|
||||
pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AxisLineData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_x_line(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw a line on the x-axis.
|
||||
#[stdlib {
|
||||
name = "xLine",
|
||||
}]
|
||||
fn inner_x_line(
|
||||
data: AxisLineData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let line_data = match data {
|
||||
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
|
||||
to: PointOrDefault::Point([length, 0.0]),
|
||||
tag,
|
||||
},
|
||||
AxisLineData::Length(length) => LineData::Point([length, 0.0]),
|
||||
};
|
||||
|
||||
let new_sketch_group = inner_line(line_data, sketch_group, args)?;
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Draw a line on the y-axis.
|
||||
pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AxisLineData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_y_line(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw a line on the y-axis.
|
||||
#[stdlib {
|
||||
name = "yLine",
|
||||
}]
|
||||
fn inner_y_line(
|
||||
data: AxisLineData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let line_data = match data {
|
||||
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
|
||||
to: PointOrDefault::Point([0.0, length]),
|
||||
tag,
|
||||
},
|
||||
AxisLineData::Length(length) => LineData::Point([0.0, length]),
|
||||
};
|
||||
|
||||
let new_sketch_group = inner_line(line_data, sketch_group, args)?;
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw an angled line.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum AngledLineData {
|
||||
/// An angle and length with a tag.
|
||||
AngleWithTag {
|
||||
/// The angle of the line.
|
||||
angle: f64,
|
||||
/// The length of the line.
|
||||
length: f64,
|
||||
/// The tag.
|
||||
tag: String,
|
||||
},
|
||||
/// An angle and length.
|
||||
AngleAndLength([f64; 2]),
|
||||
}
|
||||
|
||||
/// Draw an angled line.
|
||||
pub fn angled_line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AngledLineData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_angled_line(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an angled line.
|
||||
#[stdlib {
|
||||
name = "angledLine",
|
||||
}]
|
||||
fn inner_angled_line(
|
||||
data: AngledLineData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
let (angle, length) = match &data {
|
||||
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
|
||||
AngledLineData::AngleAndLength(angle_and_length) => {
|
||||
(angle_and_length[0], angle_and_length[1])
|
||||
}
|
||||
};
|
||||
let to: [f64; 2] = [
|
||||
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
|
||||
from.y + length * f64::sin(angle * std::f64::consts::PI / 180.0),
|
||||
];
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
let current_path = Path::ToPoint {
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to,
|
||||
name: if let AngledLineData::AngleWithTag { tag, .. } = data {
|
||||
tag.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
geo_meta: GeoMeta {
|
||||
id,
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let mut new_sketch_group = sketch_group.clone();
|
||||
new_sketch_group.value.push(current_path);
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Draw an angled line of a given x length.
|
||||
pub fn angled_line_of_x_length(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AngledLineData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_angled_line_of_x_length(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an angled line of a given x length.
|
||||
#[stdlib {
|
||||
name = "angledLineOfXLength",
|
||||
}]
|
||||
fn inner_angled_line_of_x_length(
|
||||
data: AngledLineData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let (angle, length) = match &data {
|
||||
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
|
||||
AngledLineData::AngleAndLength(angle_and_length) => {
|
||||
(angle_and_length[0], angle_and_length[1])
|
||||
}
|
||||
};
|
||||
|
||||
let to = get_y_component(angle, length);
|
||||
|
||||
let new_sketch_group = inner_line(
|
||||
if let AngledLineData::AngleWithTag { tag, .. } = data {
|
||||
LineData::PointWithTag {
|
||||
to: PointOrDefault::Point(to),
|
||||
tag,
|
||||
}
|
||||
} else {
|
||||
LineData::Point(to)
|
||||
},
|
||||
sketch_group,
|
||||
args,
|
||||
)?;
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw an angled line to a point.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum AngledLineToData {
|
||||
/// An angle and point with a tag.
|
||||
AngleWithTag {
|
||||
/// The angle of the line.
|
||||
angle: f64,
|
||||
/// The point to draw to.
|
||||
to: f64,
|
||||
/// The tag.
|
||||
tag: String,
|
||||
},
|
||||
/// An angle and point to draw to.
|
||||
AngleAndPoint([f64; 2]),
|
||||
}
|
||||
|
||||
/// Draw an angled line to a given x coordinate.
|
||||
pub fn angled_line_to_x(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AngledLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_angled_line_to_x(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an angled line to a given x coordinate.
|
||||
#[stdlib {
|
||||
name = "angledLineToX",
|
||||
}]
|
||||
fn inner_angled_line_to_x(
|
||||
data: AngledLineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
let (angle, x_to) = match &data {
|
||||
AngledLineToData::AngleWithTag { angle, to, .. } => (*angle, *to),
|
||||
AngledLineToData::AngleAndPoint(angle_and_to) => (angle_and_to[0], angle_and_to[1]),
|
||||
};
|
||||
|
||||
let x_component = x_to - from.x;
|
||||
let y_component = x_component * f64::tan(angle * std::f64::consts::PI / 180.0);
|
||||
let y_to = from.y + y_component;
|
||||
|
||||
let new_sketch_group = inner_line_to(
|
||||
if let AngledLineToData::AngleWithTag { tag, .. } = data {
|
||||
LineToData::PointWithTag {
|
||||
to: [x_to, y_to],
|
||||
tag,
|
||||
}
|
||||
} else {
|
||||
LineToData::Point([x_to, y_to])
|
||||
},
|
||||
sketch_group,
|
||||
args,
|
||||
)?;
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Draw an angled line of a given y length.
|
||||
pub fn angled_line_of_y_length(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AngledLineData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_angled_line_of_y_length(data, sketch_group, args)?;
|
||||
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an angled line of a given y length.
|
||||
#[stdlib {
|
||||
name = "angledLineOfYLength",
|
||||
}]
|
||||
fn inner_angled_line_of_y_length(
|
||||
data: AngledLineData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let (angle, length) = match &data {
|
||||
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
|
||||
AngledLineData::AngleAndLength(angle_and_length) => {
|
||||
(angle_and_length[0], angle_and_length[1])
|
||||
}
|
||||
};
|
||||
|
||||
let to = get_x_component(angle, length);
|
||||
|
||||
let new_sketch_group = inner_line(
|
||||
if let AngledLineData::AngleWithTag { tag, .. } = data {
|
||||
LineData::PointWithTag {
|
||||
to: PointOrDefault::Point(to),
|
||||
tag,
|
||||
}
|
||||
} else {
|
||||
LineData::Point(to)
|
||||
},
|
||||
sketch_group,
|
||||
args,
|
||||
)?;
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Draw an angled line to a given y coordinate.
|
||||
pub fn angled_line_to_y(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AngledLineToData, SketchGroup) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_angled_line_to_y(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an angled line to a given y coordinate.
|
||||
#[stdlib {
|
||||
name = "angledLineToY",
|
||||
}]
|
||||
fn inner_angled_line_to_y(
|
||||
data: AngledLineToData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
let (angle, y_to) = match &data {
|
||||
AngledLineToData::AngleWithTag { angle, to, .. } => (*angle, *to),
|
||||
AngledLineToData::AngleAndPoint(angle_and_to) => (angle_and_to[0], angle_and_to[1]),
|
||||
};
|
||||
|
||||
let y_component = y_to - from.y;
|
||||
let x_component = y_component / f64::tan(angle * std::f64::consts::PI / 180.0);
|
||||
let x_to = from.x + x_component;
|
||||
|
||||
let new_sketch_group = inner_line_to(
|
||||
if let AngledLineToData::AngleWithTag { tag, .. } = data {
|
||||
LineToData::PointWithTag {
|
||||
to: [x_to, y_to],
|
||||
tag,
|
||||
}
|
||||
} else {
|
||||
LineToData::Point([x_to, y_to])
|
||||
},
|
||||
sketch_group,
|
||||
args,
|
||||
)?;
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data for drawing an angled line that intersects with a given line.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
// TODO: make sure the docs on the args below are correct.
|
||||
pub struct AngeledLineThatIntersectsData {
|
||||
/// The angle of the line.
|
||||
pub angle: f64,
|
||||
/// The tag of the line to intersect with.
|
||||
pub intersect_tag: String,
|
||||
/// The offset from the intersecting line.
|
||||
pub offset: Option<f64>,
|
||||
/// The tag.
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
/// Draw an angled line that intersects with a given line.
|
||||
pub fn angled_line_that_intersects(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) =
|
||||
args.get_data_and_sketch_group()?;
|
||||
let new_sketch_group = inner_angled_line_that_intersects(data, sketch_group, args)?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an angled line that intersects with a given line.
|
||||
#[stdlib {
|
||||
name = "angledLineThatIntersects",
|
||||
}]
|
||||
fn inner_angled_line_that_intersects(
|
||||
data: AngeledLineThatIntersectsData,
|
||||
sketch_group: SketchGroup,
|
||||
args: &mut Args,
|
||||
) -> Result<SketchGroup, KclError> {
|
||||
let intersect_path = sketch_group
|
||||
.get_path_by_name(&data.intersect_tag)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a line that exists in the given SketchGroup, found `{}`",
|
||||
data.intersect_tag
|
||||
),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base();
|
||||
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
let to = intersection_with_parallel_line(
|
||||
&[intersect_path.from, intersect_path.to],
|
||||
data.offset.unwrap_or_default(),
|
||||
data.angle,
|
||||
from.into(),
|
||||
);
|
||||
|
||||
let line_to_data = if let Some(tag) = data.tag {
|
||||
LineToData::PointWithTag { to, tag }
|
||||
} else {
|
||||
LineToData::Point(to)
|
||||
};
|
||||
|
||||
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Start a sketch at a given point.
|
||||
pub fn start_sketch_at(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let data: LineData = args.get_data()?;
|
||||
|
||||
let sketch_group = inner_start_sketch_at(data, args)?;
|
||||
Ok(MemoryItem::SketchGroup(sketch_group))
|
||||
}
|
||||
|
||||
/// Start a sketch at a given point.
|
||||
#[stdlib {
|
||||
name = "startSketchAt",
|
||||
}]
|
||||
fn inner_start_sketch_at(data: LineData, args: &mut Args) -> Result<SketchGroup, KclError> {
|
||||
let default = [0.0, 0.0];
|
||||
let to = match &data {
|
||||
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
|
||||
LineData::Point(to) => *to,
|
||||
LineData::Default(_) => default,
|
||||
};
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let path_id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(path_id, ModelingCmd::StartPath {})?;
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::MovePathPen {
|
||||
path: path_id,
|
||||
to: Point3D {
|
||||
x: to[0],
|
||||
y: to[1],
|
||||
z: 0.0,
|
||||
},
|
||||
},
|
||||
)?;
|
||||
|
||||
let current_path = BasePath {
|
||||
from: to,
|
||||
to,
|
||||
name: if let LineData::PointWithTag { tag, .. } = data {
|
||||
tag.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
geo_meta: GeoMeta {
|
||||
id,
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
};
|
||||
|
||||
let sketch_group = SketchGroup {
|
||||
id: path_id,
|
||||
position: Position([0.0, 0.0, 0.0]),
|
||||
rotation: Rotation([0.0, 0.0, 0.0, 1.0]),
|
||||
value: vec![],
|
||||
start: current_path,
|
||||
meta: vec![args.source_range.into()],
|
||||
};
|
||||
Ok(sketch_group)
|
||||
}
|
||||
|
||||
/// Close the current sketch.
|
||||
pub fn close(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
let sketch_group = args.get_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_close(sketch_group, args)?;
|
||||
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Close the current sketch.
|
||||
#[stdlib {
|
||||
name = "close",
|
||||
}]
|
||||
fn inner_close(sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
|
||||
let from = sketch_group.get_coords_from_paths()?;
|
||||
let to: Point2d = sketch_group.start.from.into();
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ClosePath {
|
||||
path_id: sketch_group.id,
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut new_sketch_group = sketch_group.clone();
|
||||
new_sketch_group.value.push(Path::ToPoint {
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to: to.into(),
|
||||
// TODO: should we use a different name?
|
||||
name: "".into(),
|
||||
geo_meta: GeoMeta {
|
||||
id,
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::std::sketch::{LineData, PointOrDefault};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_line_data() {
|
||||
let mut str_json = "\"default\"".to_string();
|
||||
let data: LineData = serde_json::from_str(&str_json).unwrap();
|
||||
assert_eq!(data, LineData::Default("default".to_string()));
|
||||
|
||||
let data = LineData::Point([0.0, 1.0]);
|
||||
str_json = serde_json::to_string(&data).unwrap();
|
||||
assert_eq!(str_json, "[0.0,1.0]");
|
||||
|
||||
str_json = "[0, 1]".to_string();
|
||||
let data: LineData = serde_json::from_str(&str_json).unwrap();
|
||||
assert_eq!(data, LineData::Point([0.0, 1.0]));
|
||||
|
||||
str_json = "{ \"to\": [0.0, 1.0], \"tag\": \"thing\" }".to_string();
|
||||
let data: LineData = serde_json::from_str(&str_json).unwrap();
|
||||
assert_eq!(
|
||||
data,
|
||||
LineData::PointWithTag {
|
||||
to: PointOrDefault::Point([0.0, 1.0]),
|
||||
tag: "thing".to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
248
src/wasm-lib/src/std/utils.rs
Normal file
248
src/wasm-lib/src/std/utils.rs
Normal file
@ -0,0 +1,248 @@
|
||||
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 {
|
||||
let x = b[0] - a[0];
|
||||
let y = b[1] - a[1];
|
||||
normalise_angle(y.atan2(x) * 180.0 / std::f64::consts::PI)
|
||||
}
|
||||
|
||||
pub fn normalise_angle(angle: f64) -> f64 {
|
||||
let result = ((angle % 360.0) + 360.0) % 360.0;
|
||||
if result > 180.0 {
|
||||
result - 360.0
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn clockwise_sign(points: &[[f64; 2]]) -> i32 {
|
||||
let mut sum = 0.0;
|
||||
for i in 0..points.len() {
|
||||
let current_point = points[i];
|
||||
let next_point = points[(i + 1) % points.len()];
|
||||
sum += (next_point[0] - current_point[0]) * (next_point[1] + current_point[1]);
|
||||
}
|
||||
if sum >= 0.0 {
|
||||
1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn normalize_rad(angle: f64) -> f64 {
|
||||
let draft = angle % (2.0 * std::f64::consts::PI);
|
||||
if draft < 0.0 {
|
||||
draft + 2.0 * std::f64::consts::PI
|
||||
} else {
|
||||
draft
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
|
||||
///
|
||||
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(delta_angle(std::f64::consts::PI/8.0, std::f64::consts::PI/4.0), std::f64::consts::PI/8.0);
|
||||
/// ```
|
||||
#[allow(dead_code)]
|
||||
pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
|
||||
let norm_from_angle = normalize_rad(from_angle);
|
||||
let norm_to_angle = normalize_rad(to_angle);
|
||||
let provisional = norm_to_angle - norm_from_angle;
|
||||
|
||||
if provisional > -std::f64::consts::PI && provisional <= std::f64::consts::PI {
|
||||
return provisional;
|
||||
}
|
||||
if provisional > std::f64::consts::PI {
|
||||
return provisional - 2.0 * std::f64::consts::PI;
|
||||
}
|
||||
if provisional < -std::f64::consts::PI {
|
||||
return provisional + 2.0 * std::f64::consts::PI;
|
||||
}
|
||||
0.0
|
||||
}
|
||||
|
||||
/// Calculates the distance between two points.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(distance_between_points(&[0.0, 0.0], &[0.0, 5.0]), 5.0);
|
||||
/// assert_eq!(distance_between_points(&[0.0, 0.0], &[3.0, 4.0]), 5.0);
|
||||
/// ```
|
||||
#[allow(dead_code)]
|
||||
pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
|
||||
let x1 = point_a[0];
|
||||
let y1 = point_a[1];
|
||||
let x2 = point_b[0];
|
||||
let y2 = point_b[1];
|
||||
|
||||
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
|
||||
}
|
||||
|
||||
pub fn calculate_intersection_of_two_lines(
|
||||
line1: &[[f64; 2]; 2],
|
||||
line2_angle: f64,
|
||||
line2_point: [f64; 2],
|
||||
) -> [f64; 2] {
|
||||
let line2_point_b = [
|
||||
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
|
||||
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
|
||||
];
|
||||
intersect(line1[0], line1[1], line2_point, line2_point_b)
|
||||
}
|
||||
|
||||
pub fn intersect(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], p4: [f64; 2]) -> [f64; 2] {
|
||||
let slope = |p1: [f64; 2], p2: [f64; 2]| (p1[1] - p2[1]) / (p1[0] - p2[0]);
|
||||
let constant = |p1: [f64; 2], p2: [f64; 2]| p1[1] - slope(p1, p2) * p1[0];
|
||||
let get_y = |for_x: f64, p1: [f64; 2], p2: [f64; 2]| slope(p1, p2) * for_x + constant(p1, p2);
|
||||
|
||||
if p1[0] == p2[0] {
|
||||
return [p1[0], get_y(p1[0], p3, p4)];
|
||||
}
|
||||
if p3[0] == p4[0] {
|
||||
return [p3[0], get_y(p3[0], p1, p2)];
|
||||
}
|
||||
|
||||
let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
|
||||
let y = get_y(x, p1, p2);
|
||||
[x, y]
|
||||
}
|
||||
|
||||
pub fn intersection_with_parallel_line(
|
||||
line1: &[[f64; 2]; 2],
|
||||
line1_offset: f64,
|
||||
line2_angle: f64,
|
||||
line2_point: [f64; 2],
|
||||
) -> [f64; 2] {
|
||||
calculate_intersection_of_two_lines(
|
||||
&offset_line(line1_offset, line1[0], line1[1]),
|
||||
line2_angle,
|
||||
line2_point,
|
||||
)
|
||||
}
|
||||
|
||||
fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] {
|
||||
if p1[0] == p2[0] {
|
||||
let direction = (p1[1] - p2[1]).signum();
|
||||
return [
|
||||
[p1[0] + offset * direction, p1[1]],
|
||||
[p2[0] + offset * direction, p2[1]],
|
||||
];
|
||||
}
|
||||
if p1[1] == p2[1] {
|
||||
let direction = (p2[0] - p1[0]).signum();
|
||||
return [
|
||||
[p1[0], p1[1] + offset * direction],
|
||||
[p2[0], p2[1] + offset * direction],
|
||||
];
|
||||
}
|
||||
let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0]));
|
||||
[[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]]
|
||||
}
|
||||
|
||||
pub fn get_y_component(angle_degree: f64, x_component: f64) -> [f64; 2] {
|
||||
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
|
||||
let y_component = x_component * f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
|
||||
let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
[sign * x_component, sign * y_component]
|
||||
}
|
||||
|
||||
pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
|
||||
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
|
||||
let x_component = y_component / f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
|
||||
let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
[sign * x_component, sign * y_component]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Here you can bring your functions into scope
|
||||
use super::{get_x_component, get_y_component};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
static EACH_QUAD: [(i32, [i32; 2]); 12] = [
|
||||
(-315, [1, 1]),
|
||||
(-225, [-1, 1]),
|
||||
(-135, [-1, -1]),
|
||||
(-45, [1, -1]),
|
||||
(45, [1, 1]),
|
||||
(135, [-1, 1]),
|
||||
(225, [-1, -1]),
|
||||
(315, [1, -1]),
|
||||
(405, [1, 1]),
|
||||
(495, [-1, 1]),
|
||||
(585, [-1, -1]),
|
||||
(675, [1, -1]),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_get_y_component() {
|
||||
let mut expected = Vec::new();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &(angle, expected_result) in EACH_QUAD.iter() {
|
||||
let res = get_y_component(angle as f64, 1.0);
|
||||
results.push([res[0].round() as i32, res[1].round() as i32]);
|
||||
expected.push(expected_result);
|
||||
}
|
||||
|
||||
assert_eq!(results, expected);
|
||||
|
||||
let result = get_y_component(0.0, 1.0);
|
||||
assert_eq!(result[0] as i32, 1);
|
||||
assert_eq!(result[1] as i32, 0);
|
||||
|
||||
let result = get_y_component(90.0, 1.0);
|
||||
assert_eq!(result[0] as i32, 1);
|
||||
assert!(result[1] > 100000.0);
|
||||
|
||||
let result = get_y_component(180.0, 1.0);
|
||||
assert_eq!(result[0] as i32, -1);
|
||||
assert!((result[1] - 0.0).abs() < f64::EPSILON);
|
||||
|
||||
let result = get_y_component(270.0, 1.0);
|
||||
assert_eq!(result[0] as i32, -1);
|
||||
assert!(result[1] < -100000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_x_component() {
|
||||
let mut expected = Vec::new();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &(angle, expected_result) in EACH_QUAD.iter() {
|
||||
let res = get_x_component(angle as f64, 1.0);
|
||||
results.push([res[0].round() as i32, res[1].round() as i32]);
|
||||
expected.push(expected_result);
|
||||
}
|
||||
|
||||
assert_eq!(results, expected);
|
||||
|
||||
let result = get_x_component(0.0, 1.0);
|
||||
assert!(result[0] > 100000.0);
|
||||
assert_eq!(result[1] as i32, 1);
|
||||
|
||||
let result = get_x_component(90.0, 1.0);
|
||||
assert!((result[0] - 0.0).abs() < f64::EPSILON);
|
||||
assert_eq!(result[1] as i32, 1);
|
||||
|
||||
let result = get_x_component(180.0, 1.0);
|
||||
assert!(result[0] < -100000.0);
|
||||
assert_eq!(result[1] as i32, 1);
|
||||
|
||||
let result = get_x_component(270.0, 1.0);
|
||||
assert!((result[0] - 0.0).abs() < f64::EPSILON);
|
||||
assert_eq!(result[1] as i32, -1);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -30,6 +31,18 @@ pub struct Token {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl From<Token> for crate::executor::SourceRange {
|
||||
fn from(token: Token) -> Self {
|
||||
Self([token.start, token.end])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Token> for crate::executor::SourceRange {
|
||||
fn from(token: &Token) -> Self {
|
||||
Self([token.start, token.end])
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref NUMBER: Regex = Regex::new(r"^-?\d+(\.\d+)?").unwrap();
|
||||
static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
|
||||
@ -265,7 +278,7 @@ pub fn lexer(str: &str) -> Vec<Token> {
|
||||
#[wasm_bindgen]
|
||||
pub fn lexer_js(str: &str) -> Result<JsValue, JsError> {
|
||||
let tokens = lexer(str);
|
||||
Ok(serde_wasm_bindgen::to_value(&tokens)?)
|
||||
Ok(JsValue::from_serde(&tokens)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"types": ["vite/client", "@types/wicg-file-system-access"],
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
|
@ -1747,10 +1747,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||
|
||||
"@kittycad/lib@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.29.tgz#e0c0751fc124dd0136f9731c6bb962cd8b8202f6"
|
||||
integrity sha512-YpyXyOfoUBItJk71AP8M9i4QGvNnNHiSO35dMDx2EsDKqEGwTLDHOBniwPEq+iJqrGcZf2CfBSOvAOnZCH790A==
|
||||
"@kittycad/lib@^0.0.34":
|
||||
version "0.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.34.tgz#c1f1021f6c77bd9f47caa685cfbff0ef358a0316"
|
||||
integrity sha512-9pUUuspJB/rayW4adfF7UqRYLw1pugBy3t0+V6qK3sWttG9flgv54fPw3JKewn7VFoEjRtNtoREMAoWb4ZrUIw==
|
||||
dependencies:
|
||||
node-fetch "3.3.2"
|
||||
openapi-types "^12.0.0"
|
||||
|
Reference in New Issue
Block a user