Merge branch 'main' into franknoirot/xstate-toolbar
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
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_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_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
11
.github/workflows/cargo-clippy.yml
vendored
11
.github/workflows/cargo-clippy.yml
vendored
@ -40,6 +40,17 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
ffmpeg \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libclang-dev \
|
||||
libswscale-dev \
|
||||
--no-install-recommends
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
|
10
.github/workflows/cargo-test.yml
vendored
10
.github/workflows/cargo-test.yml
vendored
@ -41,6 +41,16 @@ jobs:
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
ffmpeg \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libclang-dev \
|
||||
libswscale-dev \
|
||||
--no-install-recommends
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |-
|
||||
|
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@ -153,6 +153,8 @@ jobs:
|
||||
needs: [build-test-web, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||
PUB_DATE: ${{ github.event.release.created_at }}
|
||||
NOTES: ${{ github.event.release.body }}
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
@ -166,6 +168,8 @@ jobs:
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
||||
--arg linux_sig "$LINUX_SIG" \
|
||||
@ -174,6 +178,8 @@ jobs:
|
||||
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"darwin-x86_64": {
|
||||
"signature": $darwin_sig,
|
||||
@ -195,6 +201,34 @@ jobs:
|
||||
}' > last_update.json
|
||||
cat last_update.json
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
},
|
||||
"appimage-x86_64": {
|
||||
"url": $linux_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_url
|
||||
}
|
||||
}
|
||||
}' > last_download.json
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v1.1.1'
|
||||
with:
|
||||
@ -219,6 +253,12 @@ jobs:
|
||||
path: last_update.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload release files to Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
@ -11173,22 +11173,13 @@
|
||||
},
|
||||
"to": {
|
||||
"description": "The to point.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A point.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -11201,10 +11192,6 @@
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -15341,22 +15328,13 @@
|
||||
},
|
||||
"to": {
|
||||
"description": "The to point.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A point.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -15369,10 +15347,6 @@
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
12
docs/kcl.md
12
docs/kcl.md
@ -2044,11 +2044,9 @@ line(data: LineData, sketch_group: SketchGroup) -> SketchGroup
|
||||
// The tag.
|
||||
tag: string,
|
||||
// The to point.
|
||||
to: [number] |
|
||||
string,
|
||||
to: [number],
|
||||
} |
|
||||
[number] |
|
||||
string
|
||||
[number]
|
||||
```
|
||||
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
|
||||
```
|
||||
@ -2784,11 +2782,9 @@ startSketchAt(data: LineData) -> SketchGroup
|
||||
// The tag.
|
||||
tag: string,
|
||||
// The to point.
|
||||
to: [number] |
|
||||
string,
|
||||
to: [number],
|
||||
} |
|
||||
[number] |
|
||||
string
|
||||
[number]
|
||||
```
|
||||
|
||||
#### Returns
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
|
@ -19,7 +19,7 @@ anyhow = "1"
|
||||
oauth2 = "4.4.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
||||
tauri = { version = "1.3.0", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater"] }
|
||||
tokio = { version = "1.29.1", features = ["time"] }
|
||||
toml = "0.6.0"
|
||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.5.0"
|
||||
"version": "0.6.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
53
src/App.tsx
53
src/App.tsx
@ -42,7 +42,9 @@ export function App() {
|
||||
setOpenPanes,
|
||||
didDragInStream,
|
||||
streamDimensions,
|
||||
guiMode,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setCode: s.setCode,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
@ -109,8 +111,41 @@ export function App() {
|
||||
})
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
|
||||
if (buttonDownInStream !== undefined) {
|
||||
if (buttonDownInStream === undefined) {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'mouse_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type
|
||||
|
||||
@ -123,6 +158,7 @@ export function App() {
|
||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||
interaction = 'zoom'
|
||||
} else {
|
||||
console.log('none')
|
||||
return
|
||||
}
|
||||
|
||||
@ -135,15 +171,6 @@ export function App() {
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,11 +198,11 @@ export function App() {
|
||||
paneOpacity
|
||||
}
|
||||
defaultSize={{
|
||||
width: '400px',
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={600}
|
||||
maxWidth={800}
|
||||
minHeight={'auto'}
|
||||
maxHeight={'auto'}
|
||||
handleClasses={{
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useStore, toolTips } from './useStore'
|
||||
import { useStore, toolTips, Selections } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
@ -15,6 +15,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import styles from './Toolbar.module.css'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useAppMode } from 'hooks/useAppMode'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
@ -24,6 +26,7 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
updateAst,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
@ -31,7 +34,9 @@ export const Toolbar = () => {
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
programMemory: s.programMemory,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
}))
|
||||
useAppMode()
|
||||
|
||||
useEffect(() => {
|
||||
console.log('guiMode', guiMode)
|
||||
@ -39,7 +44,7 @@ export const Toolbar = () => {
|
||||
|
||||
function ToolbarButtons() {
|
||||
return (
|
||||
<>
|
||||
<span className="overflow-x-auto">
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -71,9 +76,18 @@ export const Toolbar = () => {
|
||||
SketchOnFace
|
||||
</button>
|
||||
)}
|
||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('guiMode.pathId', guiMode.pathId)
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: guiMode.pathId,
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
@ -125,14 +139,23 @@ export const Toolbar = () => {
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
setGuiMode({ mode: 'default' })
|
||||
}}
|
||||
>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||
)
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
@ -143,7 +166,18 @@ export const Toolbar = () => {
|
||||
return (
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool:
|
||||
guiMode.sketchMode === sketchFnName
|
||||
? 'select'
|
||||
: (sketchFnName as any),
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
@ -153,10 +187,11 @@ export const Toolbar = () => {
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
waitingFirstClick: true,
|
||||
isTooltip: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||
@ -180,7 +215,7 @@ export const Toolbar = () => {
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
</>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -43,7 +44,8 @@ export const AppHeader = ({
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
)}
|
||||
|
@ -144,7 +144,7 @@ export function useCalc({
|
||||
try {
|
||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||
const ast = parser_wasm(code)
|
||||
const _programMem: any = { root: {} }
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
|
@ -10,7 +10,7 @@ describe('processMemory', () => {
|
||||
// Enable rotations #152
|
||||
const code = `
|
||||
const myVar = 5
|
||||
const myFn = (a) => {
|
||||
fn myFn = (a) => {
|
||||
return a - 2
|
||||
}
|
||||
const otherVar = myFn(5)
|
||||
@ -29,6 +29,7 @@ describe('processMemory', () => {
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {},
|
||||
return: null,
|
||||
})
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
|
@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { useStore } from '../useStore'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory } from '../lang/executor'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
@ -49,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
Object.keys(programMemory.root).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
|
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar'
|
||||
import {
|
||||
NETWORK_CONTENT,
|
||||
NetworkHealthIndicator,
|
||||
} from './NetworkHealthIndicator'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('NetworkHealthIndicator tests', () => {
|
||||
test('Renders the network indicator', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||
NETWORK_CONTENT.good
|
||||
)
|
||||
})
|
||||
|
||||
test('Responds to network changes', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||
NETWORK_CONTENT.bad
|
||||
)
|
||||
})
|
||||
})
|
112
src/components/NetworkHealthIndicator.tsx
Normal file
112
src/components/NetworkHealthIndicator.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
faCheck,
|
||||
faExclamation,
|
||||
faWifi,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
|
||||
export const NETWORK_CONTENT = {
|
||||
good: 'Network health is good',
|
||||
bad: 'Network issue',
|
||||
}
|
||||
|
||||
const NETWORK_MESSAGES = {
|
||||
offline: 'You are offline',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||
const hasIssues = [...networkIssues.values()].length > 0
|
||||
|
||||
useEffect(() => {
|
||||
const offlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [
|
||||
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||
NETWORK_MESSAGES.offline,
|
||||
]
|
||||
})
|
||||
window.addEventListener('offline', offlineListener)
|
||||
|
||||
const onlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||
})
|
||||
window.addEventListener('online', onlineListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineListener)
|
||||
window.removeEventListener('online', onlineListener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className={
|
||||
'p-0 border-none relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
}
|
||||
data-testid="network-toggle"
|
||||
>
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
}
|
||||
bgClassName={
|
||||
hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
||||
}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||
{!hasIssues ? (
|
||||
<span
|
||||
className="flex items-center justify-center gap-1 px-4"
|
||||
data-testid="network-good"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faCheck}
|
||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||
/>
|
||||
{NETWORK_CONTENT.good}
|
||||
</span>
|
||||
) : (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
<span
|
||||
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
|
||||
data-testid="network-bad"
|
||||
>
|
||||
{NETWORK_CONTENT.bad}
|
||||
{networkIssues.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{networkIssues.map((issue) => (
|
||||
<li
|
||||
key={issue}
|
||||
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faExclamation}
|
||||
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||
className="ml-4"
|
||||
/>
|
||||
<p className="flex-1 mr-4">{issue}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@ -7,11 +7,14 @@ import {
|
||||
} from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import { addNewSketchLn } from 'lang/std/sketch'
|
||||
|
||||
export const Stream = ({ className = '' }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -25,6 +28,11 @@ export const Stream = ({ className = '' }) => {
|
||||
setDidDragInStream,
|
||||
streamDimensions,
|
||||
isExecuting,
|
||||
guiMode,
|
||||
ast,
|
||||
updateAst,
|
||||
setGuiMode,
|
||||
programMemory,
|
||||
} = useStore((s) => ({
|
||||
mediaStream: s.mediaStream,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
@ -34,6 +42,11 @@ export const Stream = ({ className = '' }) => {
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
isExecuting: s.isExecuting,
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
setGuiMode: s.setGuiMode,
|
||||
programMemory: s.programMemory,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
@ -64,7 +77,7 @@ export const Stream = ({ className = '' }) => {
|
||||
const newId = uuidv4()
|
||||
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type
|
||||
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||
|
||||
if (
|
||||
interactionGuards.pan.callback(e) ||
|
||||
@ -81,19 +94,33 @@ export const Stream = ({ className = '' }) => {
|
||||
interactionGuards.zoom.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'zoom'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_start',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
)
|
||||
) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
}
|
||||
|
||||
setButtonDownInStream(e.button)
|
||||
setClickCoords({ x, y })
|
||||
@ -118,6 +145,7 @@ export const Stream = ({ className = '' }) => {
|
||||
ctrlKey,
|
||||
}) => {
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -128,7 +156,7 @@ export const Stream = ({ className = '' }) => {
|
||||
const newCmdId = uuidv4()
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
const command: Models['WebSocketRequest_type'] = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_end',
|
||||
@ -136,9 +164,8 @@ export const Stream = ({ className = '' }) => {
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
setButtonDownInStream(undefined)
|
||||
if (!didDragInStream) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -150,6 +177,95 @@ export const Stream = ({ className = '' }) => {
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
}
|
||||
|
||||
if (!didDragInStream && guiMode.mode === 'default') {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
(!didDragInStream &&
|
||||
guiMode.mode === 'sketch' &&
|
||||
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||
(guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any))
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'mouse_click',
|
||||
window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('move' as any)
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'handle_mouse_drag_end',
|
||||
window: { x, y },
|
||||
}
|
||||
}
|
||||
engineCommandManager?.sendSceneCommand(command).then(async ({ data }) => {
|
||||
if (command.cmd.type !== 'mouse_click' || !ast) return
|
||||
if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any as 'line')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _addStartSketch = addStartSketch(
|
||||
ast,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
pathToNode: _pathToNode,
|
||||
waitingFirstClick: false,
|
||||
})
|
||||
updateAst(_modifiedAst)
|
||||
} else if (
|
||||
data?.data?.entities_modified?.length &&
|
||||
!guiMode.waitingFirstClick
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _modifiedAst = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [coords[1].x, coords[1].y],
|
||||
fnName: 'line',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
}).modifiedAst
|
||||
updateAst(_modifiedAst)
|
||||
}
|
||||
})
|
||||
setDidDragInStream(false)
|
||||
setClickCoords(undefined)
|
||||
}
|
||||
|
@ -63,7 +63,6 @@ export const TextEditor = ({
|
||||
sourceRangeMap,
|
||||
} = useStore((s) => ({
|
||||
code: s.code,
|
||||
defferedCode: s.defferedCode,
|
||||
defferedSetCode: s.defferedSetCode,
|
||||
editorView: s.editorView,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
@ -71,7 +70,6 @@ export const TextEditor = ({
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setCode: s.setCode,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
|
243
src/hooks/useAppMode.ts
Normal file
243
src/hooks/useAppMode.ts
Normal file
@ -0,0 +1,243 @@
|
||||
// needed somewhere to dump this logic,
|
||||
// Once we have xState this should be removed
|
||||
|
||||
import { useStore, Selections } from 'useStore'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
interface DefaultPlanes {
|
||||
xy: string
|
||||
yz: string
|
||||
xz: string
|
||||
}
|
||||
|
||||
export function useAppMode() {
|
||||
const {
|
||||
guiMode,
|
||||
setGuiMode,
|
||||
selectionRanges,
|
||||
engineCommandManager,
|
||||
selectionRangeTypeMap,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
}))
|
||||
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
|
||||
useEffect(() => {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === 'selectFace' &&
|
||||
engineCommandManager
|
||||
) {
|
||||
if (!defaultPlanes) {
|
||||
const xy = createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||
})
|
||||
const yz = createPlane(engineCommandManager, {
|
||||
x_axis: { x: 0, y: 1, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||
})
|
||||
const xz = createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||
})
|
||||
setDefaultPlanes({ xy, yz, xz })
|
||||
} else {
|
||||
hideDefaultPlanes(engineCommandManager, defaultPlanes)
|
||||
}
|
||||
}
|
||||
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else if (guiMode.mode === 'default') {
|
||||
const pathId =
|
||||
engineCommandManager &&
|
||||
isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
if (pathId) {
|
||||
setGuiMode({
|
||||
mode: 'canEditSketch',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
pathId,
|
||||
})
|
||||
}
|
||||
} else if (guiMode.mode === 'canEditSketch') {
|
||||
if (
|
||||
!engineCommandManager ||
|
||||
!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
) {
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
guiMode,
|
||||
guiMode.mode,
|
||||
engineCommandManager,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const unSub = engineCommandManager?.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: async ({ data }) => {
|
||||
if (!data.entity_id) return
|
||||
if (!defaultPlanes) return
|
||||
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
|
||||
// user clicked something else in the scene
|
||||
return
|
||||
}
|
||||
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
|
||||
{
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: data.entity_id,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
}
|
||||
)
|
||||
hideDefaultPlanes(engineCommandManager, defaultPlanes)
|
||||
const sketchUuid = uuidv4()
|
||||
const proms: any[] = []
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
})
|
||||
)
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
})
|
||||
)
|
||||
const res = await Promise.all(proms)
|
||||
console.log('res', res)
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
})
|
||||
|
||||
console.log('sketchModeResponse', sketchModeResponse)
|
||||
},
|
||||
})
|
||||
return unSub
|
||||
}, [engineCommandManager, defaultPlanes])
|
||||
}
|
||||
|
||||
function createPlane(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
{
|
||||
x_axis,
|
||||
y_axis,
|
||||
color,
|
||||
}: {
|
||||
x_axis: Models['Point3d_type']
|
||||
y_axis: Models['Point3d_type']
|
||||
color: Models['Color_type']
|
||||
}
|
||||
) {
|
||||
const planeId = uuidv4()
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 60,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
clobber: false,
|
||||
},
|
||||
cmd_id: planeId,
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
return planeId
|
||||
}
|
||||
|
||||
function hideDefaultPlanes(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
defaultPlanes: DefaultPlanes
|
||||
) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function isCursorInSketchCommandRange(
|
||||
artifactMap: ArtifactMap,
|
||||
selectionRanges: Selections
|
||||
): string | false {
|
||||
const overlapingEntries = Object.entries(artifactMap || {}).filter(
|
||||
([id, artifact]) =>
|
||||
selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection.range) &&
|
||||
Array.isArray(artifact.range) &&
|
||||
isOverlap(selection.range, artifact.range) &&
|
||||
(artifact.commandType === 'start_path' ||
|
||||
artifact.commandType === 'extend_path' ||
|
||||
'close_path')
|
||||
)
|
||||
)
|
||||
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId
|
||||
? overlapingEntries[0][1].parentId
|
||||
: false
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { KCLUnexpectedError } from './errors'
|
||||
import { KCLError } from './errors'
|
||||
import { initPromise } from './rust'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
@ -1744,6 +1744,12 @@ describe('parsing errors', () => {
|
||||
_theError = e
|
||||
}
|
||||
const theError = _theError as any
|
||||
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
|
||||
expect(theError).toEqual(
|
||||
new KCLError(
|
||||
'unexpected',
|
||||
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
|
||||
[[29, 30]]
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -21,7 +21,7 @@ show(mySketch001)`
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -77,7 +77,7 @@ show(mySketch001)`
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
|
||||
)
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
|
||||
__meta: [{ sourceRange: [13, 34] }],
|
||||
},
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { ProgramMemory } from './executor'
|
||||
import { ProgramMemory, SketchGroup } from './executor'
|
||||
import { initPromise } from './rust'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { vi } from 'vitest'
|
||||
@ -117,10 +117,10 @@ show(mySketch)
|
||||
// ].join('\n')
|
||||
// const { root } = await exe(code)
|
||||
// expect(root.mySk1.value).toHaveLength(3)
|
||||
// expect(root?.rotated?.type).toBe('sketchGroup')
|
||||
// expect(root?.rotated?.type).toBe('SketchGroup')
|
||||
// if (
|
||||
// root?.mySk1?.type !== 'sketchGroup' ||
|
||||
// root?.rotated?.type !== 'sketchGroup'
|
||||
// root?.mySk1?.type !== 'SketchGroup' ||
|
||||
// root?.rotated?.type !== 'SketchGroup'
|
||||
// )
|
||||
// throw new Error('not a sketch group')
|
||||
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
|
||||
@ -143,7 +143,7 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.mySk1).toEqual({
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -199,7 +199,7 @@ show(mySketch)
|
||||
// TODO path to node is probably wrong here, zero indexes are not correct
|
||||
expect(root).toEqual({
|
||||
three: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
@ -208,7 +208,7 @@ show(mySketch)
|
||||
],
|
||||
},
|
||||
yo: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
@ -225,7 +225,7 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.yo).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
{
|
||||
@ -240,7 +240,7 @@ show(mySketch)
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
@ -338,7 +338,7 @@ describe('testing math operators', () => {
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
// result of `-legLen(5, min(3, 999))` should be -4
|
||||
const yVal = sketch.value?.[0]?.to?.[1]
|
||||
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
|
||||
expect(yVal).toBe(-4)
|
||||
})
|
||||
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
||||
@ -356,8 +356,8 @@ describe('testing math operators', () => {
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
|
||||
expect(sketch.value?.[1]?.from).toEqual([3, 4])
|
||||
expect(sketch.value?.[1]?.to).toEqual([6, 0])
|
||||
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
|
||||
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
|
||||
const removedUnaryExp = code.replace(
|
||||
`-legLen(segLen('seg01', %), myVar)`,
|
||||
`legLen(segLen('seg01', %), myVar)`
|
||||
@ -366,7 +366,9 @@ describe('testing math operators', () => {
|
||||
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
|
||||
|
||||
// without the minus sign, the y value should be 8
|
||||
expect(removedUnaryExpRootSketch.value?.[1]?.to).toEqual([6, 8])
|
||||
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
|
||||
6, 8,
|
||||
])
|
||||
})
|
||||
it('with nested callExpression and binaryExpression', async () => {
|
||||
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
||||
@ -397,7 +399,10 @@ show(theExtrude)`
|
||||
|
||||
// helpers
|
||||
|
||||
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
|
||||
async function exe(
|
||||
code: string,
|
||||
programMemory: ProgramMemory = { root: {}, return: null }
|
||||
) {
|
||||
const ast = parser_wasm(code)
|
||||
|
||||
const result = await enginelessExecutor(ast, programMemory)
|
||||
|
@ -5,96 +5,21 @@ import {
|
||||
SourceRangeMap,
|
||||
} from './std/engineConnection'
|
||||
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { rangeTypeFix } from './abstractSyntaxTree'
|
||||
|
||||
export type SourceRange = [number, number]
|
||||
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
|
||||
export type Metadata = {
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
export type Position = [number, number, number]
|
||||
export type Rotation = [number, number, number, number]
|
||||
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
|
||||
export type { Position } from '../wasm-lib/kcl/bindings/Position'
|
||||
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
|
||||
export type { Path } from '../wasm-lib/kcl/bindings/Path'
|
||||
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
|
||||
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||
|
||||
interface BasePath {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
name?: string
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
export interface ToPoint extends BasePath {
|
||||
type: 'toPoint'
|
||||
}
|
||||
|
||||
export interface Base extends BasePath {
|
||||
type: 'base'
|
||||
}
|
||||
|
||||
export interface HorizontalLineTo extends BasePath {
|
||||
type: 'horizontalLineTo'
|
||||
x: number
|
||||
}
|
||||
|
||||
export interface AngledLineTo extends BasePath {
|
||||
type: 'angledLineTo'
|
||||
angle: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
interface GeoMeta {
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
|
||||
|
||||
export interface SketchGroup {
|
||||
type: 'sketchGroup'
|
||||
id: string
|
||||
value: Path[]
|
||||
start?: Base
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
interface ExtrudePlane {
|
||||
type: 'extrudePlane'
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type ExtrudeSurface = GeoMeta &
|
||||
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
|
||||
|
||||
export interface ExtrudeGroup {
|
||||
type: 'extrudeGroup'
|
||||
id: string
|
||||
value: ExtrudeSurface[]
|
||||
height: number
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
/** UserVal not produced by one of our internal functions */
|
||||
export interface UserVal {
|
||||
type: 'userVal'
|
||||
value: any
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
|
||||
export type PathToNode = [string | number, string][]
|
||||
|
||||
interface Memory {
|
||||
[key: string]: MemoryItem
|
||||
@ -102,12 +27,12 @@ interface Memory {
|
||||
|
||||
export interface ProgramMemory {
|
||||
root: Memory
|
||||
return?: ProgramReturn
|
||||
return: ProgramReturn | null
|
||||
}
|
||||
|
||||
export const executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager,
|
||||
// work around while the gemotry is still be stored on the frontend
|
||||
// will be removed when the stream UI is added.
|
||||
@ -132,7 +57,7 @@ export const executor = async (
|
||||
|
||||
export const _executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager
|
||||
): Promise<ProgramMemory> => {
|
||||
try {
|
||||
|
@ -176,7 +176,7 @@ show(part001)`
|
||||
})
|
||||
|
||||
describe('Testing moveValueIntoNewVariable', () => {
|
||||
const fn = (fnName: string) => `const ${fnName} = (x) => {
|
||||
const fn = (fnName: string) => `fn ${fnName} = (x) => {
|
||||
return x
|
||||
}
|
||||
`
|
||||
|
@ -27,6 +27,48 @@ import {
|
||||
getFirstArg,
|
||||
createFirstArg,
|
||||
} from './std/sketch'
|
||||
import { isLiteralArrayOrStatic } from './std/sketchcombos'
|
||||
|
||||
export function addStartSketch(
|
||||
node: Program,
|
||||
start: [number, number],
|
||||
end: [number, number]
|
||||
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
||||
const _node = { ...node }
|
||||
const _name = findUniqueName(node, 'part')
|
||||
|
||||
const startSketchAt = createCallExpression('startSketchAt', [
|
||||
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
|
||||
])
|
||||
const initialLineTo = createCallExpression('line', [
|
||||
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
const pipeBody = [startSketchAt, initialLineTo]
|
||||
|
||||
const variableDeclaration = createVariableDeclaration(
|
||||
_name,
|
||||
createPipeExpression(pipeBody)
|
||||
)
|
||||
|
||||
const newIndex = node.body.length
|
||||
_node.body = [...node.body, variableDeclaration]
|
||||
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[newIndex.toString(10), 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
id: _name,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
export function addSketchTo(
|
||||
node: Program,
|
||||
@ -151,7 +193,7 @@ export function mutateArrExp(
|
||||
): boolean {
|
||||
if (node.type === 'ArrayExpression') {
|
||||
node.elements.forEach((element, i) => {
|
||||
if (element.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(element)) {
|
||||
node.elements[i] = updateWith.elements[i]
|
||||
}
|
||||
})
|
||||
@ -169,8 +211,8 @@ export function mutateObjExpProp(
|
||||
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
|
||||
if (keyIndex !== -1) {
|
||||
if (
|
||||
updateWith.type === 'Literal' &&
|
||||
node.properties[keyIndex].value.type === 'Literal'
|
||||
isLiteralArrayOrStatic(updateWith) &&
|
||||
isLiteralArrayOrStatic(node.properties[keyIndex].value)
|
||||
) {
|
||||
node.properties[keyIndex].value = updateWith
|
||||
return true
|
||||
@ -180,7 +222,7 @@ export function mutateObjExpProp(
|
||||
) {
|
||||
const arrExp = node.properties[keyIndex].value as ArrayExpression
|
||||
arrExp.elements.forEach((element, i) => {
|
||||
if (element.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(element)) {
|
||||
arrExp.elements[i] = updateWith.elements[i]
|
||||
}
|
||||
})
|
||||
|
@ -224,7 +224,7 @@ const key = 'c'
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('comments in a fn block', () => {
|
||||
const code = `const myFn = () => {
|
||||
const code = `fn myFn = () => {
|
||||
// this is a comment
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
|
||||
|
@ -6,6 +6,8 @@ import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as Sentry from '@sentry/react'
|
||||
|
||||
let lastMessage = ''
|
||||
|
||||
interface CommandInfo {
|
||||
commandType: CommandTypes
|
||||
range: SourceRange
|
||||
@ -754,6 +756,13 @@ export class EngineCommandManager {
|
||||
})
|
||||
}
|
||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||
if (
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
) {
|
||||
console.log('sending command', command.cmd.type)
|
||||
lastMessage = command.cmd.type
|
||||
}
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('socket not ready')
|
||||
return Promise.resolve()
|
||||
@ -761,7 +770,8 @@ export class EngineCommandManager {
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||
const cmd = command.cmd
|
||||
if (
|
||||
cmd.type === 'camera_drag_move' &&
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
cmd.type === 'handle_mouse_drag_move') &&
|
||||
this.engineConnection?.unreliableDataChannel
|
||||
) {
|
||||
cmd.sequence = this.outSequence
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
SketchGroup,
|
||||
SourceRange,
|
||||
PathToNode,
|
||||
MemoryItem,
|
||||
} from '../executor'
|
||||
import {
|
||||
Program,
|
||||
@ -19,8 +20,9 @@ import {
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||
@ -185,7 +187,7 @@ export const line: SketchLineHelper = {
|
||||
createCallback,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression>(
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
@ -197,12 +199,12 @@ export const line: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newXVal, newYVal],
|
||||
@ -220,7 +222,11 @@ export const line: SketchLineHelper = {
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
@ -238,22 +244,10 @@ export const line: SketchLineHelper = {
|
||||
createLiteral(roundOff(to[1] - from[1], 2)),
|
||||
])
|
||||
|
||||
if (
|
||||
callExpression.arguments?.[0].type === 'Literal' &&
|
||||
callExpression.arguments?.[0].value === 'default'
|
||||
) {
|
||||
callExpression.arguments[0] = toArrExp
|
||||
} else if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
const toProp = callExpression.arguments?.[0].properties?.find(
|
||||
({ key }) => key.name === 'to'
|
||||
)
|
||||
if (
|
||||
toProp &&
|
||||
toProp.value.type === 'Literal' &&
|
||||
toProp.value.value === 'default'
|
||||
) {
|
||||
toProp.value = toArrExp
|
||||
}
|
||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||
} else {
|
||||
mutateArrExp(callExpression.arguments?.[0], toArrExp)
|
||||
@ -301,7 +295,7 @@ export const xLineTo: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newX = createLiteral(roundOff(to[0], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
|
||||
@ -349,7 +343,7 @@ export const yLineTo: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newY = createLiteral(roundOff(to[1], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
|
||||
@ -399,7 +393,7 @@ export const xLine: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newX = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
|
||||
@ -443,7 +437,7 @@ export const yLine: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newY = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
|
||||
@ -546,7 +540,7 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
|
||||
const newLine = createCallback
|
||||
@ -619,7 +613,7 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
|
||||
@ -876,7 +870,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
|
||||
const intersectPath = sketchGroup.value.find(
|
||||
({ name }) => name === intersectTagName
|
||||
({ name }: Path) => name === intersectTagName
|
||||
)
|
||||
let offset = 0
|
||||
if (intersectPath) {
|
||||
@ -968,60 +962,14 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const { node: pipeExp, shallowPath: pipePath } =
|
||||
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
|
||||
const maybeStartSketchAt = pipeExp.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'startSketchAt' &&
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const maybeDefaultLine = pipeExp.body.findIndex(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'line' &&
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const defaultLinePath: PathToNode = [
|
||||
...pipePath,
|
||||
['body', ''],
|
||||
[maybeDefaultLine, ''],
|
||||
]
|
||||
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
|
||||
PipeExpression | CallExpression
|
||||
>(node, pathToNode, 'PipeExpression')
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
if (maybeStartSketchAt) {
|
||||
const startSketchAt = maybeStartSketchAt as any
|
||||
startSketchAt.arguments[0] = createArrayExpression([
|
||||
createLiteral(to[0]),
|
||||
createLiteral(to[1]),
|
||||
])
|
||||
return {
|
||||
modifiedAst: node,
|
||||
}
|
||||
}
|
||||
if (maybeDefaultLine !== -1) {
|
||||
const defaultLine = getNodeFromPath<CallExpression>(
|
||||
node,
|
||||
defaultLinePath
|
||||
).node
|
||||
const { from } = getSketchSegmentFromSourceRange(sketch, [
|
||||
defaultLine.start,
|
||||
defaultLine.end,
|
||||
]).segment
|
||||
return updateArgs({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
pathToNode: defaultLinePath,
|
||||
to,
|
||||
from,
|
||||
})
|
||||
}
|
||||
|
||||
const last = sketch.value[sketch.value.length - 1]
|
||||
const last = sketch.value[sketch.value.length - 1] || sketch.start
|
||||
const from = last.to
|
||||
|
||||
return add({
|
||||
@ -1089,10 +1037,11 @@ export function addTagForSketchOnFace(
|
||||
|
||||
function isAngleLiteral(lineArugement: Value): boolean {
|
||||
return lineArugement?.type === 'ArrayExpression'
|
||||
? lineArugement.elements[0].type === 'Literal'
|
||||
? isLiteralArrayOrStatic(lineArugement.elements[0])
|
||||
: lineArugement?.type === 'ObjectExpression'
|
||||
? lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
|
||||
.type === 'Literal'
|
||||
? isLiteralArrayOrStatic(
|
||||
lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
|
||||
)
|
||||
: false
|
||||
}
|
||||
|
||||
@ -1198,14 +1147,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
||||
} {
|
||||
// used for lineTo, line
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg.type === 'Literal' && firstArg.value === 'default') {
|
||||
return {
|
||||
val:
|
||||
callExpression.callee.name === 'startSketchAt'
|
||||
? [createLiteral(0), createLiteral(0)]
|
||||
: [createLiteral(1), createLiteral(1)],
|
||||
}
|
||||
}
|
||||
if (firstArg.type === 'ArrayExpression') {
|
||||
return { val: [firstArg.elements[0], firstArg.elements[1]] }
|
||||
}
|
||||
@ -1215,8 +1156,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
||||
if (to?.type === 'ArrayExpression') {
|
||||
const [x, y] = to.elements
|
||||
return { val: [x, y], tag }
|
||||
} else if (to?.type === 'Literal' && to.value === 'default') {
|
||||
return { val: [createLiteral(0), createLiteral(0)], tag }
|
||||
}
|
||||
}
|
||||
throw new Error('expected ArrayExpression or ObjectExpression')
|
||||
|
@ -401,6 +401,11 @@ show(part001)`
|
||||
programMemory.root['part001'] as SketchGroup,
|
||||
[index, index]
|
||||
).segment
|
||||
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
|
||||
expect(segment).toEqual({
|
||||
to: [0, 0.04],
|
||||
from: [0, 0.04],
|
||||
name: '',
|
||||
type: 'base',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
VariableDeclarator,
|
||||
CallExpression,
|
||||
} from '../abstractSyntaxTreeTypes'
|
||||
import { SketchGroup, SourceRange } from '../executor'
|
||||
import { SketchGroup, SourceRange, Path } from '../executor'
|
||||
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
sketchGroup: SketchGroup,
|
||||
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
|
||||
startSourceRange[1] >= rangeEnd &&
|
||||
sketchGroup.start
|
||||
)
|
||||
return { segment: sketchGroup.start, index: -1 }
|
||||
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
|
||||
|
||||
const lineIndex = sketchGroup.value.findIndex(
|
||||
({ __geoMeta: { sourceRange } }) =>
|
||||
({ __geoMeta: { sourceRange } }: Path) =>
|
||||
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
|
||||
)
|
||||
const line = sketchGroup.value[lineIndex]
|
||||
|
@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
||||
import { PathToNode, ProgramMemory } from '../executor'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
|
||||
|
||||
type LineInputsType =
|
||||
| 'xAbsolute'
|
||||
@ -1136,27 +1137,18 @@ export function getRemoveConstraintsTransform(
|
||||
|
||||
// check if the function is locked down and so can't be transformed
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
if (Array.isArray(firstArg.val)) {
|
||||
const [a, b] = firstArg.val
|
||||
if (a?.type !== 'Literal' || b?.type !== 'Literal') {
|
||||
return transformInfo
|
||||
}
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') {
|
||||
return transformInfo
|
||||
}
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
return transformInfo
|
||||
}
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) &&
|
||||
firstArg.val?.[0]?.type === 'Literal' &&
|
||||
firstArg.val?.[1]?.type === 'Literal'
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isTwoValFree) {
|
||||
return false
|
||||
}
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isOneValFree) {
|
||||
return transformInfo
|
||||
}
|
||||
@ -1187,25 +1179,12 @@ function getTransformMapPath(
|
||||
|
||||
// check if the function is locked down and so can't be transformed
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
if (Array.isArray(firstArg.val)) {
|
||||
const [a, b] = firstArg.val
|
||||
if (a?.type !== 'Literal' && b?.type !== 'Literal') {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') {
|
||||
return false
|
||||
}
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) &&
|
||||
firstArg.val?.[0]?.type === 'Literal' &&
|
||||
firstArg.val?.[1]?.type === 'Literal'
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
||||
if (isTwoValFree || isOneValFree) {
|
||||
if (isLiteralArrayOrStatic(firstArg.val)) {
|
||||
const info = transformMap?.[name]?.free?.[constraintType]
|
||||
if (info)
|
||||
return {
|
||||
@ -1259,7 +1238,7 @@ export function getConstraintType(
|
||||
if (fnName === 'xLineTo') return 'yAbsolute'
|
||||
if (fnName === 'yLineTo') return 'xAbsolute'
|
||||
} else {
|
||||
const isFirstArgLockedDown = val?.[0]?.type !== 'Literal'
|
||||
const isFirstArgLockedDown = isNotLiteralArrayOrStatic(val[0])
|
||||
if (fnName === 'line')
|
||||
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
|
||||
if (fnName === 'lineTo')
|
||||
@ -1452,7 +1431,7 @@ export function transformAstSketchLines({
|
||||
|
||||
const varName = varDec.id.name
|
||||
const sketchGroup = programMemory.root?.[varName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
|
||||
throw new Error('not a sketch group')
|
||||
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
|
||||
const referencedSegment = referencedSegmentRange
|
||||
@ -1538,23 +1517,46 @@ export function getConstraintLevelFromSourceRange(
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
|
||||
// check if the function is fully constrained
|
||||
if (Array.isArray(firstArg.val)) {
|
||||
const [a, b] = firstArg.val
|
||||
if (a?.type !== 'Literal' && b?.type !== 'Literal') return 'full'
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') return 'full'
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
return 'full'
|
||||
}
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) &&
|
||||
firstArg.val?.[0]?.type === 'Literal' &&
|
||||
firstArg.val?.[1]?.type === 'Literal'
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
|
||||
if (isTwoValFree) return 'free'
|
||||
if (isOneValFree) return 'partial'
|
||||
|
||||
return 'partial'
|
||||
}
|
||||
|
||||
export function isLiteralArrayOrStatic(
|
||||
val: Value | [Value, Value] | [Value, Value, Value] | undefined
|
||||
): boolean {
|
||||
if (!val) return false
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
const [a, b] = val
|
||||
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
|
||||
}
|
||||
return (
|
||||
val.type === 'Literal' ||
|
||||
(val.type === 'UnaryExpression' && val.argument.type === 'Literal')
|
||||
)
|
||||
}
|
||||
|
||||
export function isNotLiteralArrayOrStatic(
|
||||
val: Value | [Value, Value] | [Value, Value, Value]
|
||||
): boolean {
|
||||
if (Array.isArray(val)) {
|
||||
const [a, b] = val
|
||||
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
|
||||
}
|
||||
return (
|
||||
(val.type !== 'Literal' && val.type !== 'UnaryExpression') ||
|
||||
(val.type === 'UnaryExpression' && val.argument.type !== 'Literal')
|
||||
)
|
||||
}
|
||||
|
@ -131,10 +131,12 @@ const yi=45`
|
||||
})
|
||||
it('test negative and decimal numbers', () => {
|
||||
expect(stringSummaryLexer('-1')).toEqual([
|
||||
"number '-1' from 0 to 2",
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1' from 1 to 2",
|
||||
])
|
||||
expect(stringSummaryLexer('-1.5')).toEqual([
|
||||
"number '-1.5' from 0 to 4",
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1.5' from 1 to 4",
|
||||
])
|
||||
expect(stringSummaryLexer('1.5')).toEqual([
|
||||
"number '1.5' from 0 to 3",
|
||||
@ -158,10 +160,12 @@ const yi=45`
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"operator '+' from 4 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"number '-2.5' from 6 to 10",
|
||||
"operator '-' from 6 to 7",
|
||||
"number '2.5' from 7 to 10",
|
||||
])
|
||||
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
|
||||
"number '-1.5' from 0 to 4",
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1.5' from 1 to 4",
|
||||
"whitespace ' ' from 4 to 5",
|
||||
"operator '+' from 5 to 6",
|
||||
"whitespace ' ' from 6 to 7",
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
readDir,
|
||||
writeTextFile,
|
||||
} from '@tauri-apps/api/fs'
|
||||
import { documentDir } from '@tauri-apps/api/path'
|
||||
import { documentDir, homeDir } from '@tauri-apps/api/path'
|
||||
import { isTauri } from './isTauri'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||
@ -32,7 +32,13 @@ export async function initializeProjectDirectory(directory: string) {
|
||||
return directory
|
||||
}
|
||||
|
||||
const docDirectory = await documentDir()
|
||||
let docDirectory: string
|
||||
try {
|
||||
docDirectory = await documentDir()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
docDirectory = await homeDir() // seems to work better on Linux
|
||||
}
|
||||
|
||||
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
||||
|
||||
|
@ -49,7 +49,7 @@ class MockEngineCommandManager {
|
||||
|
||||
export async function enginelessExecutor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {} }
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
): Promise<ProgramMemory> {
|
||||
const mockEngineCommandManager = new MockEngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
@ -64,7 +64,7 @@ export async function enginelessExecutor(
|
||||
|
||||
export async function executor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {} }
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
): Promise<ProgramMemory> {
|
||||
const engineCommandManager = new EngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
|
@ -54,9 +54,12 @@ export type TooTip =
|
||||
| 'yLineTo'
|
||||
| 'angledLineThatIntersects'
|
||||
|
||||
export const toolTips: TooTip[] = [
|
||||
'lineTo',
|
||||
export const toolTips = [
|
||||
'sketch_line',
|
||||
'move',
|
||||
// original tooltips
|
||||
'line',
|
||||
'lineTo',
|
||||
'angledLine',
|
||||
'angledLineOfXLength',
|
||||
'angledLineOfYLength',
|
||||
@ -67,7 +70,7 @@ export const toolTips: TooTip[] = [
|
||||
'xLineTo',
|
||||
'yLineTo',
|
||||
'angledLineThatIntersects',
|
||||
]
|
||||
] as any as TooTip[]
|
||||
|
||||
export type GuiModes =
|
||||
| {
|
||||
@ -77,6 +80,7 @@ export type GuiModes =
|
||||
mode: 'sketch'
|
||||
sketchMode: TooTip
|
||||
isTooltip: true
|
||||
waitingFirstClick: boolean
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
id?: string
|
||||
@ -95,6 +99,7 @@ export type GuiModes =
|
||||
}
|
||||
| {
|
||||
mode: 'canEditSketch'
|
||||
pathId: string
|
||||
pathToNode: PathToNode
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
@ -133,8 +138,8 @@ export interface StoreState {
|
||||
kclErrors: KCLError[]
|
||||
addKCLError: (err: KCLError) => void
|
||||
resetKCLErrors: () => void
|
||||
ast: Program | null
|
||||
setAst: (ast: Program | null) => void
|
||||
ast: Program
|
||||
setAst: (ast: Program) => void
|
||||
updateAst: (
|
||||
ast: Program,
|
||||
optionalParams?: {
|
||||
@ -233,12 +238,13 @@ export const useStore = create<StoreState>()(
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
})
|
||||
ranges.length &&
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
})
|
||||
})
|
||||
},
|
||||
setCursor2: (codeSelections) => {
|
||||
@ -292,7 +298,15 @@ export const useStore = create<StoreState>()(
|
||||
resetKCLErrors: () => {
|
||||
set({ kclErrors: [] })
|
||||
},
|
||||
ast: null,
|
||||
ast: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
body: [],
|
||||
nonCodeMeta: {
|
||||
noneCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
},
|
||||
setAst: (ast) => {
|
||||
set({ ast })
|
||||
},
|
||||
@ -301,7 +315,11 @@ export const useStore = create<StoreState>()(
|
||||
const astWithUpdatedSource = parser_wasm(newCode)
|
||||
callBack(astWithUpdatedSource)
|
||||
|
||||
set({ ast: astWithUpdatedSource, code: newCode })
|
||||
set({
|
||||
ast: astWithUpdatedSource,
|
||||
code: newCode,
|
||||
defferedCode: newCode,
|
||||
})
|
||||
if (focusPath) {
|
||||
const { node } = getNodeFromPath<any>(
|
||||
astWithUpdatedSource,
|
||||
@ -353,7 +371,7 @@ export const useStore = create<StoreState>()(
|
||||
setError: (error = '') => {
|
||||
set({ errorState: { isError: !!error, error } })
|
||||
},
|
||||
programMemory: { root: {}, pendingMemory: {} },
|
||||
programMemory: { root: {}, return: null },
|
||||
setProgramMemory: (programMemory) => set({ programMemory }),
|
||||
isShiftDown: false,
|
||||
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
|
||||
|
698
src/wasm-lib/Cargo.lock
generated
698
src/wasm-lib/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -12,10 +12,19 @@ bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
|
||||
serde_json = "1.0.93"
|
||||
serde_json = "1.0.106"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-futures = "0.4.37"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
image = "0.24.7"
|
||||
kittycad = "0.2.25"
|
||||
reqwest = { version = "0.11.20", default-features = false }
|
||||
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.6.1"
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures = "0.3.28"
|
||||
js-sys = "0.3.64"
|
||||
|
@ -16,7 +16,7 @@ proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.29", features = ["full"] }
|
||||
syn = { version = "2.0.32", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
expectorate = "1.0.7"
|
||||
|
@ -19,7 +19,7 @@ parse-display = "0.8.2"
|
||||
regex = "1.7.1"
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
serde = {version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.93"
|
||||
serde_json = "1.0.106"
|
||||
thiserror = "1.0.47"
|
||||
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
|
@ -12,7 +12,7 @@ use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, R
|
||||
use crate::{
|
||||
engine::EngineConnection,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange},
|
||||
executor::{MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, UserVal},
|
||||
parser::PIPE_OPERATOR,
|
||||
};
|
||||
|
||||
@ -449,6 +449,7 @@ pub enum BinaryPart {
|
||||
BinaryExpression(Box<BinaryExpression>),
|
||||
CallExpression(Box<CallExpression>),
|
||||
UnaryExpression(Box<UnaryExpression>),
|
||||
MemberExpression(Box<MemberExpression>),
|
||||
}
|
||||
|
||||
impl From<BinaryPart> for crate::executor::SourceRange {
|
||||
@ -471,6 +472,7 @@ impl BinaryPart {
|
||||
BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.recast(options, indentation_level, false),
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
|
||||
BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,6 +483,7 @@ impl BinaryPart {
|
||||
BinaryPart::BinaryExpression(binary_expression) => binary_expression.start(),
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.start(),
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.start(),
|
||||
BinaryPart::MemberExpression(member_expression) => member_expression.start(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,6 +494,7 @@ impl BinaryPart {
|
||||
BinaryPart::BinaryExpression(binary_expression) => binary_expression.end(),
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.end(),
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.end(),
|
||||
BinaryPart::MemberExpression(member_expression) => member_expression.end(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,6 +527,7 @@ impl BinaryPart {
|
||||
source_ranges: vec![unary_expression.into()],
|
||||
}))
|
||||
}
|
||||
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(memory),
|
||||
}
|
||||
}
|
||||
|
||||
@ -536,6 +541,9 @@ impl BinaryPart {
|
||||
}
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code),
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
|
||||
BinaryPart::MemberExpression(member_expression) => {
|
||||
member_expression.get_hover_value_for_position(pos, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -553,6 +561,9 @@ impl BinaryPart {
|
||||
BinaryPart::UnaryExpression(ref mut unary_expression) => {
|
||||
unary_expression.rename_identifiers(old_name, new_name)
|
||||
}
|
||||
BinaryPart::MemberExpression(ref mut member_expression) => {
|
||||
member_expression.rename_identifiers(old_name, new_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -751,12 +762,7 @@ impl CallExpression {
|
||||
})
|
||||
})?
|
||||
.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::MemberExpression(member_expression) => member_expression.get_result(memory)?,
|
||||
Value::FunctionExpression(function_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
|
||||
@ -1082,23 +1088,23 @@ impl Literal {
|
||||
|
||||
impl From<Literal> for MemoryItem {
|
||||
fn from(literal: Literal) -> Self {
|
||||
MemoryItem::UserVal {
|
||||
MemoryItem::UserVal(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 {
|
||||
MemoryItem::UserVal(UserVal {
|
||||
value: literal.value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: literal.into(),
|
||||
}],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1227,12 +1233,7 @@ impl ArrayExpression {
|
||||
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::MemberExpression(member_expression) => member_expression.get_result(memory)?,
|
||||
Value::FunctionExpression(function_expression) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
|
||||
@ -1245,12 +1246,12 @@ impl ArrayExpression {
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(UserVal {
|
||||
value: results.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Rename all identifiers that have the old name to the new given name.
|
||||
@ -1370,12 +1371,12 @@ impl ObjectExpression {
|
||||
object.insert(property.key.name.clone(), result.get_json_value()?);
|
||||
}
|
||||
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(UserVal {
|
||||
value: object.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Rename all identifiers that have the old name to the new given name.
|
||||
@ -1554,6 +1555,38 @@ impl MemberExpression {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_result_array(&self, memory: &mut ProgramMemory, index: usize) -> Result<MemoryItem, KclError> {
|
||||
let array = 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::Array(array) = array {
|
||||
if let Some(value) = array.get(index) {
|
||||
Ok(MemoryItem::UserVal(UserVal {
|
||||
value: value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("index {} not found in array", index),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("MemberExpression array is not an array: {:?}", array),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_result(&self, memory: &mut ProgramMemory) -> Result<MemoryItem, KclError> {
|
||||
let property_name = match &self.property {
|
||||
LiteralIdentifier::Identifier(identifier) => identifier.name.to_string(),
|
||||
@ -1562,9 +1595,12 @@ impl MemberExpression {
|
||||
// Parse this as a string.
|
||||
if let serde_json::Value::String(string) = value {
|
||||
string
|
||||
} else if let serde_json::Value::Number(_) = &value {
|
||||
// It can also be a number if we are getting a member of an array.
|
||||
return self.get_result_array(memory, parse_json_number_as_usize(&value, self.into())?);
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Expected string literal for property name, found {:?}", value),
|
||||
message: format!("Expected string literal or number for property name, found {:?}", value),
|
||||
source_ranges: vec![literal.into()],
|
||||
}));
|
||||
}
|
||||
@ -1582,12 +1618,12 @@ impl MemberExpression {
|
||||
|
||||
if let serde_json::Value::Object(map) = object {
|
||||
if let Some(value) = map.get(&property_name) {
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(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),
|
||||
@ -1715,12 +1751,12 @@ impl BinaryExpression {
|
||||
parse_json_value_as_string(&right_json_value),
|
||||
) {
|
||||
let value = serde_json::Value::String(format!("{}{}", left, right));
|
||||
return Ok(MemoryItem::UserVal {
|
||||
return Ok(MemoryItem::UserVal(UserVal {
|
||||
value,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1735,12 +1771,12 @@ impl BinaryExpression {
|
||||
BinaryOperator::Mod => (left % right).into(),
|
||||
};
|
||||
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(UserVal {
|
||||
value,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Rename all identifiers that have the old name to the new given name.
|
||||
@ -1766,6 +1802,22 @@ pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_number_as_usize(j: &serde_json::Value, source_range: SourceRange) -> Result<usize, KclError> {
|
||||
if let serde_json::Value::Number(n) = &j {
|
||||
Ok(n.as_i64().ok_or_else(|| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid index: {}", j),
|
||||
})
|
||||
})? as usize)
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid index: {}", 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())
|
||||
@ -1845,12 +1897,12 @@ impl UnaryExpression {
|
||||
.get_json_value()?,
|
||||
self.into(),
|
||||
)?;
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(UserVal {
|
||||
value: (-(num)).into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns a hover value that includes the given character position.
|
||||
@ -2231,7 +2283,7 @@ show(part001)
|
||||
|
||||
#[test]
|
||||
fn test_recast_comment_in_a_fn_block() {
|
||||
let some_program_string = r#"const myFn = () => {
|
||||
let some_program_string = r#"fn myFn = () => {
|
||||
// this is a comment
|
||||
const yo = { a: { b: { c: '123' } } } /* block
|
||||
comment */
|
||||
@ -2247,7 +2299,7 @@ show(part001)
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"const myFn = () => {
|
||||
r#"fn myFn = () => {
|
||||
// this is a comment
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
/* block
|
||||
@ -2542,4 +2594,15 @@ show(firstExtrude)
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_recast_math_start_negative() {
|
||||
let some_program_string = r#"const myVar = -5 + 6"#;
|
||||
let tokens = crate::tokeniser::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(recasted.trim(), some_program_string);
|
||||
}
|
||||
}
|
||||
|
@ -98,16 +98,14 @@ impl ProgramReturn {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum MemoryItem {
|
||||
UserVal {
|
||||
value: serde_json::Value,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
UserVal(UserVal),
|
||||
SketchGroup(SketchGroup),
|
||||
ExtrudeGroup(ExtrudeGroup),
|
||||
#[ts(skip)]
|
||||
ExtrudeTransform(ExtrudeTransform),
|
||||
#[ts(skip)]
|
||||
Function {
|
||||
#[serde(skip)]
|
||||
func: Option<MemoryFunction>,
|
||||
@ -119,7 +117,16 @@ pub enum MemoryItem {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct UserVal {
|
||||
pub value: serde_json::Value,
|
||||
#[serde(rename = "__meta")]
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct ExtrudeTransform {
|
||||
pub position: Position,
|
||||
pub rotation: Rotation,
|
||||
@ -138,7 +145,7 @@ pub type MemoryFunction = fn(
|
||||
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::UserVal(u) => u.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(),
|
||||
@ -149,8 +156,8 @@ impl From<MemoryItem> for Vec<SourceRange> {
|
||||
|
||||
impl MemoryItem {
|
||||
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
|
||||
if let MemoryItem::UserVal { value, .. } = self {
|
||||
Ok(value.clone())
|
||||
if let MemoryItem::UserVal(user_val) = self {
|
||||
Ok(user_val.value.clone())
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Not a user value: {:?}", self),
|
||||
@ -186,7 +193,7 @@ impl MemoryItem {
|
||||
/// A sketch group is a collection of paths.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct SketchGroup {
|
||||
/// The id of the sketch group.
|
||||
pub id: uuid::Uuid,
|
||||
@ -238,7 +245,7 @@ impl SketchGroup {
|
||||
/// 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")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct ExtrudeGroup {
|
||||
/// The id of the extrude group.
|
||||
pub id: uuid::Uuid,
|
||||
@ -276,15 +283,15 @@ pub enum BodyType {
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Position(pub [f64; 3]);
|
||||
pub struct Position(#[ts(type = "[number, number, number]")] pub [f64; 3]);
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Rotation(pub [f64; 4]);
|
||||
pub struct Rotation(#[ts(type = "[number, number, number, number]")] pub [f64; 4]);
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
|
||||
#[ts(export)]
|
||||
pub struct SourceRange(pub [usize; 2]);
|
||||
pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
|
||||
|
||||
impl SourceRange {
|
||||
/// Create a new source range.
|
||||
@ -401,8 +408,10 @@ impl From<SourceRange> for Metadata {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasePath {
|
||||
/// The from point.
|
||||
#[ts(type = "[number, number]")]
|
||||
pub from: [f64; 2],
|
||||
/// The to point.
|
||||
#[ts(type = "[number, number]")]
|
||||
pub to: [f64; 2],
|
||||
/// The name of the path.
|
||||
pub name: String,
|
||||
@ -804,16 +813,16 @@ show(part001)"#,
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_fn_definitions() {
|
||||
let ast = r#"const def = (x) => {
|
||||
let ast = r#"fn def = (x) => {
|
||||
return x
|
||||
}
|
||||
const ghi = (x) => {
|
||||
fn ghi = (x) => {
|
||||
return x
|
||||
}
|
||||
const jkl = (x) => {
|
||||
fn jkl = (x) => {
|
||||
return x
|
||||
}
|
||||
const hmm = (x) => {
|
||||
fn hmm = (x) => {
|
||||
return x
|
||||
}
|
||||
|
||||
@ -981,7 +990,7 @@ show(firstExtrude)"#;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_sketch() {
|
||||
let ast = r#"const box = (h, l, w) => {
|
||||
let ast = r#"fn box = (h, l, w) => {
|
||||
const myBox = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
@ -998,4 +1007,159 @@ show(fnBox)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_member_of_object_with_function_period() {
|
||||
let ast = r#"fn box = (obj) => {
|
||||
let myBox = startSketchAt(obj.start)
|
||||
|> line([0, obj.l], %)
|
||||
|> line([obj.w, 0], %)
|
||||
|> line([0, -obj.l], %)
|
||||
|> close(%)
|
||||
|> extrude(obj.h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||
|
||||
show(thisBox)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_member_of_object_with_function_brace() {
|
||||
let ast = r#"fn box = (obj) => {
|
||||
let myBox = startSketchAt(obj["start"])
|
||||
|> line([0, obj["l"]], %)
|
||||
|> line([obj["w"], 0], %)
|
||||
|> line([0, -obj["l"]], %)
|
||||
|> close(%)
|
||||
|> extrude(obj["h"], %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||
|
||||
show(thisBox)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_member_of_object_with_function_mix_period_brace() {
|
||||
let ast = r#"fn box = (obj) => {
|
||||
let myBox = startSketchAt(obj["start"])
|
||||
|> line([0, obj["l"]], %)
|
||||
|> line([obj["w"], 0], %)
|
||||
|> line([10 - obj["w"], -obj.l], %)
|
||||
|> close(%)
|
||||
|> extrude(obj["h"], %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||
|
||||
show(thisBox)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore] // ignore til we get loops
|
||||
async fn test_execute_with_function_sketch_loop_objects() {
|
||||
let ast = r#"fn box = (obj) => {
|
||||
let myBox = startSketchAt(obj.start)
|
||||
|> line([0, obj.l], %)
|
||||
|> line([obj.w, 0], %)
|
||||
|> line([0, -obj.l], %)
|
||||
|> close(%)
|
||||
|> extrude(obj.h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
|
||||
const thisBox = box(var)
|
||||
show(thisBox)
|
||||
}"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore] // ignore til we get loops
|
||||
async fn test_execute_with_function_sketch_loop_array() {
|
||||
let ast = r#"fn box = (h, l, w, start) => {
|
||||
const myBox = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
|
||||
for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
|
||||
const thisBox = box(var[0], var[1], var[2], var[3])
|
||||
show(thisBox)
|
||||
}"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_member_of_array_with_function() {
|
||||
let ast = r#"fn box = (array) => {
|
||||
let myBox = startSketchAt(array[0])
|
||||
|> line([0, array[1]], %)
|
||||
|> line([array[2], 0], %)
|
||||
|> line([0, -array[1]], %)
|
||||
|> close(%)
|
||||
|> extrude(array[3], %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const thisBox = box([[0,0], 6, 10, 3])
|
||||
|
||||
show(thisBox)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_with_functions() {
|
||||
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(5.0),
|
||||
memory.root.get("myVar").unwrap().get_json_value().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute() {
|
||||
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(7.4),
|
||||
memory.root.get("myVar").unwrap().get_json_value().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_start_negative() {
|
||||
let ast = r#"const myVar = -5 + 6"#;
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::json!(1.0),
|
||||
memory.root.get("myVar").unwrap().get_json_value().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
abstract_syntax_tree_types::{
|
||||
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, ValueMeta,
|
||||
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, MemberExpression, ValueMeta,
|
||||
},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::SourceRange,
|
||||
@ -81,6 +81,7 @@ pub enum MathExpression {
|
||||
BinaryExpression(Box<BinaryExpression>),
|
||||
ExtendedBinaryExpression(Box<ExtendedBinaryExpression>),
|
||||
ParenthesisToken(Box<ParenthesisToken>),
|
||||
MemberExpression(Box<MemberExpression>),
|
||||
}
|
||||
|
||||
impl MathExpression {
|
||||
@ -92,6 +93,7 @@ impl MathExpression {
|
||||
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
|
||||
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
|
||||
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.start(),
|
||||
MathExpression::MemberExpression(member_expression) => member_expression.start(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +105,7 @@ impl MathExpression {
|
||||
MathExpression::BinaryExpression(binary_expression) => binary_expression.end(),
|
||||
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
|
||||
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.end(),
|
||||
MathExpression::MemberExpression(member_expression) => member_expression.end(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -133,7 +136,7 @@ impl ReversePolishNotation {
|
||||
}
|
||||
|
||||
let current_token = self.parser.get_token(0)?;
|
||||
if current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword {
|
||||
if current_token.token_type == TokenType::Word {
|
||||
if let Ok(next) = self.parser.get_token(1) {
|
||||
if next.token_type == TokenType::Brace && next.value == "(" {
|
||||
let closing_brace = self.parser.find_closing_brace(1, 0, "")?;
|
||||
@ -149,6 +152,24 @@ impl ReversePolishNotation {
|
||||
);
|
||||
return rpn.parse();
|
||||
}
|
||||
if (current_token.token_type == TokenType::Word)
|
||||
&& (next.token_type == TokenType::Period
|
||||
|| (next.token_type == TokenType::Brace && next.value == "["))
|
||||
{
|
||||
// Find the end of the binary expression, ie the member expression.
|
||||
let end = self.parser.make_member_expression(0)?.last_index;
|
||||
let rpn = ReversePolishNotation::new(
|
||||
&self.parser.tokens[end + 1..],
|
||||
&self
|
||||
.previous_postfix
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(self.parser.tokens[0..end + 1].iter().cloned())
|
||||
.collect::<Vec<Token>>(),
|
||||
&self.operators,
|
||||
);
|
||||
return rpn.parse();
|
||||
}
|
||||
}
|
||||
|
||||
let rpn = ReversePolishNotation::new(
|
||||
@ -164,7 +185,6 @@ impl ReversePolishNotation {
|
||||
return rpn.parse();
|
||||
} else if current_token.token_type == TokenType::Number
|
||||
|| current_token.token_type == TokenType::Word
|
||||
|| current_token.token_type == TokenType::Keyword
|
||||
|| current_token.token_type == TokenType::String
|
||||
{
|
||||
let rpn = ReversePolishNotation::new(
|
||||
@ -180,6 +200,35 @@ impl ReversePolishNotation {
|
||||
return rpn.parse();
|
||||
} else if let Ok(binop) = BinaryOperator::from_str(current_token.value.as_str()) {
|
||||
if !self.operators.is_empty() {
|
||||
if binop == BinaryOperator::Sub {
|
||||
// We need to check if we have a "sub" and if the previous token is a word or
|
||||
// number or string, then we need to treat it as a negative number.
|
||||
// This oddity only applies to the "-" operator.
|
||||
if let Some(prevtoken) = self.previous_postfix.last() {
|
||||
if prevtoken.token_type == TokenType::Operator {
|
||||
// Get the next token and see if it is a number.
|
||||
if let Ok(nexttoken) = self.parser.get_token(1) {
|
||||
if nexttoken.token_type == TokenType::Number {
|
||||
// We have a negative number/ word or string.
|
||||
// Change the value of the token to be the negative number/ word or string.
|
||||
let mut new_token = nexttoken.clone();
|
||||
new_token.value = format!("-{}", nexttoken.value);
|
||||
let rpn = ReversePolishNotation::new(
|
||||
&self.parser.tokens[2..],
|
||||
&self
|
||||
.previous_postfix
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(vec![new_token.clone()])
|
||||
.collect::<Vec<Token>>(),
|
||||
&self.operators,
|
||||
);
|
||||
return rpn.parse();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(prevbinop) = BinaryOperator::from_str(self.operators[self.operators.len() - 1].value.as_str())
|
||||
{
|
||||
if prevbinop.precedence() >= binop.precedence() {
|
||||
@ -196,6 +245,29 @@ impl ReversePolishNotation {
|
||||
return rpn.parse();
|
||||
}
|
||||
}
|
||||
} else if self.previous_postfix.is_empty()
|
||||
&& current_token.token_type == TokenType::Operator
|
||||
&& current_token.value == "-"
|
||||
{
|
||||
if let Ok(nexttoken) = self.parser.get_token(1) {
|
||||
if nexttoken.token_type == TokenType::Number {
|
||||
// We have a negative number/ word or string.
|
||||
// Change the value of the token to be the negative number/ word or string.
|
||||
let mut new_token = nexttoken.clone();
|
||||
new_token.value = format!("-{}", nexttoken.value);
|
||||
let rpn = ReversePolishNotation::new(
|
||||
&self.parser.tokens[2..],
|
||||
&self
|
||||
.previous_postfix
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(vec![new_token.clone()])
|
||||
.collect::<Vec<Token>>(),
|
||||
&self.operators,
|
||||
);
|
||||
return rpn.parse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rpn = ReversePolishNotation::new(
|
||||
@ -299,7 +371,7 @@ impl ReversePolishNotation {
|
||||
return Err(KclError::InvalidExpression(KclErrorDetails {
|
||||
source_ranges: vec![SourceRange([a.start(), a.end()])],
|
||||
message: format!("{:?}", a),
|
||||
}))
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -338,7 +410,7 @@ impl ReversePolishNotation {
|
||||
start_extended: None,
|
||||
})));
|
||||
return self.build_tree(&reverse_polish_notation_tokens[1..], new_stack);
|
||||
} else if current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword {
|
||||
} else if current_token.token_type == TokenType::Word {
|
||||
if reverse_polish_notation_tokens.len() > 1 {
|
||||
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
|
||||
&& reverse_polish_notation_tokens[1].value == "("
|
||||
@ -350,6 +422,18 @@ impl ReversePolishNotation {
|
||||
)));
|
||||
return self.build_tree(&reverse_polish_notation_tokens[closing_brace + 1..], new_stack);
|
||||
}
|
||||
if reverse_polish_notation_tokens[1].token_type == TokenType::Period
|
||||
|| (reverse_polish_notation_tokens[1].token_type == TokenType::Brace
|
||||
&& reverse_polish_notation_tokens[1].value == "[")
|
||||
{
|
||||
let mut new_stack = stack;
|
||||
let member_expression = self.parser.make_member_expression(0)?;
|
||||
new_stack.push(MathExpression::MemberExpression(Box::new(member_expression.expression)));
|
||||
return self.build_tree(
|
||||
&reverse_polish_notation_tokens[member_expression.last_index + 1..],
|
||||
new_stack,
|
||||
);
|
||||
}
|
||||
let mut new_stack = stack;
|
||||
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
|
||||
name: current_token.value.clone(),
|
||||
@ -396,7 +480,7 @@ impl ReversePolishNotation {
|
||||
return Err(KclError::InvalidExpression(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("{:?}", a),
|
||||
}))
|
||||
}));
|
||||
}
|
||||
};
|
||||
let paran = match &stack[stack.len() - 2] {
|
||||
@ -445,7 +529,7 @@ impl ReversePolishNotation {
|
||||
return Err(KclError::InvalidExpression(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("{:?}", a),
|
||||
}))
|
||||
}));
|
||||
}
|
||||
};
|
||||
let mut new_stack = stack[0..stack.len() - 2].to_vec();
|
||||
@ -483,6 +567,10 @@ impl ReversePolishNotation {
|
||||
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
|
||||
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start),
|
||||
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start),
|
||||
MathExpression::MemberExpression(member_expression) => (
|
||||
BinaryPart::MemberExpression(member_expression.clone()),
|
||||
member_expression.start,
|
||||
),
|
||||
a => {
|
||||
return Err(KclError::InvalidExpression(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
@ -513,6 +601,10 @@ impl ReversePolishNotation {
|
||||
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
|
||||
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end),
|
||||
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end),
|
||||
MathExpression::MemberExpression(member_expression) => (
|
||||
BinaryPart::MemberExpression(member_expression.clone()),
|
||||
member_expression.end,
|
||||
),
|
||||
a => {
|
||||
return Err(KclError::InvalidExpression(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
@ -521,13 +613,7 @@ impl ReversePolishNotation {
|
||||
}
|
||||
};
|
||||
|
||||
let right_end = match right.0.clone() {
|
||||
BinaryPart::BinaryExpression(_bin_exp) => right.1,
|
||||
BinaryPart::Literal(lit) => lit.end,
|
||||
BinaryPart::Identifier(ident) => ident.end,
|
||||
BinaryPart::CallExpression(call) => call.end,
|
||||
BinaryPart::UnaryExpression(unary_exp) => unary_exp.end,
|
||||
};
|
||||
let right_end = right.0.clone().end();
|
||||
|
||||
let tree = BinaryExpression {
|
||||
operator: BinaryOperator::from_str(¤t_token.value.clone()).map_err(|err| {
|
||||
@ -562,25 +648,13 @@ impl MathParser {
|
||||
pub fn parse(&mut self) -> Result<BinaryExpression, KclError> {
|
||||
let rpn = self.rpn.parse()?;
|
||||
let tree_with_maybe_bad_top_level_start_end = self.rpn.build_tree(&rpn, vec![])?;
|
||||
let left_start = match tree_with_maybe_bad_top_level_start_end.clone().left {
|
||||
BinaryPart::BinaryExpression(bin_exp) => bin_exp.start,
|
||||
BinaryPart::Literal(lit) => lit.start,
|
||||
BinaryPart::Identifier(ident) => ident.start,
|
||||
BinaryPart::CallExpression(call) => call.start,
|
||||
BinaryPart::UnaryExpression(unary_exp) => unary_exp.start,
|
||||
};
|
||||
let left_start = tree_with_maybe_bad_top_level_start_end.clone().left.start();
|
||||
let min_start = if left_start < tree_with_maybe_bad_top_level_start_end.start {
|
||||
left_start
|
||||
} else {
|
||||
tree_with_maybe_bad_top_level_start_end.start
|
||||
};
|
||||
let right_end = match tree_with_maybe_bad_top_level_start_end.clone().right {
|
||||
BinaryPart::BinaryExpression(bin_exp) => bin_exp.end,
|
||||
BinaryPart::Literal(lit) => lit.end,
|
||||
BinaryPart::Identifier(ident) => ident.end,
|
||||
BinaryPart::CallExpression(call) => call.end,
|
||||
BinaryPart::UnaryExpression(unary_exp) => unary_exp.end,
|
||||
};
|
||||
let right_end = tree_with_maybe_bad_top_level_start_end.clone().right.end();
|
||||
let max_end = if right_end > tree_with_maybe_bad_top_level_start_end.end {
|
||||
right_end
|
||||
} else {
|
||||
@ -629,6 +703,60 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expression_add_no_spaces() {
|
||||
let tokens = crate::tokeniser::lexer("1+2");
|
||||
let mut parser = MathParser::new(&tokens);
|
||||
let result = parser.parse().unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
BinaryExpression {
|
||||
operator: BinaryOperator::Add,
|
||||
start: 0,
|
||||
end: 3,
|
||||
left: BinaryPart::Literal(Box::new(Literal {
|
||||
value: serde_json::Value::Number(serde_json::Number::from(1)),
|
||||
raw: "1".to_string(),
|
||||
start: 0,
|
||||
end: 1,
|
||||
})),
|
||||
right: BinaryPart::Literal(Box::new(Literal {
|
||||
value: serde_json::Value::Number(serde_json::Number::from(2)),
|
||||
raw: "2".to_string(),
|
||||
start: 2,
|
||||
end: 3,
|
||||
})),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expression_sub_no_spaces() {
|
||||
let tokens = crate::tokeniser::lexer("1 -2");
|
||||
let mut parser = MathParser::new(&tokens);
|
||||
let result = parser.parse().unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
BinaryExpression {
|
||||
operator: BinaryOperator::Sub,
|
||||
start: 0,
|
||||
end: 4,
|
||||
left: BinaryPart::Literal(Box::new(Literal {
|
||||
value: serde_json::Value::Number(serde_json::Number::from(1)),
|
||||
raw: "1".to_string(),
|
||||
start: 0,
|
||||
end: 1,
|
||||
})),
|
||||
right: BinaryPart::Literal(Box::new(Literal {
|
||||
value: serde_json::Value::Number(serde_json::Number::from(2)),
|
||||
raw: "2".to_string(),
|
||||
start: 3,
|
||||
end: 4,
|
||||
})),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expression_plus_followed_by_star() {
|
||||
let tokens = crate::tokeniser::lexer("1 + 2 * 3");
|
||||
|
@ -427,7 +427,7 @@ impl Parser {
|
||||
}
|
||||
let current_token = self.get_token(index)?;
|
||||
let very_next_token = self.get_token(index + 1)?;
|
||||
if (current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword)
|
||||
if (current_token.token_type == TokenType::Word)
|
||||
&& very_next_token.token_type == TokenType::Brace
|
||||
&& very_next_token.value == "("
|
||||
{
|
||||
@ -550,22 +550,41 @@ impl Parser {
|
||||
&self,
|
||||
index: usize,
|
||||
_previous_keys: Option<Vec<ObjectKeyInfo>>,
|
||||
has_opening_brace: bool,
|
||||
) -> Result<Vec<ObjectKeyInfo>, KclError> {
|
||||
let previous_keys = _previous_keys.unwrap_or(vec![]);
|
||||
let next_token = self.next_meaningful_token(index, None)?;
|
||||
let _next_token = next_token.clone();
|
||||
if _next_token.index == self.tokens.len() - 1 {
|
||||
if next_token.index == self.tokens.len() - 1 {
|
||||
return Ok(previous_keys);
|
||||
}
|
||||
let period_or_opening_bracket = match next_token.token {
|
||||
let mut has_opening_brace = match &next_token.token {
|
||||
Some(next_token_val) => {
|
||||
if next_token_val.token_type == TokenType::Brace && next_token_val.value == "]" {
|
||||
self.next_meaningful_token(next_token.index, None)?
|
||||
if next_token_val.token_type == TokenType::Brace && next_token_val.value == "[" {
|
||||
true
|
||||
} else {
|
||||
_next_token
|
||||
has_opening_brace
|
||||
}
|
||||
}
|
||||
None => _next_token,
|
||||
None => has_opening_brace,
|
||||
};
|
||||
let period_or_opening_bracket = match &next_token.token {
|
||||
Some(next_token_val) => {
|
||||
if has_opening_brace && next_token_val.token_type == TokenType::Brace && next_token_val.value == "]" {
|
||||
// We need to reset our has_opening_brace flag, since we've closed it.
|
||||
has_opening_brace = false;
|
||||
let next_next_token = self.next_meaningful_token(next_token.index, None)?;
|
||||
if let Some(next_next_token_val) = &next_next_token.token {
|
||||
if next_next_token_val.token_type == TokenType::Brace && next_next_token_val.value == "[" {
|
||||
// Set the opening brace flag again, since we've opened it again.
|
||||
has_opening_brace = true;
|
||||
}
|
||||
}
|
||||
next_next_token.clone()
|
||||
} else {
|
||||
next_token.clone()
|
||||
}
|
||||
}
|
||||
None => next_token.clone(),
|
||||
};
|
||||
if let Some(period_or_opening_bracket_token) = period_or_opening_bracket.token {
|
||||
if period_or_opening_bracket_token.token_type != TokenType::Period
|
||||
@ -573,11 +592,26 @@ impl Parser {
|
||||
{
|
||||
return Ok(previous_keys);
|
||||
}
|
||||
|
||||
// We don't care if we never opened the brace.
|
||||
if !has_opening_brace && period_or_opening_bracket_token.token_type == TokenType::Brace {
|
||||
return Ok(previous_keys);
|
||||
}
|
||||
|
||||
// Make sure its the right kind of brace, we don't care about ().
|
||||
if period_or_opening_bracket_token.token_type == TokenType::Brace
|
||||
&& period_or_opening_bracket_token.value != "["
|
||||
&& period_or_opening_bracket_token.value != "]"
|
||||
{
|
||||
return Ok(previous_keys);
|
||||
}
|
||||
|
||||
let key_token = self.next_meaningful_token(period_or_opening_bracket.index, None)?;
|
||||
let next_period_or_opening_bracket = self.next_meaningful_token(key_token.index, None)?;
|
||||
let is_braced = match next_period_or_opening_bracket.token {
|
||||
Some(next_period_or_opening_bracket_val) => {
|
||||
next_period_or_opening_bracket_val.token_type == TokenType::Brace
|
||||
has_opening_brace
|
||||
&& next_period_or_opening_bracket_val.token_type == TokenType::Brace
|
||||
&& next_period_or_opening_bracket_val.value == "]"
|
||||
}
|
||||
None => false,
|
||||
@ -588,23 +622,19 @@ impl Parser {
|
||||
key_token.index
|
||||
};
|
||||
if let Some(key_token_token) = key_token.token {
|
||||
let key = if key_token_token.token_type == TokenType::Word
|
||||
|| key_token_token.token_type == TokenType::Keyword
|
||||
{
|
||||
let key = if key_token_token.token_type == TokenType::Word {
|
||||
LiteralIdentifier::Identifier(Box::new(self.make_identifier(key_token.index)?))
|
||||
} else {
|
||||
LiteralIdentifier::Literal(Box::new(self.make_literal(key_token.index)?))
|
||||
};
|
||||
let computed = is_braced
|
||||
&& (key_token_token.token_type == TokenType::Word
|
||||
|| key_token_token.token_type == TokenType::Keyword);
|
||||
let computed = is_braced && key_token_token.token_type == TokenType::Word;
|
||||
let mut new_previous_keys = previous_keys;
|
||||
new_previous_keys.push(ObjectKeyInfo {
|
||||
key,
|
||||
index: end_index,
|
||||
computed,
|
||||
});
|
||||
self.collect_object_keys(key_token.index, Some(new_previous_keys))
|
||||
self.collect_object_keys(key_token.index, Some(new_previous_keys), has_opening_brace)
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![period_or_opening_bracket_token.clone().into()],
|
||||
@ -616,9 +646,9 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_member_expression(&self, index: usize) -> Result<MemberExpressionReturn, KclError> {
|
||||
pub fn make_member_expression(&self, index: usize) -> Result<MemberExpressionReturn, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
let mut keys_info = self.collect_object_keys(index, None)?;
|
||||
let mut keys_info = self.collect_object_keys(index, None, false)?;
|
||||
if keys_info.is_empty() {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
@ -653,6 +683,7 @@ impl Parser {
|
||||
|
||||
fn find_end_of_binary_expression(&self, index: usize) -> Result<usize, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
|
||||
if current_token.token_type == TokenType::Brace && current_token.value == "(" {
|
||||
let closing_parenthesis = self.find_closing_brace(index, 0, "")?;
|
||||
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
|
||||
@ -669,28 +700,42 @@ impl Parser {
|
||||
Ok(closing_parenthesis)
|
||||
};
|
||||
}
|
||||
if (current_token.token_type == TokenType::Keyword || current_token.token_type == TokenType::Word)
|
||||
&& self.get_token(index + 1)?.token_type == TokenType::Brace
|
||||
&& self.get_token(index + 1)?.value == "("
|
||||
{
|
||||
let closing_parenthesis = self.find_closing_brace(index + 1, 0, "")?;
|
||||
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
|
||||
return if let Some(maybe_another_operator_token) = maybe_another_operator.token {
|
||||
if maybe_another_operator_token.token_type != TokenType::Operator
|
||||
|| maybe_another_operator_token.value == PIPE_OPERATOR
|
||||
|
||||
if current_token.token_type == TokenType::Word {
|
||||
if let Ok(next_token) = self.get_token(index + 1) {
|
||||
if next_token.token_type == TokenType::Period
|
||||
|| (next_token.token_type == TokenType::Brace && next_token.value == "[")
|
||||
{
|
||||
Ok(closing_parenthesis)
|
||||
} else {
|
||||
let next_right = self.next_meaningful_token(maybe_another_operator.index, None)?;
|
||||
self.find_end_of_binary_expression(next_right.index)
|
||||
let member_expression = self.make_member_expression(index)?;
|
||||
return self.find_end_of_binary_expression(member_expression.last_index);
|
||||
}
|
||||
} else {
|
||||
Ok(closing_parenthesis)
|
||||
};
|
||||
|
||||
if next_token.token_type == TokenType::Brace && next_token.value == "(" {
|
||||
let closing_parenthesis = self.find_closing_brace(index + 1, 0, "")?;
|
||||
let maybe_another_operator = self.next_meaningful_token(closing_parenthesis, None)?;
|
||||
return if let Some(maybe_another_operator_token) = maybe_another_operator.token {
|
||||
if maybe_another_operator_token.token_type != TokenType::Operator
|
||||
|| maybe_another_operator_token.value == PIPE_OPERATOR
|
||||
{
|
||||
Ok(closing_parenthesis)
|
||||
} else {
|
||||
let next_right = self.next_meaningful_token(maybe_another_operator.index, None)?;
|
||||
self.find_end_of_binary_expression(next_right.index)
|
||||
}
|
||||
} else {
|
||||
Ok(closing_parenthesis)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_operator = self.next_meaningful_token(index, None)?;
|
||||
if let Some(maybe_operator_token) = maybe_operator.token {
|
||||
if maybe_operator_token.token_type != TokenType::Operator || maybe_operator_token.value == PIPE_OPERATOR {
|
||||
if maybe_operator_token.token_type == TokenType::Number {
|
||||
return self.find_end_of_binary_expression(maybe_operator.index);
|
||||
} else if maybe_operator_token.token_type != TokenType::Operator
|
||||
|| maybe_operator_token.value == PIPE_OPERATOR
|
||||
{
|
||||
return Ok(index);
|
||||
}
|
||||
let next_right = self.next_meaningful_token(maybe_operator.index, None)?;
|
||||
@ -731,7 +776,6 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
if current_token.token_type == TokenType::Word
|
||||
|| current_token.token_type == TokenType::Keyword
|
||||
|| current_token.token_type == TokenType::Number
|
||||
|| current_token.token_type == TokenType::String
|
||||
{
|
||||
@ -745,6 +789,29 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Account for negative numbers.
|
||||
if current_token.token_type == TokenType::Operator || current_token.value == "-" {
|
||||
if let Some(next_token) = &next.token {
|
||||
if next_token.token_type == TokenType::Word
|
||||
|| next_token.token_type == TokenType::Number
|
||||
|| next_token.token_type == TokenType::String
|
||||
{
|
||||
// See if the next token is an operator.
|
||||
let next_right = self.next_meaningful_token(next.index, None)?;
|
||||
if let Some(next_right_token) = next_right.token {
|
||||
if next_right_token.token_type == TokenType::Operator {
|
||||
let binary_expression = self.make_binary_expression(index)?;
|
||||
return Ok(ValueReturn {
|
||||
value: Value::BinaryExpression(Box::new(binary_expression.expression)),
|
||||
last_index: binary_expression.last_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current_token.token_type == TokenType::Brace && current_token.value == "{" {
|
||||
let object_expression = self.make_object_expression(index)?;
|
||||
return Ok(ValueReturn {
|
||||
@ -761,11 +828,25 @@ impl Parser {
|
||||
}
|
||||
|
||||
if let Some(next_token) = next.token {
|
||||
if (current_token.token_type == TokenType::Keyword || current_token.token_type == TokenType::Word)
|
||||
if (current_token.token_type == TokenType::Word)
|
||||
&& (next_token.token_type == TokenType::Period
|
||||
|| (next_token.token_type == TokenType::Brace && next_token.value == "["))
|
||||
{
|
||||
let member_expression = self.make_member_expression(index)?;
|
||||
// If the next token is an operator, we need to make a binary expression.
|
||||
let next_right = self.next_meaningful_token(member_expression.last_index, None)?;
|
||||
if let Some(next_right_token) = next_right.token {
|
||||
if next_right_token.token_type == TokenType::Operator
|
||||
|| next_right_token.token_type == TokenType::Number
|
||||
{
|
||||
let binary_expression = self.make_binary_expression(index)?;
|
||||
return Ok(ValueReturn {
|
||||
value: Value::BinaryExpression(Box::new(binary_expression.expression)),
|
||||
last_index: binary_expression.last_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(ValueReturn {
|
||||
value: Value::MemberExpression(Box::new(member_expression.expression)),
|
||||
last_index: member_expression.last_index,
|
||||
@ -820,7 +901,7 @@ impl Parser {
|
||||
|
||||
Err(KclError::Unexpected(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("{:?}", current_token.token_type),
|
||||
message: format!("Unexpected token {:?}", current_token),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -841,10 +922,12 @@ impl Parser {
|
||||
if let Some(next_token_token) = next_token.token {
|
||||
let is_closing_brace = next_token_token.token_type == TokenType::Brace && next_token_token.value == "]";
|
||||
let is_comma = next_token_token.token_type == TokenType::Comma;
|
||||
if !is_closing_brace && !is_comma {
|
||||
// Check if we have a double period, which would act as an expansion operator.
|
||||
let is_double_period = next_token_token.token_type == TokenType::DoublePeriod;
|
||||
if !is_closing_brace && !is_comma && !is_double_period {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![next_token_token.clone().into()],
|
||||
message: format!("Expected a comma or closing brace, found {:?}", next_token_token.value),
|
||||
message: format!("Expected a `,`, `]`, or `..`, found {:?}", next_token_token.value),
|
||||
}));
|
||||
}
|
||||
let next_call_index = if is_closing_brace {
|
||||
@ -852,9 +935,60 @@ impl Parser {
|
||||
} else {
|
||||
self.next_meaningful_token(next_token.index, None)?.index
|
||||
};
|
||||
let mut _previous_elements = previous_elements;
|
||||
_previous_elements.push(current_element.value);
|
||||
self.make_array_elements(next_call_index, _previous_elements)
|
||||
|
||||
if is_double_period {
|
||||
// We want to expand the array.
|
||||
// Make sure the previous element is a number literal.
|
||||
if first_element_token.token_type != TokenType::Number {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![first_element_token.into()],
|
||||
message: "`..` expansion operator requires a number literal on both sides".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Make sure the next element is a number literal.
|
||||
let last_element_token = self.get_token(next_call_index)?;
|
||||
if last_element_token.token_type != TokenType::Number {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![last_element_token.into()],
|
||||
message: "`..` expansion operator requires a number literal on both sides".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Expand the array.
|
||||
let mut previous_elements = previous_elements.clone();
|
||||
let first_element = first_element_token.value.parse::<i64>().map_err(|_| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![first_element_token.into()],
|
||||
message: "expected a number literal".to_string(),
|
||||
})
|
||||
})?;
|
||||
let last_element = last_element_token.value.parse::<i64>().map_err(|_| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![last_element_token.into()],
|
||||
message: "expected a number literal".to_string(),
|
||||
})
|
||||
})?;
|
||||
if first_element > last_element {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![first_element_token.into(), last_element_token.into()],
|
||||
message: "first element must be less than or equal to the last element".to_string(),
|
||||
}));
|
||||
}
|
||||
for i in first_element..=last_element {
|
||||
previous_elements.push(Value::Literal(Box::new(Literal {
|
||||
start: first_element_token.start,
|
||||
end: first_element_token.end,
|
||||
value: i.into(),
|
||||
raw: i.to_string(),
|
||||
})));
|
||||
}
|
||||
return self.make_array_elements(next_call_index + 1, previous_elements);
|
||||
}
|
||||
|
||||
let mut previous_elements = previous_elements.clone();
|
||||
previous_elements.push(current_element.value);
|
||||
self.make_array_elements(next_call_index, previous_elements)
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![first_element_token.into()],
|
||||
@ -867,12 +1001,18 @@ impl Parser {
|
||||
let opening_brace_token = self.get_token(index)?;
|
||||
let first_element_token = self.next_meaningful_token(index, None)?;
|
||||
// Make sure there is a closing brace.
|
||||
let _closing_brace = self.find_closing_brace(index, 0, "")?;
|
||||
let closing_brace_index = self.find_closing_brace(index, 0, "").map_err(|_| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![opening_brace_token.into()],
|
||||
message: "missing a closing brace for the array".to_string(),
|
||||
})
|
||||
})?;
|
||||
let closing_brace_token = self.get_token(closing_brace_index)?;
|
||||
let array_elements = self.make_array_elements(first_element_token.index, Vec::new())?;
|
||||
Ok(ArrayReturn {
|
||||
expression: ArrayExpression {
|
||||
start: opening_brace_token.start,
|
||||
end: self.get_token(array_elements.last_index)?.end,
|
||||
end: closing_brace_token.end,
|
||||
elements: array_elements.elements,
|
||||
},
|
||||
last_index: array_elements.last_index,
|
||||
@ -949,9 +1089,23 @@ impl Parser {
|
||||
});
|
||||
}
|
||||
let argument_token = self.next_meaningful_token(index, None)?;
|
||||
|
||||
if let Some(argument_token_token) = argument_token.token {
|
||||
let next_brace_or_comma = self.next_meaningful_token(argument_token.index, None)?;
|
||||
if let Some(next_brace_or_comma_token) = next_brace_or_comma.token {
|
||||
if (argument_token_token.token_type == TokenType::Word)
|
||||
&& (next_brace_or_comma_token.token_type == TokenType::Period
|
||||
|| (next_brace_or_comma_token.token_type == TokenType::Brace
|
||||
&& next_brace_or_comma_token.value == "["))
|
||||
{
|
||||
let member_expression = self.make_member_expression(argument_token.index)?;
|
||||
let mut _previous_args = previous_args;
|
||||
_previous_args.push(Value::MemberExpression(Box::new(member_expression.expression)));
|
||||
let next_comma_or_brace_token_index =
|
||||
self.next_meaningful_token(member_expression.last_index, None)?.index;
|
||||
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
|
||||
}
|
||||
|
||||
let is_identifier_or_literal = next_brace_or_comma_token.token_type == TokenType::Comma
|
||||
|| next_brace_or_comma_token.token_type == TokenType::Brace;
|
||||
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "[" {
|
||||
@ -962,12 +1116,12 @@ impl Parser {
|
||||
_previous_args.push(Value::ArrayExpression(Box::new(array_expression.expression)));
|
||||
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
|
||||
}
|
||||
|
||||
if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "-" {
|
||||
let unary_expression = self.make_unary_expression(argument_token.index)?;
|
||||
let next_comma_or_brace_token_index =
|
||||
self.next_meaningful_token(unary_expression.last_index, None)?.index;
|
||||
let value = self.make_value(argument_token.index)?;
|
||||
let next_comma_or_brace_token_index = self.next_meaningful_token(value.last_index, None)?.index;
|
||||
let mut _previous_args = previous_args;
|
||||
_previous_args.push(Value::UnaryExpression(Box::new(unary_expression.expression)));
|
||||
_previous_args.push(value.value);
|
||||
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
|
||||
}
|
||||
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "{" {
|
||||
@ -978,8 +1132,7 @@ impl Parser {
|
||||
_previous_args.push(Value::ObjectExpression(Box::new(object_expression.expression)));
|
||||
return self.make_arguments(next_comma_or_brace_token_index, _previous_args);
|
||||
}
|
||||
if (argument_token_token.token_type == TokenType::Keyword
|
||||
|| argument_token_token.token_type == TokenType::Word
|
||||
if (argument_token_token.token_type == TokenType::Word
|
||||
|| argument_token_token.token_type == TokenType::Number
|
||||
|| argument_token_token.token_type == TokenType::String)
|
||||
&& next_brace_or_comma_token.token_type == TokenType::Operator
|
||||
@ -998,6 +1151,7 @@ impl Parser {
|
||||
_previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
|
||||
return self.make_arguments(binary_expression.last_index, _previous_args);
|
||||
}
|
||||
|
||||
if argument_token_token.token_type == TokenType::Operator
|
||||
&& argument_token_token.value == PIPE_SUBSTITUTION_OPERATOR
|
||||
{
|
||||
@ -1010,8 +1164,7 @@ impl Parser {
|
||||
_previous_args.push(value);
|
||||
return self.make_arguments(next_brace_or_comma.index, _previous_args);
|
||||
}
|
||||
if (argument_token_token.token_type == TokenType::Keyword
|
||||
|| argument_token_token.token_type == TokenType::Word)
|
||||
if argument_token_token.token_type == TokenType::Word
|
||||
&& next_brace_or_comma_token.token_type == TokenType::Brace
|
||||
&& next_brace_or_comma_token.value == "("
|
||||
{
|
||||
@ -1085,9 +1238,14 @@ impl Parser {
|
||||
let brace_token = self.next_meaningful_token(index, None)?;
|
||||
let callee = self.make_identifier(index)?;
|
||||
// Make sure there is a closing brace.
|
||||
let _closing_brace_token = self.find_closing_brace(brace_token.index, 0, "")?;
|
||||
let closing_brace_index = self.find_closing_brace(brace_token.index, 0, "").map_err(|_| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "missing a closing brace for the function call".to_string(),
|
||||
})
|
||||
})?;
|
||||
let closing_brace_token = self.get_token(closing_brace_index)?;
|
||||
let args = self.make_arguments(brace_token.index, vec![])?;
|
||||
let closing_brace_token = self.get_token(args.last_index)?;
|
||||
let function = if let Some(stdlib_fn) = self.stdlib.get(&callee.name) {
|
||||
crate::abstract_syntax_tree_types::Function::StdLib { func: stdlib_fn }
|
||||
} else {
|
||||
@ -1127,6 +1285,18 @@ impl Parser {
|
||||
previous_declarators: Vec<VariableDeclarator>,
|
||||
) -> Result<VariableDeclaratorsReturn, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
|
||||
// Make sure they are not assigning a variable to a reserved keyword.
|
||||
if current_token.token_type == TokenType::Keyword {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!(
|
||||
"Cannot assign a variable to a reserved keyword: {}",
|
||||
current_token.value
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
let assignment = self.next_meaningful_token(index, None)?;
|
||||
let Some(assignment_token) = assignment.token else {
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
@ -1169,17 +1339,40 @@ impl Parser {
|
||||
fn make_variable_declaration(&self, index: usize) -> Result<VariableDeclarationResult, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
let declaration_start_token = self.next_meaningful_token(index, None)?;
|
||||
let kind = VariableKind::from_str(¤t_token.value).map_err(|_| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("Unexpected token: {}", current_token.value),
|
||||
})
|
||||
})?;
|
||||
let variable_declarators_result = self.make_variable_declarators(declaration_start_token.index, vec![])?;
|
||||
|
||||
// Check if we have a fn variable kind but are not assigning a function.
|
||||
if !variable_declarators_result.declarations.is_empty() {
|
||||
if let Some(declarator) = variable_declarators_result.declarations.get(0) {
|
||||
if let Value::FunctionExpression(_) = declarator.init {
|
||||
if kind != VariableKind::Fn {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("Expected a `fn` variable kind, found: `{}`", current_token.value),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// If we have anything other than a function, make sure we are not using the `fn` variable kind.
|
||||
if kind == VariableKind::Fn {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: format!("Expected a `let` variable kind, found: `{}`", current_token.value),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(VariableDeclarationResult {
|
||||
declaration: VariableDeclaration {
|
||||
start: current_token.start,
|
||||
end: variable_declarators_result.declarations[variable_declarators_result.declarations.len() - 1].end,
|
||||
kind: VariableKind::from_str(¤t_token.value).map_err(|_| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "Unexpected token".to_string(),
|
||||
})
|
||||
})?,
|
||||
kind,
|
||||
declarations: variable_declarators_result.declarations,
|
||||
},
|
||||
last_index: variable_declarators_result.last_index,
|
||||
@ -1189,27 +1382,38 @@ impl Parser {
|
||||
fn make_params(&self, index: usize, previous_params: Vec<Identifier>) -> Result<ParamsResult, KclError> {
|
||||
let brace_or_comma_token = self.get_token(index)?;
|
||||
let argument = self.next_meaningful_token(index, None)?;
|
||||
if let Some(argument_token) = argument.token {
|
||||
let should_finish_recursion = (argument_token.token_type == TokenType::Brace
|
||||
&& argument_token.value == ")")
|
||||
|| (brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")");
|
||||
if should_finish_recursion {
|
||||
return Ok(ParamsResult {
|
||||
params: previous_params,
|
||||
last_index: index,
|
||||
});
|
||||
}
|
||||
let next_brace_or_comma_token = self.next_meaningful_token(argument.index, None)?;
|
||||
let identifier = self.make_identifier(argument.index)?;
|
||||
let mut _previous_params = previous_params;
|
||||
_previous_params.push(identifier);
|
||||
self.make_params(next_brace_or_comma_token.index, _previous_params)
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
let Some(argument_token) = argument.token else {
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {}", brace_or_comma_token.value),
|
||||
}))
|
||||
message: format!("expected a function parameter, found: {}", brace_or_comma_token.value),
|
||||
}));
|
||||
};
|
||||
|
||||
let should_finish_recursion = (argument_token.token_type == TokenType::Brace && argument_token.value == ")")
|
||||
|| (brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")");
|
||||
if should_finish_recursion {
|
||||
return Ok(ParamsResult {
|
||||
params: previous_params,
|
||||
last_index: index,
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure they are not assigning a variable to a reserved keyword.
|
||||
if argument_token.token_type == TokenType::Keyword {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![argument_token.clone().into()],
|
||||
message: format!(
|
||||
"Cannot assign a variable to a reserved keyword: {}",
|
||||
argument_token.value
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
let next_brace_or_comma_token = self.next_meaningful_token(argument.index, None)?;
|
||||
let identifier = self.make_identifier(argument.index)?;
|
||||
let mut _previous_params = previous_params;
|
||||
_previous_params.push(identifier);
|
||||
self.make_params(next_brace_or_comma_token.index, _previous_params)
|
||||
}
|
||||
|
||||
fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
|
||||
@ -1239,10 +1443,11 @@ impl Parser {
|
||||
Value::Literal(literal) => BinaryPart::Literal(literal),
|
||||
Value::UnaryExpression(unary_expression) => BinaryPart::UnaryExpression(unary_expression),
|
||||
Value::CallExpression(call_expression) => BinaryPart::CallExpression(call_expression),
|
||||
Value::MemberExpression(member_expression) => BinaryPart::MemberExpression(member_expression),
|
||||
_ => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "Invalid argument for unary expression".to_string(),
|
||||
message: format!("Invalid argument for unary expression: {:?}", argument.value),
|
||||
}));
|
||||
}
|
||||
},
|
||||
@ -1462,7 +1667,7 @@ impl Parser {
|
||||
}
|
||||
|
||||
if let Some(next_token) = next.token {
|
||||
if (token.token_type == TokenType::Keyword || token.token_type == TokenType::Word)
|
||||
if token.token_type == TokenType::Word
|
||||
&& next_token.token_type == TokenType::Brace
|
||||
&& next_token.value == "("
|
||||
{
|
||||
@ -1488,9 +1693,7 @@ impl Parser {
|
||||
|
||||
let next_thing = self.next_meaningful_token(token_index, None)?;
|
||||
if let Some(next_thing_token) = next_thing.token {
|
||||
if (token.token_type == TokenType::Number
|
||||
|| token.token_type == TokenType::Word
|
||||
|| token.token_type == TokenType::Keyword)
|
||||
if (token.token_type == TokenType::Number || token.token_type == TokenType::Word)
|
||||
&& next_thing_token.token_type == TokenType::Operator
|
||||
{
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
@ -1513,7 +1716,7 @@ impl Parser {
|
||||
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![token.into()],
|
||||
message: "unexpected token".to_string(),
|
||||
message: format!("unexpected token {}", token.value),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -1730,7 +1933,7 @@ const key = 'c'"#,
|
||||
fn test_collect_object_keys() {
|
||||
let tokens = crate::tokeniser::lexer("const prop = yo.one[\"two\"]");
|
||||
let parser = Parser::new(tokens);
|
||||
let keys_info = parser.collect_object_keys(6, None).unwrap();
|
||||
let keys_info = parser.collect_object_keys(6, None, false).unwrap();
|
||||
assert_eq!(keys_info.len(), 2);
|
||||
let first_key = match keys_info[0].key.clone() {
|
||||
LiteralIdentifier::Identifier(identifier) => format!("identifier-{}", identifier.name),
|
||||
@ -2759,6 +2962,73 @@ show(mySk1)"#;
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_double_nested_braces() {
|
||||
let tokens = crate::tokeniser::lexer(r#"const prop = yo["one"][two]"#);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_period_number_first() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = 1 - obj.a"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_brace_number_first() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = 1 - obj["a"]"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_brace_number_second() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = obj["a"] - 1"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_in_array_number_first() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = [1 - obj["a"], 0]"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_in_array_number_second() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = [obj["a"] - 1, 0]"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_in_array_number_second_missing_space() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = [obj["a"] -1, 0]"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_half_pipe() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
@ -2816,7 +3086,10 @@ z(-[["#,
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("unexpected end"));
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([1, 2])], message: "missing a closing brace for the function call" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2828,7 +3101,10 @@ z(-[["#,
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("unexpected end"));
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 1])], message: "missing a closing brace for the function call" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2881,4 +3157,201 @@ e
|
||||
.to_string()
|
||||
.contains("unexpected end of expression"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expand_array() {
|
||||
let code = "const myArray = [0..10]";
|
||||
let parser = Parser::new(crate::tokeniser::lexer(code));
|
||||
let result = parser.ast().unwrap();
|
||||
let expected_result = Program {
|
||||
start: 0,
|
||||
end: 23,
|
||||
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
|
||||
start: 0,
|
||||
end: 23,
|
||||
declarations: vec![VariableDeclarator {
|
||||
start: 6,
|
||||
end: 23,
|
||||
id: Identifier {
|
||||
start: 6,
|
||||
end: 13,
|
||||
name: "myArray".to_string(),
|
||||
},
|
||||
init: Value::ArrayExpression(Box::new(ArrayExpression {
|
||||
start: 16,
|
||||
end: 23,
|
||||
elements: vec![
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 0.into(),
|
||||
raw: "0".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 1.into(),
|
||||
raw: "1".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 2.into(),
|
||||
raw: "2".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 3.into(),
|
||||
raw: "3".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 4.into(),
|
||||
raw: "4".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 5.into(),
|
||||
raw: "5".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 6.into(),
|
||||
raw: "6".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 7.into(),
|
||||
raw: "7".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 8.into(),
|
||||
raw: "8".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 9.into(),
|
||||
raw: "9".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 10.into(),
|
||||
raw: "10".to_string(),
|
||||
})),
|
||||
],
|
||||
})),
|
||||
}],
|
||||
kind: VariableKind::Const,
|
||||
})],
|
||||
non_code_meta: NoneCodeMeta {
|
||||
none_code_nodes: Default::default(),
|
||||
start: None,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_variable() {
|
||||
let some_program_string = r#"const let = "thing""#;
|
||||
let tokens = crate::tokeniser::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([6, 9])], message: "Cannot assign a variable to a reserved keyword: let" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_fn_name() {
|
||||
let some_program_string = r#"fn let = () {}"#;
|
||||
let tokens = crate::tokeniser::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6])], message: "Cannot assign a variable to a reserved keyword: let" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_fn_args() {
|
||||
let some_program_string = r#"fn thing = (let) => {
|
||||
return 1
|
||||
}"#;
|
||||
let tokens = crate::tokeniser::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15])], message: "Cannot assign a variable to a reserved keyword: let" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keyword_ok_in_fn_args_return() {
|
||||
let some_program_string = r#"fn thing = (param) => {
|
||||
return true
|
||||
}
|
||||
|
||||
thing(false)
|
||||
"#;
|
||||
let tokens = crate::tokeniser::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_define_function_as_var() {
|
||||
for name in ["var", "let", "const"] {
|
||||
let some_program_string = format!(
|
||||
r#"{} thing = (param) => {{
|
||||
return true
|
||||
}}
|
||||
|
||||
thing(false)
|
||||
"#,
|
||||
name
|
||||
);
|
||||
let tokens = crate::tokeniser::lexer(&some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
format!(
|
||||
r#"syntax: KclErrorDetails {{ source_ranges: [SourceRange([0, {}])], message: "Expected a `fn` variable kind, found: `{}`" }}"#,
|
||||
name.len(),
|
||||
name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_define_var_as_function() {
|
||||
let some_program_string = r#"fn thing = "thing""#;
|
||||
let tokens = crate::tokeniser::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 2])], message: "Expected a `let` variable kind, found: `fn`" }"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -103,12 +103,12 @@ impl<'a> Args<'a> {
|
||||
}
|
||||
|
||||
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(crate::executor::UserVal {
|
||||
value: j,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.source_range,
|
||||
}],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
|
||||
|
@ -161,34 +161,12 @@ pub enum LineData {
|
||||
/// A point with a tag.
|
||||
PointWithTag {
|
||||
/// The to point.
|
||||
to: PointOrDefault,
|
||||
to: [f64; 2],
|
||||
/// 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.
|
||||
@ -205,12 +183,9 @@ pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
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::PointWithTag { to, .. } => *to,
|
||||
LineData::Point(to) => *to,
|
||||
LineData::Default(_) => default,
|
||||
};
|
||||
|
||||
let to = [from.x + inner_args[0], from.y + inner_args[1]];
|
||||
@ -283,10 +258,7 @@ pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
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::LengthWithTag { length, tag } => LineData::PointWithTag { to: [length, 0.0], tag },
|
||||
AxisLineData::Length(length) => LineData::Point([length, 0.0]),
|
||||
};
|
||||
|
||||
@ -308,10 +280,7 @@ pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
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::LengthWithTag { length, tag } => LineData::PointWithTag { to: [0.0, length], tag },
|
||||
AxisLineData::Length(length) => LineData::Point([0.0, length]),
|
||||
};
|
||||
|
||||
@ -427,10 +396,7 @@ fn inner_angled_line_of_x_length(
|
||||
|
||||
let new_sketch_group = inner_line(
|
||||
if let AngledLineData::AngleWithTag { tag, .. } = data {
|
||||
LineData::PointWithTag {
|
||||
to: PointOrDefault::Point(to),
|
||||
tag,
|
||||
}
|
||||
LineData::PointWithTag { to, tag }
|
||||
} else {
|
||||
LineData::Point(to)
|
||||
},
|
||||
@ -525,10 +491,7 @@ fn inner_angled_line_of_y_length(
|
||||
|
||||
let new_sketch_group = inner_line(
|
||||
if let AngledLineData::AngleWithTag { tag, .. } = data {
|
||||
LineData::PointWithTag {
|
||||
to: PointOrDefault::Point(to),
|
||||
tag,
|
||||
}
|
||||
LineData::PointWithTag { to, tag }
|
||||
} else {
|
||||
LineData::Point(to)
|
||||
},
|
||||
@ -654,11 +617,9 @@ pub fn start_sketch_at(args: &mut Args) -> Result<MemoryItem, KclError> {
|
||||
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::PointWithTag { to, .. } => *to,
|
||||
LineData::Point(to) => *to,
|
||||
LineData::Default(_) => default,
|
||||
};
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
@ -992,16 +953,12 @@ mod tests {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::std::sketch::{LineData, PointOrDefault};
|
||||
use crate::std::sketch::LineData;
|
||||
|
||||
#[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();
|
||||
let mut str_json = serde_json::to_string(&data).unwrap();
|
||||
assert_eq!(str_json, "[0.0,1.0]");
|
||||
|
||||
str_json = "[0, 1]".to_string();
|
||||
@ -1013,7 +970,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
data,
|
||||
LineData::PointWithTag {
|
||||
to: PointOrDefault::Point([0.0, 1.0]),
|
||||
to: [0.0, 1.0],
|
||||
tag: "thing".to_string()
|
||||
}
|
||||
);
|
||||
|
@ -34,6 +34,8 @@ pub enum TokenType {
|
||||
Colon,
|
||||
/// A period.
|
||||
Period,
|
||||
/// A double period: `..`.
|
||||
DoublePeriod,
|
||||
/// A line comment.
|
||||
LineComment,
|
||||
/// A block comment.
|
||||
@ -54,7 +56,12 @@ impl TryFrom<TokenType> for SemanticTokenType {
|
||||
TokenType::LineComment => Self::COMMENT,
|
||||
TokenType::BlockComment => Self::COMMENT,
|
||||
TokenType::Function => Self::FUNCTION,
|
||||
TokenType::Whitespace | TokenType::Brace | TokenType::Comma | TokenType::Colon | TokenType::Period => {
|
||||
TokenType::Whitespace
|
||||
| TokenType::Brace
|
||||
| TokenType::Comma
|
||||
| TokenType::Colon
|
||||
| TokenType::Period
|
||||
| TokenType::DoublePeriod => {
|
||||
anyhow::bail!("unsupported token type: {:?}", token_type)
|
||||
}
|
||||
})
|
||||
@ -135,7 +142,7 @@ lazy_static! {
|
||||
static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
|
||||
// TODO: these should be generated using our struct types for these.
|
||||
static ref KEYWORD: Regex =
|
||||
Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|true|false|nil|and|or|not|var|const)\b").unwrap();
|
||||
Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|mut|loop|true|false|nil|and|or|not|var|const)\b").unwrap();
|
||||
static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
|
||||
static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
|
||||
static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
|
||||
@ -147,6 +154,7 @@ lazy_static! {
|
||||
static ref COMMA: Regex = Regex::new(r"^,").unwrap();
|
||||
static ref COLON: Regex = Regex::new(r"^:").unwrap();
|
||||
static ref PERIOD: Regex = Regex::new(r"^\.").unwrap();
|
||||
static ref DOUBLE_PERIOD: Regex = Regex::new(r"^\.\.").unwrap();
|
||||
static ref LINECOMMENT: Regex = Regex::new(r"^//.*").unwrap();
|
||||
static ref BLOCKCOMMENT: Regex = Regex::new(r"^/\*[\s\S]*?\*/").unwrap();
|
||||
}
|
||||
@ -196,6 +204,9 @@ fn is_comma(character: &str) -> bool {
|
||||
fn is_colon(character: &str) -> bool {
|
||||
COLON.is_match(character)
|
||||
}
|
||||
fn is_double_period(character: &str) -> bool {
|
||||
DOUBLE_PERIOD.is_match(character)
|
||||
}
|
||||
fn is_period(character: &str) -> bool {
|
||||
PERIOD.is_match(character)
|
||||
}
|
||||
@ -296,13 +307,6 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
|
||||
start_index,
|
||||
));
|
||||
}
|
||||
if is_number(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::Number,
|
||||
&match_first(str_from_index, &NUMBER)?,
|
||||
start_index,
|
||||
));
|
||||
}
|
||||
if is_operator(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::Operator,
|
||||
@ -310,6 +314,13 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
|
||||
start_index,
|
||||
));
|
||||
}
|
||||
if is_number(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::Number,
|
||||
&match_first(str_from_index, &NUMBER)?,
|
||||
start_index,
|
||||
));
|
||||
}
|
||||
if is_keyword(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::Keyword,
|
||||
@ -331,6 +342,13 @@ fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
|
||||
start_index,
|
||||
));
|
||||
}
|
||||
if is_double_period(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::DoublePeriod,
|
||||
&match_first(str_from_index, &DOUBLE_PERIOD)?,
|
||||
start_index,
|
||||
));
|
||||
}
|
||||
if is_period(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::Period,
|
||||
|
98
src/wasm-lib/tests/executor/main.rs
Normal file
98
src/wasm-lib/tests/executor/main.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Executes a kcl program and takes a snapshot of the result.
|
||||
/// This returns the bytes of the snapshot.
|
||||
async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
|
||||
let ws = client
|
||||
.modeling()
|
||||
.commands_ws(None, None, None, None, Some(false))
|
||||
.await?;
|
||||
|
||||
// Create a temporary file to write the output to.
|
||||
let output_file = std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
|
||||
|
||||
let tokens = kcl_lib::tokeniser::lexer(code);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
let program = parser.ast()?;
|
||||
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
|
||||
let mut engine = kcl_lib::engine::EngineConnection::new(
|
||||
ws,
|
||||
std::env::temp_dir().display().to_string().as_str(),
|
||||
output_file.display().to_string().as_str(),
|
||||
)
|
||||
.await?;
|
||||
let _ = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)?;
|
||||
|
||||
// Send a snapshot request to the engine.
|
||||
engine.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||
format: kittycad::types::ImageFormat::Png,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Wait for the snapshot to be taken.
|
||||
engine.wait_for_snapshot().await;
|
||||
|
||||
// Read the output file.
|
||||
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
|
||||
Ok(actual)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_sketch() {
|
||||
let code = r#"fn box = (h, l, w) => {
|
||||
const myBox = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const fnBox = box(3, 6, 10)
|
||||
|
||||
show(fnBox)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 1.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_angled_line() {
|
||||
let code = r#"const part001 = startSketchAt([4.83, 12.56])
|
||||
|> line([15.1, 2.48], %)
|
||||
|> line({ to: [3.15, -9.85], tag: 'seg01' }, %)
|
||||
|> line([-15.17, -4.1], %)
|
||||
|> angledLine([segAng('seg01', %), 12.35], %)
|
||||
|> line([-13.02, 10.03], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0);
|
||||
}
|
BIN
src/wasm-lib/tests/executor/outputs/angled_line.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/angled_line.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
src/wasm-lib/tests/executor/outputs/function_sketch.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/function_sketch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
Reference in New Issue
Block a user