Merge branch 'main' into franknoirot/xstate-toolbar

This commit is contained in:
Frank Noirot
2023-09-01 18:04:51 -04:00
54 changed files with 4388 additions and 1341 deletions

View File

@ -1,4 +1,7 @@
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_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
VITE_KC_SENTRY_DSN=

View File

@ -1,4 +1,7 @@
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=15000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

@ -124,6 +124,8 @@ jobs:
publish-apps-release:
runs-on: ubuntu-20.04
if: github.event_name == 'release'
permissions:
contents: write
needs: [build-test-web, build-apps]
env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
@ -189,3 +191,8 @@ jobs:
with:
path: last_update.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload release files to Github
uses: softprops/action-gh-release@v1
with:
files: artifact/*/kittycad-modeling-app*

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,8 @@
* [`angledLineThatIntersects`](#angledLineThatIntersects)
* [`startSketchAt`](#startSketchAt)
* [`close`](#close)
* [`arc`](#arc)
* [`bezierCurve`](#bezierCurve)
## Functions
@ -3046,3 +3048,351 @@ close(sketch_group: SketchGroup) -> SketchGroup
### arc
Draw an arc.
```
arc(data: ArcData, sketch_group: SketchGroup) -> SketchGroup
```
#### Arguments
* `data`: `ArcData` - Data to draw an arc.
```
{
// The end angle.
"angle_end": number,
// The start angle.
"angle_start": number,
// The radius.
"radius": number,
// The tag.
"tag": string,
} |
{
// The end angle.
"angle_end": number,
// The start angle.
"angle_start": number,
// The radius.
"radius": number,
} |
{
// The center.
"center": [number],
// The radius.
"radius": number,
// The tag.
"tag": string,
// The to point.
"to": [number],
} |
{
// The center.
"center": [number],
// The radius.
"radius": number,
// The to point.
"to": [number],
}
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```
#### Returns
* `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```
### bezierCurve
Draw a bezier curve.
```
bezierCurve(data: BezierData, sketch_group: SketchGroup) -> SketchGroup
```
#### Arguments
* `data`: `BezierData` - Data to draw a bezier curve.
```
{
// The first control point.
"control1": [number],
// The second control point.
"control2": [number],
// The tag.
"tag": string,
// The to point.
"to": [number],
} |
{
// The first control point.
"control1": [number],
// The second control point.
"control2": [number],
// The to point.
"to": [number],
}
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```
#### Returns
* `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2",
@ -9,8 +9,9 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.13",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.34",
"@kittycad/lib": "^0.0.35",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
@ -56,13 +57,13 @@
"build:both:local": "yarn build:wasm && vite build",
"test": "vitest --mode development",
"test:nowatch": "vitest run --mode development",
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)",
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
"test:cov": "vitest run --coverage --mode development",
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
"simpleserver": "http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",

56
src-tauri/Cargo.lock generated
View File

@ -648,6 +648,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.1"
@ -1150,7 +1156,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@ -1163,6 +1169,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.3.3"
@ -1378,7 +1390,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"serde",
]
@ -2128,7 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64 0.21.2",
"indexmap",
"indexmap 1.9.3",
"line-wrap",
"quick-xml",
"serde",
@ -2701,14 +2724,15 @@ dependencies = [
[[package]]
name = "serde_with"
version = "2.3.3"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
dependencies = [
"base64 0.13.1",
"base64 0.21.2",
"chrono",
"hex",
"indexmap",
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_json",
"serde_with_macros",
@ -2717,9 +2741,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "2.3.3"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
dependencies = [
"darling",
"proc-macro2",
@ -3075,9 +3099,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389"
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
dependencies = [
"anyhow",
"cargo_toml",
@ -3088,7 +3112,6 @@ dependencies = [
"serde_json",
"tauri-utils",
"tauri-winres",
"winnow",
]
[[package]]
@ -3186,12 +3209,13 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
dependencies = [
"brotli",
"ctor",
"dunce",
"glob",
"heck 0.4.1",
"html5ever",
@ -3407,7 +3431,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"nom8",
"serde",
"serde_spanned",
@ -3420,7 +3444,7 @@ version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"serde",
"serde_spanned",
"toml_datetime 0.6.2",

View File

@ -12,7 +12,7 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.3.0", features = [] }
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
anyhow = "1"

View File

@ -8,7 +8,7 @@
},
"package": {
"productName": "kittycad-modeling-app",
"version": "0.1.0"
"version": "0.2.0"
},
"tauri": {
"allowlist": {

View File

@ -49,6 +49,7 @@ import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
import { IndexLoaderData } from './Router'
import { toast } from 'react-hot-toast'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding'
export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -154,7 +155,7 @@ export function App() {
useHotkeys('shift + d', () => togglePane('debug'))
const paneOpacity =
onboardingStatus === 'camera'
onboardingStatus === onboardingPaths.CAMERA
? 'opacity-20'
: didDragInStream
? 'opacity-40'
@ -278,6 +279,8 @@ export function App() {
useEffect(() => {
if (!isStreamReady) return
if (!engineCommandManager) return
let unsubFn: any[] = []
const asyncWrap = async () => {
try {
if (!code) {
@ -288,11 +291,8 @@ export function App() {
setAst(_ast)
resetLogs()
resetKCLErrors()
if (engineCommandManager) {
engineCommandManager.endSession()
engineCommandManager.startNewSession()
}
if (!engineCommandManager) return
engineCommandManager.endSession()
engineCommandManager.startNewSession()
const programMemory = await _executor(
_ast,
{
@ -326,22 +326,29 @@ export function App() {
await engineCommandManager.waitForAllCommands()
setArtifactMap({ artifactMap, sourceRangeMap })
engineCommandManager.onHover((id) => {
if (!id) {
setHighlightRange([0, 0])
} else {
const sourceRange = sourceRangeMap[id]
setHighlightRange(sourceRange)
}
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (!data?.entity_id) {
setHighlightRange([0, 0])
} else {
const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
}
},
})
engineCommandManager.onClick((selections) => {
if (!selections) {
setCursor2()
return
}
const { id, type } = selections
setCursor2({ range: sourceRangeMap[id], type })
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
unsubFn.push(unSubHover, unSubClick)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
@ -358,7 +365,10 @@ export function App() {
}
}
asyncWrap()
}, [code, isStreamReady])
return () => {
unsubFn.forEach((fn) => fn())
}
}, [code, isStreamReady, engineCommandManager])
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message)

View File

@ -3,8 +3,15 @@ import {
createBrowserRouter,
Outlet,
redirect,
useLocation,
RouterProvider,
} from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, {
@ -31,6 +38,40 @@ import {
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({
dsn: VITE_KC_SENTRY_DSN,
// TODO(paultag): pass in the right env here.
// environment: "production",
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
),
}),
new Sentry.Replay(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
tracesSampleRate: 1.0,
// TODO: Add in kittycad.io endpoints
tracePropagationTargets: ['localhost'],
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -121,7 +162,9 @@ const router = createBrowserRouter(
notEnRouteToOnboarding && hasValidOnboardingStatus
if (shouldRedirectToOnboarding) {
return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status)
return redirect(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
)
}
if (params.id && params.id !== 'new') {

View File

@ -0,0 +1,7 @@
/*
Some CSS cannot be represented
in Tailwind, such as complex grid layouts.
*/
.header {
grid-template-columns: 1fr auto 1fr;
}

View File

@ -3,6 +3,7 @@ import UserSidebarMenu from './UserSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -27,7 +28,9 @@ export const AppHeader = ({
return (
<header
className={
'overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
(showToolbar ? 'grid ' : 'flex justify-between ') +
styles.header +
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
className
}
>
@ -39,7 +42,11 @@ export const AppHeader = ({
</div>
)}
{/* If there are children, show them, otherwise show User menu */}
{children || <UserSidebarMenu user={user} />}
{children || (
<div className="ml-auto">
<UserSidebarMenu user={user} />
</div>
)}
</header>
)
}

View File

@ -1,10 +1,10 @@
.panel {
@apply relative overflow-auto z-0;
@apply bg-chalkboard-20/40;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
}
:global(.dark) .panel {
@apply bg-chalkboard-110/50;
@apply bg-chalkboard-110/50 backdrop-blur-0;
}
.header {

View File

@ -15,7 +15,7 @@ import {
settingsMachine,
} from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import { setThemeClass } from 'lib/theme'
import { setThemeClass, Themes } from 'lib/theme'
import {
AnyStateMachine,
ContextFrom,
@ -87,10 +87,21 @@ export const GlobalStateProvider = ({
commandBarMeta: settingsCommandBarMeta,
})
useEffect(
() => setThemeClass(settingsState.context.theme),
[settingsState.context.theme]
)
// Listen for changes to the system theme and update the app theme accordingly
// This is only done if the theme setting is set to 'system'.
// It can't be done in XState (in an invoked callback, for example)
// because there doesn't seem to be a good way to listen to
// events outside of the machine that also depend on the machine's context
useEffect(() => {
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
const listener = (e: MediaQueryListEvent) => {
if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
}
matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener)
}, [settingsState.context])
// Auth machine setup
const [authState, authSend] = useMachine(authMachine, {

View File

@ -1,10 +1,11 @@
import { Popover } from '@headlessui/react'
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata, paths } from '../Router'
import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton'
import { Fragment } from 'react'
const ProjectSidebarMenu = ({
project,
@ -34,7 +35,7 @@ const ProjectSidebarMenu = ({
) : (
<Popover className="relative">
<Popover.Button
className="border-0 px-1 pr-2 pl-0 flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-energy-50"
className="border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
data-testid="project-sidebar-toggle"
>
<img
@ -46,54 +47,77 @@ const ProjectSidebarMenu = ({
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
</span>
</Popover.Button>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
</Transition>
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 shadow-md rounded-r-lg overflow-hidden">
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="h-9 w-auto"
/>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 -translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 -translate-x-4"
as={Fragment}
>
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden">
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="h-9 w-auto"
/>
<div>
<p
className="m-0 text-energy-10 text-mono"
data-testid="projectName"
>
{project?.name ? project.name : 'KittyCAD Modeling App'}
</p>
{project?.entrypoint_metadata && (
<p className="m-0 text-energy-40 text-xs" data-testid="createdAt">
Created{' '}
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
<div>
<p
className="m-0 text-energy-10 text-mono"
data-testid="projectName"
>
{project?.name ? project.name : 'KittyCAD Modeling App'}
</p>
{project?.entrypoint_metadata && (
<p
className="m-0 text-energy-40 text-xs"
data-testid="createdAt"
>
Created{' '}
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
</p>
)}
</div>
</div>
<div className="p-4 flex flex-col gap-2">
<ExportButton
className={{
button:
'border-transparent dark:border-transparent dark:hover:border-energy-60',
}}
>
Export Model
</ExportButton>
{isTauri() && (
<ActionButton
Element="link"
to={paths.HOME}
icon={{
icon: faHome,
}}
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
>
Go to Home
</ActionButton>
)}
</div>
</div>
<div className="p-4 flex flex-col gap-2">
<ExportButton
className={{
button:
'border-transparent dark:border-transparent dark:hover:border-energy-60',
}}
>
Export Model
</ExportButton>
{isTauri() && (
<ActionButton
Element="link"
to={paths.HOME}
icon={{
icon: faHome,
}}
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
>
Go to Home
</ActionButton>
)}
</div>
</Popover.Panel>
</Popover.Panel>
</Transition>
</Popover>
)
}

View File

@ -1,9 +1,9 @@
import { Popover } from '@headlessui/react'
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { Fragment, useState } from 'react'
import { paths } from '../Router'
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
import { Models } from '@kittycad/lib'
@ -61,82 +61,102 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
Menu
</ActionButton>
)}
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
</Transition>
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 shadow-md rounded-l-lg overflow-hidden">
{({ close }) => (
<>
{user && (
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
{user.image && !imageLoadFailed && (
<div className="rounded-full shadow-inner overflow-hidden">
<img
src={user.image}
alt={user.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
)}
<div>
<p
className="m-0 text-liquid-10 text-mono"
data-testid="username"
>
{displayedName || ''}
</p>
{displayedName !== user.email && (
<p
className="m-0 text-liquid-40 text-xs"
data-testid="email"
>
{user.email}
</p>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 translate-x-4"
as={Fragment}
>
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden">
{({ close }) => (
<>
{user && (
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
{user.image && !imageLoadFailed && (
<div className="rounded-full shadow-inner overflow-hidden">
<img
src={user.image}
alt={user.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
)}
<div>
<p
className="m-0 text-liquid-10 text-mono"
data-testid="username"
>
{displayedName || ''}
</p>
{displayedName !== user.email && (
<p
className="m-0 text-liquid-40 text-xs"
data-testid="email"
>
{user.email}
</p>
)}
</div>
</div>
)}
<div className="p-4 flex flex-col gap-2">
<ActionButton
Element="button"
icon={{ icon: faGear }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
// automatically when navigating to it
close()
navigate(makeUrlPathRelative(paths.SETTINGS))
}}
>
Settings
</ActionButton>
<ActionButton
Element="link"
to="https://github.com/KittyCAD/modeling-app/discussions"
icon={{ icon: faGithub }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
>
Request a feature
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('Log out')}
icon={{
icon: faSignOutAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
>
Sign out
</ActionButton>
</div>
)}
<div className="p-4 flex flex-col gap-2">
<ActionButton
Element="button"
icon={{ icon: faGear }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
// automatically when navigating to it
close()
navigate(makeUrlPathRelative(paths.SETTINGS))
}}
>
Settings
</ActionButton>
<ActionButton
Element="link"
to="https://github.com/KittyCAD/modeling-app/discussions"
icon={{ icon: faGithub }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
>
Request a feature
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('Log out')}
icon={{
icon: faSignOutAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
>
Sign out
</ActionButton>
</div>
</>
)}
</Popover.Panel>
</>
)}
</Popover.Panel>
</Transition>
</Popover>
)
}

View File

@ -8,6 +8,9 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
.VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
export const TEST = import.meta.env.TEST

View File

@ -86,8 +86,18 @@ code {
@apply bg-transparent;
}
#code-mirror-override .cm-activeLine,
#code-mirror-override .cm-activeLineGutter {
@apply bg-liquid-10/50;
}
.dark #code-mirror-override .cm-activeLine,
.dark #code-mirror-override .cm-activeLineGutter {
@apply bg-liquid-80/50;
}
#code-mirror-override .cm-gutters {
@apply bg-chalkboard-10/50;
@apply bg-chalkboard-10/30;
}
.dark #code-mirror-override .cm-gutters {
@ -99,14 +109,24 @@ code {
}
#code-mirror-override .cm-cursor {
display: block;
width: 200px;
background: linear-gradient(
to right,
rgb(0, 55, 94) 0%,
#0084e2ff 2%,
#0084e255 5%,
transparent 100%
);
width: 1ch;
@apply bg-liquid-40 mix-blend-multiply;
animation: blink 2s ease-out infinite;
}
.dark #code-mirror-override .cm-cursor {
@apply bg-liquid-50;
}
@keyframes blink {
0%,
100% {
opacity: 0;
}
15% {
opacity: 0.75;
}
}
.react-json-view {

View File

@ -16,10 +16,8 @@ export const parser_wasm = (code: string): Program => {
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
parsed.kind === 'invalid_expression'
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)
@ -36,10 +34,8 @@ export async function asyncParser(code: string): Promise<Program> {
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
parsed.kind === 'invalid_expression'
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)

View File

@ -146,10 +146,8 @@ export const _executor = async (
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
parsed.kind === 'invalid_expression'
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)

View File

@ -1,9 +1,14 @@
import { SourceRange } from 'lang/executor'
import { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import {
VITE_KC_API_WS_MODELING_URL,
VITE_KC_CONNECTION_TIMEOUT_MS,
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
} from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
interface ResultCommand {
type: 'result'
@ -22,16 +27,6 @@ export interface SourceRangeMap {
[key: string]: SourceRange
}
interface SelectionsArgs {
id: string
type: Selections['codeBasedSelections'][number]['type']
}
interface CursorSelectionsArgs {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
}
interface NewTrackArgs {
conn: EngineConnection
mediaStream: MediaStream
@ -45,7 +40,7 @@ type WebSocketResponse = Models['OkWebSocketResponseData_type']
export class EngineConnection {
websocket?: WebSocket
pc?: RTCPeerConnection
lossyDataChannel?: RTCDataChannel
unreliableDataChannel?: RTCDataChannel
private ready: boolean
@ -107,6 +102,11 @@ export class EngineConnection {
isReady() {
return this.ready
}
// shouldTrace will return true when Sentry should be used to instrument
// the Engine.
shouldTrace() {
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
}
// connect will attempt to connect to the Engine over a WebSocket, and
// establish the WebRTC connections.
//
@ -116,6 +116,44 @@ export class EngineConnection {
// TODO(paultag): make this safe to call multiple times, and figure out
// when a connection is in progress (state: connecting or something).
// Information on the connect transaction
class SpanPromise {
span: Sentry.Span
promise: Promise<void>
resolve?: (v: void) => void
constructor(span: Sentry.Span) {
this.span = span
this.promise = new Promise((resolve) => {
this.resolve = (v: void) => {
// here we're going to invoke finish before resolving the
// promise so that a `.then()` will order strictly after
// all spans have -- for sure -- been resolved, rather than
// doing a `then` on this promise.
this.span.finish()
resolve(v)
}
})
}
}
let webrtcMediaTransaction: Sentry.Transaction
let websocketSpan: SpanPromise
let mediaTrackSpan: SpanPromise
let dataChannelSpan: SpanPromise
let handshakeSpan: SpanPromise
let iceSpan: SpanPromise
if (this.shouldTrace()) {
webrtcMediaTransaction = Sentry.startTransaction({
name: 'webrtc-media',
})
websocketSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'websocket' })
)
}
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
@ -129,6 +167,37 @@ export class EngineConnection {
})
this.websocket.addEventListener('open', (event) => {
if (this.shouldTrace()) {
websocketSpan.resolve?.()
handshakeSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'handshake' })
)
iceSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'ice' })
)
dataChannelSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'data-channel',
})
)
mediaTrackSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'media-track',
})
)
}
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
this.onWebsocketOpen(this)
})
@ -191,6 +260,13 @@ export class EngineConnection {
sdp: answer.sdp,
})
)
if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.resolve?.()
}
}
} else if (resp.type === 'trickle_ice') {
let candidate = resp.data?.candidate
@ -220,9 +296,9 @@ export class EngineConnection {
// PeerConnection and waiting for events to fire our callbacks.
this.pc.addEventListener('connectionstatechange', (event) => {
// if (this.pc?.iceConnectionState === 'disconnected') {
// this.close()
// }
if (this.pc?.iceConnectionState === 'connected') {
iceSpan.resolve?.()
}
})
this.pc.addEventListener('icecandidate', (event) => {
@ -272,8 +348,142 @@ export class EngineConnection {
})
this.pc.addEventListener('track', (event) => {
console.log('received track', event)
const mediaStream = event.streams[0]
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
// let settings = mediaStreamTrack.getSettings()
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
// mediaTrackSpan.span.setTag("width", settings.width)
// mediaTrackSpan.span.setTag("height", settings.height)
mediaTrackSpan.resolve?.()
})
}
// Set up the background thread to keep an eye on statistical
// information about the WebRTC media stream from the server to
// us. We'll also eventually want more global statistical information,
// but this will give us a baseline.
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
setInterval(() => {
if (this.pc === undefined) {
return
}
if (!this.shouldTrace()) {
return
}
// Use the WebRTC Statistics API to collect statistical information
// about the WebRTC connection we're using to report to Sentry.
mediaStream.getVideoTracks().forEach((videoTrack) => {
let trackStats = new Map<string, any>()
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// Sentry only allows 10 metrics per transaction. We're going
// to have to pick carefully here, eventually send like a prom
// file or something to the peer.
const transaction = Sentry.startTransaction({
name: 'webrtc-stats',
})
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
// RTC Stream Info
// transaction.setMeasurement(
// 'mediaStreamTrack.framesDecoded',
// videoTrackReport.framesDecoded,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesDropped',
videoTrackReport.framesDropped,
''
)
// transaction.setMeasurement(
// 'mediaStreamTrack.framesReceived',
// videoTrackReport.framesReceived,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesPerSecond',
videoTrackReport.framesPerSecond,
'fps'
)
transaction.setMeasurement(
'rtcFreezeCount',
videoTrackReport.freezeCount,
''
)
transaction.setMeasurement(
'rtcJitter',
videoTrackReport.jitter,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferDelay',
// videoTrackReport.jitterBufferDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferEmittedCount',
// videoTrackReport.jitterBufferEmittedCount,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferMinimumDelay',
// videoTrackReport.jitterBufferMinimumDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferTargetDelay',
// videoTrackReport.jitterBufferTargetDelay,
// ''
// )
transaction.setMeasurement(
'rtcKeyFramesDecoded',
videoTrackReport.keyFramesDecoded,
''
)
transaction.setMeasurement(
'rtcTotalFreezesDuration',
videoTrackReport.totalFreezesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalInterFrameDelay',
// videoTrackReport.totalInterFrameDelay,
// ''
// )
transaction.setMeasurement(
'rtcTotalPausesDuration',
videoTrackReport.totalPausesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalProcessingDelay',
// videoTrackReport.totalProcessingDelay,
// 'second'
// )
} else if (videoTrackReport.type === 'transport') {
// // Bytes i/o
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesReceived',
// videoTrackReport.bytesReceived,
// 'byte'
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesSent',
// videoTrackReport.bytesSent,
// 'byte'
// )
}
})
transaction?.finish()
})
})
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
}
this.onNewTrack({
conn: this,
mediaStream: mediaStream,
@ -285,45 +495,48 @@ export class EngineConnection {
let connectionStarted = new Date()
this.pc.addEventListener('datachannel', (event) => {
this.lossyDataChannel = event.channel
this.unreliableDataChannel = event.channel
console.log('accepted lossy data channel', event.channel.label)
this.lossyDataChannel.addEventListener('open', (event) => {
console.log('lossy data channel opened', event)
console.log('accepted unreliable data channel', event.channel.label)
this.unreliableDataChannel.addEventListener('open', (event) => {
console.log('unreliable data channel opened', event)
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
this.onDataChannelOpen(this)
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
this.onEngineConnectionOpen(this)
this.ready = true
})
this.lossyDataChannel.addEventListener('close', (event) => {
console.log('lossy data channel closed')
this.unreliableDataChannel.addEventListener('close', (event) => {
console.log('unreliable data channel closed')
this.close()
})
this.lossyDataChannel.addEventListener('error', (event) => {
console.log('lossy data channel error')
this.unreliableDataChannel.addEventListener('error', (event) => {
console.log('unreliable data channel error')
this.close()
})
})
this.onConnectionStarted(this)
}
send(message: object) {
send(message: object | string) {
// TODO(paultag): Add in logic to determine the connection state and
// take actions if needed?
this.websocket?.send(JSON.stringify(message))
this.websocket?.send(
typeof message === 'string' ? message : JSON.stringify(message)
)
}
close() {
this.websocket?.close()
this.pc?.close()
this.lossyDataChannel?.close()
this.unreliableDataChannel?.close()
this.websocket = undefined
this.pc = undefined
this.lossyDataChannel = undefined
this.unreliableDataChannel = undefined
this.onClose(this)
this.ready = false
@ -331,6 +544,23 @@ export class EngineConnection {
}
export type EngineCommand = Models['WebSocketRequest_type']
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
type UnreliableResponses = Extract<
Models['OkModelingCmdResponse_type'],
{ type: 'highlight_set_entity' }
>
interface UnreliableSubscription<T extends UnreliableResponses['type']> {
event: T
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
}
interface Subscription<T extends ModelTypes> {
event: T
callback: (
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
) => void
}
export class EngineCommandManager {
artifactMap: ArtifactMap = {}
@ -340,10 +570,17 @@ export class EngineCommandManager {
engineConnection?: EngineConnection
waitForReady: Promise<void> = new Promise(() => {})
private resolveReady = () => {}
onHoverCallback: (id?: string) => void = () => {}
onClickCallback: (selection?: SelectionsArgs) => void = () => {}
onCursorsSelectedCallback: (selections: CursorSelectionsArgs) => void =
() => {}
subscriptions: {
[event: string]: {
[localUnsubscribeId: string]: (a: any) => void
}
} = {} as any
unreliableSubscriptions: {
[event: string]: {
[localUnsubscribeId: string]: (a: any) => void
}
} = {} as any
constructor({
setMediaStream,
setIsStreamReady,
@ -373,20 +610,28 @@ export class EngineCommandManager {
},
onConnectionStarted: (engineConnection) => {
engineConnection?.pc?.addEventListener('datachannel', (event) => {
let lossyDataChannel = event.channel
let unreliableDataChannel = event.channel
lossyDataChannel.addEventListener('message', (event) => {
const result: Models['OkModelingCmdResponse_type'] = JSON.parse(
event.data
unreliableDataChannel.addEventListener('message', (event) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
if (
result?.data?.sequence &&
result?.data.sequence > this.inSequence &&
result.type === 'highlight_set_entity'
) {
this.inSequence = result.data.sequence
callback(result)
}
}
)
if (
result.type === 'highlight_set_entity' &&
result?.data?.sequence &&
result.data.sequence > this.inSequence
) {
this.onHoverCallback(result.data.entity_id)
this.inSequence = result.data.sequence
}
})
})
@ -418,8 +663,8 @@ export class EngineCommandManager {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.log('peer is not sending video to us')
this.engineConnection?.close()
this.engineConnection?.connect()
// this.engineConnection?.close()
// this.engineConnection?.connect()
})
setMediaStream(mediaStream)
@ -433,18 +678,11 @@ export class EngineCommandManager {
return
}
const modelingResponse = message.data.modeling_response
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
(callback) => callback(modelingResponse)
)
const command = this.artifactMap[id]
if (modelingResponse.type === 'select_with_point') {
if (modelingResponse?.data?.entity_id) {
this.onClickCallback({
id: modelingResponse?.data?.entity_id,
type: 'default',
})
} else {
this.onClickCallback()
}
}
if (command && command.type === 'pending') {
const resolve = command.resolve
this.artifactMap[id] = {
@ -453,6 +691,7 @@ export class EngineCommandManager {
}
resolve({
id,
data: modelingResponse,
})
} else {
this.artifactMap[id] = {
@ -468,21 +707,49 @@ export class EngineCommandManager {
this.artifactMap = {}
this.sourceRangeMap = {}
}
subscribeTo<T extends ModelTypes>({
event,
callback,
}: Subscription<T>): () => void {
const localUnsubscribeId = uuidv4()
const otherEventCallbacks = this.subscriptions[event]
if (otherEventCallbacks) {
otherEventCallbacks[localUnsubscribeId] = callback
} else {
this.subscriptions[event] = {
[localUnsubscribeId]: callback,
}
}
return () => this.unSubscribeTo(event, localUnsubscribeId)
}
private unSubscribeTo(event: ModelTypes, id: string) {
delete this.subscriptions[event][id]
}
subscribeToUnreliable<T extends UnreliableResponses['type']>({
event,
callback,
}: UnreliableSubscription<T>): () => void {
const localUnsubscribeId = uuidv4()
const otherEventCallbacks = this.unreliableSubscriptions[event]
if (otherEventCallbacks) {
otherEventCallbacks[localUnsubscribeId] = callback
} else {
this.unreliableSubscriptions[event] = {
[localUnsubscribeId]: callback,
}
}
return () => this.unSubscribeToUnreliable(event, localUnsubscribeId)
}
private unSubscribeToUnreliable(
event: UnreliableResponses['type'],
id: string
) {
delete this.unreliableSubscriptions[event][id]
}
endSession() {
// this.websocket?.close()
// socket.off('command')
}
onHover(callback: (id?: string) => void) {
// It's when the user hovers over a part in the 3d scene, and so the engine should tell the
// frontend about that (with it's id) so that the FE can highlight code associated with that id
this.onHoverCallback = callback
}
onClick(callback: (selection?: SelectionsArgs) => void) {
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
// line of code
this.onClickCallback = callback
}
cusorsSelected(selections: {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
@ -507,32 +774,38 @@ export class EngineCommandManager {
cmd_id: uuidv4(),
})
}
sendSceneCommand(command: EngineCommand) {
sendSceneCommand(command: EngineCommand): Promise<any> {
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return
return Promise.resolve()
}
if (command.type !== 'modeling_cmd_req') return
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd
if (
cmd.type === 'camera_drag_move' &&
this.engineConnection?.lossyDataChannel
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
return
this.engineConnection?.unreliableDataChannel?.send(
JSON.stringify(command)
)
return Promise.resolve()
} else if (
cmd.type === 'highlight_set_entity' &&
this.engineConnection?.lossyDataChannel
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
return
this.engineConnection?.unreliableDataChannel?.send(
JSON.stringify(command)
)
return Promise.resolve()
}
console.log('sending command', command)
// since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id)
}
sendModelingCommand({
id,
@ -541,15 +814,18 @@ export class EngineCommandManager {
}: {
id: string
range: SourceRange
command: EngineCommand
command: EngineCommand | string
}): Promise<any> {
this.sourceRangeMap[id] = range
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return new Promise(() => {})
return Promise.resolve()
}
this.engineConnection?.send(command)
return this.handlePendingCommand(id)
}
handlePendingCommand(id: string) {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
@ -575,10 +851,9 @@ export class EngineCommandManager {
if (commandStr === undefined) {
throw new Error('commandStr is undefined')
}
const command: EngineCommand = JSON.parse(commandStr)
const range: SourceRange = JSON.parse(rangeStr)
return this.sendModelingCommand({ id, range, command })
return this.sendModelingCommand({ id, range, command: commandStr })
}
commandResult(id: string): Promise<any> {
const command = this.artifactMap[id]

View File

@ -73,8 +73,6 @@ export function createMachineCommand<T extends AnyStateMachine>({
arg.defaultValue as keyof typeof state.context
] as string | undefined
console.log(arg.name, { defaultValueFromContext })
const options =
arg.options instanceof Array
? arg.options.map((o) => ({

View File

@ -4,17 +4,18 @@ export enum Themes {
System = 'system',
}
// Get the theme from the system settings manually
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof window !== 'undefined' &&
'matchMedia' in window &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark
return typeof window !== 'undefined' && 'matchMedia' in window
? window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark
: Themes.Light
: Themes.Light
}
// Set the theme class on the body element
export function setThemeClass(theme: Themes) {
const systemTheme = theme === Themes.System && getSystemTheme()
if (theme === Themes.Dark || systemTheme === Themes.Dark) {
if (theme === Themes.Dark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')

View File

@ -3,6 +3,23 @@ import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL'
import { CommandBarMeta } from '../lib/commands'
const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
const LOCAL_USER: Models['User_type'] = {
id: '8675309',
name: 'Test User',
email: 'kittycad.sidebar.test@example.com',
image: 'https://placekitten.com/200/200',
created_at: 'yesteryear',
updated_at: 'today',
company: 'Test Company',
discord: 'Test User#1234',
github: 'testuser',
phone: '555-555-5555',
first_name: 'Test',
last_name: 'User',
}
export interface UserContext {
user?: Models['User_type']
token?: string
@ -81,7 +98,9 @@ export const authMachine = createMachine<UserContext, Events>(
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
predictableActionArguments: true,
preserveActionOrder: true,
context: { token: persistedToken },
context: {
token: persistedToken,
},
},
{
actions: {},
@ -98,12 +117,17 @@ async function getUser(context: UserContext) {
}
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
if (SKIP_AUTH) return LOCAL_USER
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
} catch (e) {
console.error(e)
}
}

View File

@ -1,7 +1,12 @@
import { assign, createMachine } from 'xstate'
import { BaseUnit, baseUnitsUnion } from '../useStore'
import { CommandBarMeta } from '../lib/commands'
import { Themes } from '../lib/theme'
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
export enum UnitSystem {
Imperial = 'imperial',
Metric = 'metric',
}
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
@ -42,7 +47,7 @@ export const settingsCommandBarMeta: CommandBarMeta = {
name: 'unitSystem',
type: 'select',
defaultValue: 'unitSystem',
options: [{ name: 'imperial' }, { name: 'metric' }],
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
},
],
},
@ -70,7 +75,7 @@ export const settingsMachine = createMachine(
context: {
theme: Themes.System,
defaultProjectName: '',
unitSystem: 'imperial' as 'imperial' | 'metric',
unitSystem: UnitSystem.Imperial,
baseUnit: 'in' as BaseUnit,
defaultDirectory: '',
showDebugPanel: false,
@ -79,6 +84,7 @@ export const settingsMachine = createMachine(
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass'],
on: {
'Set Theme': {
actions: [
@ -87,6 +93,7 @@ export const settingsMachine = createMachine(
}),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
@ -154,7 +161,6 @@ export const settingsMachine = createMachine(
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
@ -173,7 +179,7 @@ export const settingsMachine = createMachine(
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Unit System'
data: { unitSystem: 'imperial' | 'metric' }
data: { unitSystem: UnitSystem }
}
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
@ -189,6 +195,13 @@ export const settingsMachine = createMachine(
console.error(e)
}
},
setThemeClass: (context, event) => {
const currentTheme =
event.type === 'Set Theme' ? event.data.theme : context.theme
setThemeClass(
currentTheme === Themes.System ? getSystemTheme() : currentTheme
)
},
},
}
)

View File

@ -21,7 +21,7 @@ export interface Typegen0 {
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
toastSuccess:
setThemeClass:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
@ -29,6 +29,14 @@ export interface Typegen0 {
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
| 'xstate.init'
toastSuccess:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
}
eventsCausingDelays: {}
eventsCausingGuards: {}

View File

@ -20,9 +20,25 @@ export default function Units() {
>
<h1 className="text-2xl font-bold">Camera</h1>
<p className="mt-6">
Moving the camera is easy. Just click and drag anywhere in the scene
to rotate the camera, or hold down the <kbd>Ctrl</kbd> key and drag to
pan the camera.
Moving the camera is easy! The controls are as you might expect:
</p>
<ul className="list-disc list-outside ms-8 mb-4">
<li>Click and drag anywhere in the scene to rotate the camera</li>
<li>
Hold down the <kbd>Shift</kbd> key while clicking and dragging to
pan the camera
</li>
<li>
Hold down the <kbd>Ctrl</kbd> key while dragging to zoom. You can
also use the scroll wheel to zoom in and out.
</li>
</ul>
<p>
What you're seeing here is just a video, and your interactions are
being sent to our Geometry Engine API, which sends back video frames
in real time. How cool is that? It means that you can use KittyCAD
Modeling App (or whatever you want to build) on any device, even a
cheap laptop with no graphics card!
</p>
<div className="flex justify-between mt-6">
<ActionButton

View File

@ -3,9 +3,9 @@ import { BaseUnit, baseUnits } from '../../useStore'
import { ActionButton } from '../../components/ActionButton'
import { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle'
import { useState } from 'react'
import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { UnitSystem } from 'machines/settingsMachine'
export default function Units() {
const dismiss = useDismiss()
@ -16,15 +16,6 @@ export default function Units() {
context: { unitSystem, baseUnit },
},
} = useGlobalStateContext()
const [tempUnitSystem, setTempUnitSystem] = useState(unitSystem)
const [tempBaseUnit, setTempBaseUnit] = useState(baseUnit)
function handleNextClick() {
send({ type: 'Set Unit System', data: { unitSystem: tempUnitSystem } })
send({ type: 'Set Base Unit', data: { baseUnit: tempBaseUnit } })
next()
}
return (
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">
@ -38,10 +29,16 @@ export default function Units() {
offLabel="Imperial"
onLabel="Metric"
name="settings-units"
checked={tempUnitSystem === 'metric'}
onChange={(e) =>
setTempUnitSystem(e.target.checked ? 'metric' : 'imperial')
}
checked={unitSystem === UnitSystem.Metric}
onChange={(e) => {
const newUnitSystem = e.target.checked
? UnitSystem.Metric
: UnitSystem.Imperial
send({
type: 'Set Unit System',
data: { unitSystem: newUnitSystem },
})
}}
/>
</SettingsSection>
<SettingsSection
@ -51,8 +48,13 @@ export default function Units() {
<select
id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={tempBaseUnit}
onChange={(e) => setTempBaseUnit(e.target.value as BaseUnit)}
value={baseUnit}
onChange={(e) => {
send({
type: 'Set Base Unit',
data: { baseUnit: e.target.value as BaseUnit },
})
}}
>
{baseUnits[unitSystem].map((unit) => (
<option key={unit} value={unit}>
@ -77,7 +79,7 @@ export default function Units() {
</ActionButton>
<ActionButton
Element="button"
onClick={handleNextClick}
onClick={next}
icon={{ icon: faArrowRight }}
>
Next: Camera

View File

@ -8,15 +8,17 @@ import { AppHeader } from '../components/AppHeader'
import { open } from '@tauri-apps/api/dialog'
import { BaseUnit, baseUnits } from '../useStore'
import { Toggle } from '../components/Toggle/Toggle'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook'
import { IndexLoaderData, paths } from '../Router'
import { Themes } from '../lib/theme'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { UnitSystem } from 'machines/settingsMachine'
export const Settings = () => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const navigate = useNavigate()
const location = useLocation()
useHotkeys('esc', () => navigate('../'))
const {
settings: {
@ -135,9 +137,11 @@ export const Settings = () => {
offLabel="Imperial"
onLabel="Metric"
name="settings-units"
checked={unitSystem === 'metric'}
checked={unitSystem === UnitSystem.Metric}
onChange={(e) => {
const newUnitSystem = e.target.checked ? 'metric' : 'imperial'
const newUnitSystem = e.target.checked
? UnitSystem.Metric
: UnitSystem.Imperial
send({
type: 'Set Unit System',
data: { unitSystem: newUnitSystem },
@ -201,24 +205,26 @@ export const Settings = () => {
))}
</select>
</SettingsSection>
<SettingsSection
title="Onboarding"
description="Replay the onboarding process"
>
<ActionButton
Element="button"
onClick={() => {
send({
type: 'Set Onboarding Status',
data: { onboardingStatus: '' },
})
navigate('..' + paths.ONBOARDING.INDEX)
}}
icon={{ icon: faArrowRotateBack }}
{location.pathname.includes(paths.FILE) && (
<SettingsSection
title="Onboarding"
description="Replay the onboarding process"
>
Replay Onboarding
</ActionButton>
</SettingsSection>
<ActionButton
Element="button"
onClick={() => {
send({
type: 'Set Onboarding Status',
data: { onboardingStatus: '' },
})
navigate('..' + paths.ONBOARDING.INDEX)
}}
icon={{ icon: faArrowRotateBack }}
>
Replay Onboarding
</ActionButton>
</SettingsSection>
)}
</div>
</div>
)

View File

@ -29,10 +29,11 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
@ -187,9 +188,9 @@ dependencies = [
[[package]]
name = "bson"
version = "2.6.1"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aeb8bae494e49dbc330dd23cf78f6f7accee22f640ce3ab17841badaa4ce232"
checksum = "58da0ae1e701ea752cc46c1bb9f39d5ecefc7395c3ecd526261a566d4f16e0c2"
dependencies = [
"ahash",
"base64 0.13.1",
@ -198,7 +199,7 @@ dependencies = [
"hex",
"indexmap 1.9.3",
"js-sys",
"lazy_static",
"once_cell",
"rand",
"serde",
"serde_bytes",
@ -413,6 +414,20 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "derive-docs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "075291fd1d6d70a886078f7b1c132a160559ceb9a0fe143177872d40ea587906"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"serde",
"serde_tokenstream",
"syn 2.0.29",
]
[[package]]
name = "diff"
version = "0.1.13"
@ -932,12 +947,12 @@ dependencies = [
]
[[package]]
name = "kcl"
version = "0.1.0"
name = "kcl-lib"
version = "0.1.10"
dependencies = [
"anyhow",
"bson",
"derive-docs",
"derive-docs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"expectorate",
"futures",
"js-sys",
@ -953,7 +968,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-tungstenite",
"ts-rs",
"ts-rs-json-value",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -961,9 +976,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.2.20"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e71916b50966110cb9f70aa6c310748a153fdcb0183a02615324a6f457fd18e8"
checksum = "b8b33e5df8f82b97e5f5af94ff1400ae37449d0f5f1bb79acedf17cf2193680f"
dependencies = [
"anyhow",
"base64 0.21.2",
@ -975,6 +990,7 @@ dependencies = [
"phonenumber",
"schemars",
"serde",
"serde_bytes",
"serde_json",
"thiserror",
"url",
@ -1696,9 +1712,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.12"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
dependencies = [
"bigdecimal",
"bytes",
@ -1713,9 +1729,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.12"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
dependencies = [
"proc-macro2",
"quote",
@ -2346,9 +2362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "ts-rs"
name = "ts-rs-json-value"
version = "7.0.0"
source = "git+https://github.com/kittycad/ts-rs.git?branch=serde_json#94e2a19c41194e47009fafc7b5a2c28ae544a6e8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66d07e64e1e39d693819307757ad16878ff2be1f26d6fc2137c4e23bc0c0545"
dependencies = [
"serde_json",
"thiserror",
@ -2359,7 +2376,8 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
version = "7.0.0"
source = "git+https://github.com/kittycad/ts-rs.git?branch=serde_json#94e2a19c41194e47009fafc7b5a2c28ae544a6e8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2"
dependencies = [
"Inflector",
"proc-macro2",
@ -2592,7 +2610,7 @@ version = "0.1.0"
dependencies = [
"bson",
"gloo-utils",
"kcl",
"kcl-lib",
"kittycad",
"serde_json",
"wasm-bindgen",

View File

@ -8,10 +8,10 @@ edition = "2021"
crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.6.1", features = ["uuid-1", "chrono"] }
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0"
kcl = { path = "kcl" }
kittycad = { version = "0.2.15", default-features = false, features = ["js"] }
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
serde_json = "1.0.93"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"

View File

@ -3,6 +3,7 @@ name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -35,10 +35,7 @@ struct StdlibMetadata {
}
#[proc_macro_attribute]
pub fn stdlib(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
@ -50,9 +47,7 @@ fn do_stdlib(
do_stdlib_inner(metadata, attr, item)
}
fn do_output(
res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>,
) -> proc_macro::TokenStream {
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => {
@ -207,11 +202,7 @@ fn do_stdlib_inner(
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let ty_string = ty
.to_string()
.replace('&', "")
.replace("mut", "")
.replace(' ', "");
let ty_string = ty.to_string().replace('&', "").replace("mut", "").replace(' ', "");
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
@ -370,8 +361,7 @@ fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> (Option<String>, Option<S
if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
lit: syn::Lit::Str(s), ..
}) = &nv.value
{
return normalize_comment_string(s.value());
@ -508,10 +498,7 @@ mod tests {
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/lineTo.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
expectorate::assert_contents("tests/lineTo.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
#[test]
@ -540,9 +527,6 @@ mod tests {
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/min.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
expectorate::assert_contents("tests/min.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
}

View File

@ -1,15 +1,16 @@
[package]
name = "kcl"
name = "kcl-lib"
description = "KittyCAD Language"
version = "0.1.0"
version = "0.1.10"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
derive-docs = { path = "../derive-docs" }
kittycad = { version = "0.2.15", default-features = false, features = ["js"] }
derive-docs = { version = "0.1.0" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"
@ -17,7 +18,7 @@ schemars = { version = "0.8", features = ["url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.47"
ts-rs = { git = "https://github.com/kittycad/ts-rs.git", branch = "serde_json", features = ["serde-json-impl", "uuid-impl"] }
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
@ -26,12 +27,16 @@ wasm-bindgen-futures = "0.4.37"
js-sys = { version = "0.3.64" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bson = { version = "2.6.1", features = ["uuid-1", "chrono"] }
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
futures = { version = "0.3.28" }
reqwest = { version = "0.11.20", default-features = false }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] }
[features]
default = ["engine"]
engine = []
[profile.release]
panic = "abort"
debug = true

View File

@ -2,6 +2,7 @@
use std::collections::HashMap;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Map;
@ -56,7 +57,7 @@ pub trait ValueMeta {
macro_rules! impl_value_meta {
{$name:ident} => {
impl ValueMeta for $name {
impl crate::abstract_syntax_tree_types::ValueMeta for $name {
fn start(&self) -> usize {
self.start
}
@ -86,6 +87,8 @@ macro_rules! impl_value_meta {
};
}
pub(crate) use impl_value_meta;
impl Value {
pub fn start(&self) -> usize {
match self {
@ -192,16 +195,11 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine)
}
BinaryPart::CallExpression(call_expression) => {
call_expression.execute(memory, pipe_info, stdlib, engine)
}
BinaryPart::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, stdlib, engine),
BinaryPart::UnaryExpression(unary_expression) => {
// Return an error this should not happen.
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"UnaryExpression should not be a BinaryPart: {:?}",
unary_expression
),
message: format!("UnaryExpression should not be a BinaryPart: {:?}", unary_expression),
source_ranges: vec![unary_expression.into()],
}))
}
@ -313,10 +311,7 @@ impl CallExpression {
}
Value::PipeExpression(pipe_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"PipeExpression not implemented here: {:?}",
pipe_expression
),
message: format!("PipeExpression not implemented here: {:?}", pipe_expression),
source_ranges: vec![pipe_expression.into()],
}));
}
@ -325,29 +320,20 @@ impl CallExpression {
.get(&pipe_info.index - 1)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"PipeSubstitution index out of bounds: {:?}",
pipe_info
),
message: format!("PipeSubstitution index out of bounds: {:?}", pipe_info),
source_ranges: vec![pipe_substitution.into()],
})
})?
.clone(),
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"MemberExpression not implemented here: {:?}",
member_expression
),
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"FunctionExpression not implemented here: {:?}",
function_expression
),
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
@ -363,14 +349,7 @@ impl CallExpression {
if pipe_info.is_in_pipe {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(
memory,
&pipe_info.body.clone(),
pipe_info,
self.into(),
stdlib,
engine,
)
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
} else {
Ok(result)
}
@ -390,14 +369,7 @@ impl CallExpression {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(
memory,
&pipe_info.body.clone(),
pipe_info,
self.into(),
stdlib,
engine,
)
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
} else {
Ok(result)
}
@ -412,11 +384,22 @@ pub struct VariableDeclaration {
pub start: usize,
pub end: usize,
pub declarations: Vec<VariableDeclarator>,
pub kind: String, // Change to enum if there are specific values
pub kind: VariableKind, // Change to enum if there are specific values
}
impl_value_meta!(VariableDeclaration);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum VariableKind {
Let,
Const,
Fn,
Var,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -533,28 +516,19 @@ impl ArrayExpression {
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"PipeSubstitution not implemented here: {:?}",
pipe_substitution
),
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"MemberExpression not implemented here: {:?}",
member_expression
),
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"FunctionExpression not implemented here: {:?}",
function_expression
),
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
@ -619,28 +593,19 @@ impl ObjectExpression {
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"PipeSubstitution not implemented here: {:?}",
pipe_substitution
),
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"MemberExpression not implemented here: {:?}",
member_expression
),
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"FunctionExpression not implemented here: {:?}",
function_expression
),
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
@ -712,10 +677,7 @@ impl MemberExpression {
string
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Expected string literal for property name, found {:?}",
value
),
message: format!("Expected string literal for property name, found {:?}", value),
source_ranges: vec![literal.into()],
}));
}
@ -837,10 +799,7 @@ impl BinaryExpression {
}
}
pub fn parse_json_number_as_f64(
j: &serde_json::Value,
source_range: SourceRange,
) -> Result<f64, KclError> {
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {

View File

@ -44,6 +44,11 @@ impl StdLibFnArg {
get_type_string_from_schema(&self.schema)
}
#[allow(dead_code)]
pub fn get_autocomplete_string(&self) -> Result<String> {
get_autocomplete_string_from_schema(&self.schema)
}
#[allow(dead_code)]
pub fn description(&self) -> Option<String> {
get_description_string_from_schema(&self.schema)
@ -93,9 +98,24 @@ pub trait StdLibFn {
deprecated: self.deprecated(),
})
}
fn fn_signature(&self) -> String {
let mut signature = String::new();
signature.push_str(&format!("{}(", self.name()));
for (i, arg) in self.args().iter().enumerate() {
if i > 0 {
signature.push_str(", ");
}
signature.push_str(&format!("{}: {}", arg.name, arg.type_));
}
signature.push_str(") -> ");
signature.push_str(&self.return_value().type_);
signature
}
}
fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
pub fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
if let schemars::schema::Schema::Object(o) = schema {
if let Some(metadata) = &o.metadata {
if let Some(description) = &metadata.description {
@ -107,7 +127,7 @@ fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Opti
None
}
fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> {
pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> {
match schema {
schemars::schema::Schema::Object(o) => {
if let Some(format) = &o.format {
@ -147,15 +167,9 @@ fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(Str
if let Some(array_val) = &o.array {
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties.
return Ok((
format!("[{}]", get_type_string_from_schema(items)?.0),
false,
));
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
} else if let Some(items) = &array_val.contains {
return Ok((
format!("[{}]", get_type_string_from_schema(items)?.0),
false,
));
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
}
}
@ -193,3 +207,78 @@ fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(Str
schemars::schema::Schema::Bool(_) => Ok((Primitive::Bool.to_string(), false)),
}
}
pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) -> Result<String> {
match schema {
schemars::schema::Schema::Object(o) => {
if let Some(format) = &o.format {
if format == "uuid" {
return Ok(Primitive::Uuid.to_string());
} else if format == "double" || format == "uint" {
return Ok(Primitive::Number.to_string());
} else {
anyhow::bail!("unknown format: {}", format);
}
}
if let Some(obj_val) = &o.object {
let mut fn_docs = String::new();
fn_docs.push_str("{\n");
// Let's print out the object's properties.
for (prop_name, prop) in obj_val.properties.iter() {
if prop_name.starts_with('_') {
continue;
}
if let Some(description) = get_description_string_from_schema(prop) {
fn_docs.push_str(&format!("\t// {}\n", description));
}
fn_docs.push_str(&format!(
"\t\"{}\": {},\n",
prop_name,
get_autocomplete_string_from_schema(prop)?,
));
}
fn_docs.push('}');
return Ok(fn_docs);
}
if let Some(array_val) = &o.array {
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties.
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
} else if let Some(items) = &array_val.contains {
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
}
}
if let Some(subschemas) = &o.subschemas {
let mut fn_docs = String::new();
if let Some(items) = &subschemas.one_of {
if let Some(item) = items.iter().next() {
// Let's print out the object's properties.
fn_docs.push_str(&get_autocomplete_string_from_schema(item)?);
}
} else if let Some(items) = &subschemas.any_of {
if let Some(item) = items.iter().next() {
// Let's print out the object's properties.
fn_docs.push_str(&get_autocomplete_string_from_schema(item)?);
}
} else {
anyhow::bail!("unknown subschemas: {:#?}", subschemas);
}
return Ok(fn_docs);
}
if let Some(schemars::schema::SingleOrVec::Single(_string)) = &o.instance_type {
return Ok(Primitive::String.to_string());
}
anyhow::bail!("unknown type: {:#?}", o)
}
schemars::schema::Schema::Bool(_) => Ok(Primitive::Bool.to_string()),
}
}

View File

@ -12,10 +12,10 @@ use crate::errors::{KclError, KclErrorDetails};
#[derive(Debug)]
pub struct EngineConnection {
tcp_write:
futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>,
tcp_write: futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>,
tcp_read_handle: tokio::task::JoinHandle<Result<()>>,
export_notifier: Arc<tokio::sync::Notify>,
snapshot_notifier: Arc<tokio::sync::Notify>,
}
impl Drop for EngineConnection {
@ -42,7 +42,7 @@ impl TcpRead {
}
impl EngineConnection {
pub async fn new(ws: reqwest::Upgraded, export_dir: &str) -> Result<EngineConnection> {
pub async fn new(ws: reqwest::Upgraded, export_dir: &str, snapshot_file: &str) -> Result<EngineConnection> {
// Make sure the export directory exists and that it is a directory.
let export_dir = std::path::Path::new(export_dir).to_owned();
if !export_dir.exists() {
@ -50,10 +50,7 @@ impl EngineConnection {
}
// Make sure it is a directory.
if !export_dir.is_dir() {
anyhow::bail!(
"Export directory is not a directory: {}",
export_dir.display()
);
anyhow::bail!("Export directory is not a directory: {}", export_dir.display());
}
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
@ -70,15 +67,22 @@ impl EngineConnection {
let export_notifier = Arc::new(tokio::sync::Notify::new());
let export_notifier_clone = export_notifier.clone();
let snapshot_notifier = Arc::new(tokio::sync::Notify::new());
let snapshot_notifier_clone = snapshot_notifier.clone();
let snapshot_file = snapshot_file.to_owned();
let tcp_read_handle = tokio::spawn(async move {
// Get Websocket messages from API server
loop {
match tcp_read.read().await {
Ok(ws_resp) => {
if !ws_resp.success {
println!("got ws errors: {:?}", ws_resp.errors);
export_notifier.notify_one();
continue;
if let Some(success) = ws_resp.success {
if !success {
println!("got ws errors: {:?}", ws_resp.errors);
export_notifier.notify_one();
continue;
}
}
if let Some(msg) = ws_resp.resp {
@ -92,7 +96,20 @@ impl EngineConnection {
OkWebSocketResponseData::TrickleIce { candidate } => {
println!("got trickle ice: {:?}", candidate);
}
OkWebSocketResponseData::Modeling { .. } => {}
OkWebSocketResponseData::Modeling { modeling_response } => {
if let kittycad::types::OkModelingCmdResponse::TakeSnapshot { data } =
modeling_response
{
if snapshot_file.is_empty() {
println!("Got snapshot, but no snapshot file specified.");
continue;
}
// Save the snapshot locally.
std::fs::write(&snapshot_file, data.contents)?;
snapshot_notifier.notify_one();
}
}
OkWebSocketResponseData::Export { files } => {
// Save the files to our export directory.
for file in files {
@ -120,13 +137,18 @@ impl EngineConnection {
tcp_write,
tcp_read_handle,
export_notifier: export_notifier_clone,
snapshot_notifier: snapshot_notifier_clone,
})
}
pub async fn wait_for_files(&self) {
pub async fn wait_for_export(&self) {
self.export_notifier.notified().await;
}
pub async fn wait_for_snapshot(&self) {
self.snapshot_notifier.notified().await;
}
pub async fn tcp_send(&mut self, msg: WebSocketRequest) -> Result<()> {
let msg = serde_json::to_string(&msg)?;
self.tcp_write.send(WsMsg::Text(msg)).await?;
@ -140,72 +162,14 @@ impl EngineConnection {
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
futures::executor::block_on(
self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id }),
)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::executor::{execute, BodyType, ProgramMemory, SourceRange};
pub async fn parse_execute_export(code: &str) -> Result<()> {
let tokens = crate::tokeniser::lexer(code);
let export_dir_str = "/tmp/";
let program = crate::parser::abstract_syntax_tree(&tokens)?;
let mut mem: ProgramMemory = Default::default();
let mut engine = EngineConnection::new(
"wss://api.dev.kittycad.io/ws/modeling/commands?webrtc=false",
std::env::var("KITTYCAD_API_TOKEN").unwrap().as_str(),
"modeling-app-tests",
export_dir_str,
)
.await?;
let _ = execute(program, &mut mem, BodyType::Root, &mut engine)?;
// Send an export request to the engine.
engine.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
kittycad::types::ModelingCmd::Export {
entity_ids: vec![],
format: kittycad::types::OutputFormat::Gltf {
presentation: kittycad::types::Presentation::Pretty,
storage: kittycad::types::Storage::Embedded,
},
futures::executor::block_on(self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id })).map_err(
|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
},
)?;
engine.wait_for_files().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_export_file() {
let ast = r#"const part001 = startSketchAt([-0.01, -0.06])
|> line([0, 20], %)
|> line([5, 0], %)
|> line([-10, 5], %)
|> close(%)
|> extrude(4, %)
show(part001)"#;
parse_execute_export(ast).await.unwrap();
// Ensure we have a file called "part001.gltf" in the export directory.
let export_dir = std::path::Path::new("/tmp/");
let export_file = export_dir.join("part001.gltf");
assert!(export_file.exists());
// Make sure the file is not empty.
assert!(export_file.metadata().unwrap().len() != 0);
}
}

View File

@ -4,16 +4,20 @@ use wasm_bindgen::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))]
#[cfg(feature = "engine")]
pub mod conn;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))]
#[cfg(feature = "engine")]
pub use conn::EngineConnection;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[cfg(feature = "engine")]
pub mod conn_wasm;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[cfg(feature = "engine")]
pub use conn_wasm::EngineConnection;
#[cfg(test)]
@ -21,6 +25,13 @@ pub mod conn_mock;
#[cfg(test)]
pub use conn_mock::EngineConnection;
#[cfg(not(feature = "engine"))]
#[cfg(not(test))]
pub mod conn_mock;
#[cfg(not(feature = "engine"))]
#[cfg(not(test))]
pub use conn_mock::EngineConnection;
use crate::executor::SourceRange;
#[derive(Debug)]
@ -33,6 +44,7 @@ pub struct EngineManager {
impl EngineManager {
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[cfg(feature = "engine")]
#[wasm_bindgen(constructor)]
pub async fn new(manager: conn_wasm::EngineCommandManager) -> EngineManager {
EngineManager {

View File

@ -20,7 +20,7 @@ pub enum KclError {
#[error("undefined value: {0:?}")]
UndefinedValue(KclErrorDetails),
#[error("invalid expression: {0:?}")]
InvalidExpression(crate::math_parser::MathExpression),
InvalidExpression(KclErrorDetails),
#[error("engine: {0:?}")]
Engine(KclErrorDetails),
}
@ -34,6 +34,35 @@ pub struct KclErrorDetails {
pub message: String,
}
impl KclError {
/// Get the error message, line and column from the error and input code.
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
let (type_, source_range, message) = match &self {
KclError::Syntax(e) => ("syntax", e.source_ranges.clone(), e.message.clone()),
KclError::Semantic(e) => ("semantic", e.source_ranges.clone(), e.message.clone()),
KclError::Type(e) => ("type", e.source_ranges.clone(), e.message.clone()),
KclError::Unimplemented(e) => ("unimplemented", e.source_ranges.clone(), e.message.clone()),
KclError::Unexpected(e) => ("unexpected", e.source_ranges.clone(), e.message.clone()),
KclError::ValueAlreadyDefined(e) => ("value already defined", e.source_ranges.clone(), e.message.clone()),
KclError::UndefinedValue(e) => ("undefined value", e.source_ranges.clone(), e.message.clone()),
KclError::InvalidExpression(e) => ("invalid expression", e.source_ranges.clone(), e.message.clone()),
KclError::Engine(e) => ("engine", e.source_ranges.clone(), e.message.clone()),
};
// Calculate the line and column of the error from the source range.
let (line, column) = if let Some(range) = source_range.first() {
let line = input[..range.0[0]].lines().count();
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
(Some(line), Some(column))
} else {
(None, None)
};
(format!("{}: {}", type_, message), line, column)
}
}
/// This is different than to_string() in that it will serialize the Error
/// the struct as JSON so we can deserialize it on the js side.
impl From<KclError> for String {

View File

@ -30,12 +30,7 @@ impl ProgramMemory {
}
/// Add to the program memory.
pub fn add(
&mut self,
key: &str,
value: MemoryItem,
source_range: SourceRange,
) -> Result<(), KclError> {
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
if self.root.get(key).is_some() {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine {}", key),
@ -169,12 +164,7 @@ impl MemoryItem {
memory: &ProgramMemory,
engine: &mut EngineConnection,
) -> Result<Option<ProgramReturn>, KclError> {
if let MemoryItem::Function {
func,
expression,
meta,
} = self
{
if let MemoryItem::Function { func, expression, meta } = self {
if let Some(func) = func {
func(args, memory, expression, meta, engine)
} else {
@ -225,10 +215,7 @@ impl SketchGroup {
if self.start.name == name {
Some(&self.start)
} else {
self.value
.iter()
.find(|p| p.get_name() == name)
.map(|p| p.get_base())
self.value.iter().find(|p| p.get_name() == name).map(|p| p.get_base())
}
}
@ -311,12 +298,24 @@ impl From<[f64; 2]> for Point2d {
}
}
impl From<&[f64; 2]> for Point2d {
fn from(p: &[f64; 2]) -> Self {
Self { x: p[0], y: p[1] }
}
}
impl From<Point2d> for [f64; 2] {
fn from(p: Point2d) -> Self {
[p.x, p.y]
}
}
impl From<Point2d> for kittycad::types::Point2D {
fn from(p: Point2d) -> Self {
Self { x: p.x, y: p.y }
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Point3d {
@ -523,8 +522,7 @@ pub fn execute(
match arg {
Value::Literal(literal) => args.push(literal.into()),
Value::Identifier(identifier) => {
let memory_item =
memory.get(&identifier.name, identifier.into())?;
let memory_item = memory.get(&identifier.name, identifier.into())?;
args.push(memory_item.clone());
}
// We do nothing for the rest.
@ -539,8 +537,7 @@ pub fn execute(
}));
}
memory.return_ =
Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else if let Some(func) = memory.clone().root.get(&fn_name) {
func.call_fn(&args, memory, engine)?;
} else {
@ -566,12 +563,7 @@ pub fn execute(
memory.add(&var_name, value.clone(), source_range)?;
}
Value::BinaryExpression(binary_expression) => {
let result = binary_expression.get_result(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = binary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::FunctionExpression(function_expression) => {
@ -608,41 +600,28 @@ pub fn execute(
)?;
}
Value::CallExpression(call_expression) => {
let result =
call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
let result = call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeExpression(pipe_expression) => {
let result = pipe_expression.get_result(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = pipe_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("pipe substitution not implemented for declaration of variable {}", var_name),
message: format!(
"pipe substitution not implemented for declaration of variable {}",
var_name
),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::ArrayExpression(array_expression) => {
let result = array_expression.execute(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = array_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::ObjectExpression(object_expression) => {
let result = object_expression.execute(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = object_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::MemberExpression(member_expression) => {
@ -650,12 +629,7 @@ pub fn execute(
memory.add(&var_name, result, source_range)?;
}
Value::UnaryExpression(unary_expression) => {
let result = unary_expression.get_result(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = unary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
}
@ -680,9 +654,10 @@ pub fn execute(
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let tokens = crate::tokeniser::lexer(code);
let program = crate::parser::abstract_syntax_tree(&tokens)?;
@ -730,23 +705,13 @@ show(part001)"#,
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()),
memory
.root
.get("intersect")
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!(
serde_json::json!(1.0000000000000002),
memory
.root
.get("intersect")
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
}

View File

@ -1,5 +1,5 @@
pub mod abstract_syntax_tree_types;
mod docs;
pub mod docs;
pub mod engine;
pub mod errors;
pub mod executor;

View File

@ -1,12 +1,13 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::abstract_syntax_tree_types::{
BinaryExpression, BinaryPart, CallExpression, Identifier, Literal,
use crate::{
abstract_syntax_tree_types::{BinaryExpression, BinaryPart, CallExpression, Identifier, Literal, ValueMeta},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
parser::{find_closing_brace, is_not_code_token, make_call_expression},
tokeniser::{Token, TokenType},
};
use crate::errors::{KclError, KclErrorDetails};
use crate::parser::{find_closing_brace, is_not_code_token, make_call_expression};
use crate::tokeniser::{Token, TokenType};
pub fn precedence(operator: &str) -> u8 {
// might be useful for reference to make it match
@ -182,6 +183,8 @@ pub struct ParenthesisToken {
pub end: usize,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ParenthesisToken);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
@ -195,10 +198,12 @@ pub struct ExtendedBinaryExpression {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedBinaryExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ExntendedLiteral {
pub struct ExtendedLiteral {
pub start: usize,
pub end: usize,
pub value: serde_json::Value,
@ -207,11 +212,13 @@ pub struct ExntendedLiteral {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedLiteral);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MathExpression {
ExntendedLiteral(Box<ExntendedLiteral>),
ExtendedLiteral(Box<ExtendedLiteral>),
Identifier(Box<Identifier>),
CallExpression(Box<CallExpression>),
BinaryExpression(Box<BinaryExpression>),
@ -219,6 +226,30 @@ pub enum MathExpression {
ParenthesisToken(Box<ParenthesisToken>),
}
impl MathExpression {
pub fn start(&self) -> usize {
match self {
MathExpression::ExtendedLiteral(literal) => literal.start(),
MathExpression::Identifier(identifier) => identifier.start(),
MathExpression::CallExpression(call_expression) => call_expression.start(),
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.start(),
}
}
pub fn end(&self) -> usize {
match self {
MathExpression::ExtendedLiteral(literal) => literal.end(),
MathExpression::Identifier(identifier) => identifier.end(),
MathExpression::CallExpression(call_expression) => call_expression.end(),
MathExpression::BinaryExpression(binary_expression) => binary_expression.end(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.end(),
}
}
}
fn build_tree(
reverse_polish_notation_tokens: &[Token],
stack: Vec<MathExpression>,
@ -241,80 +272,76 @@ fn build_tree(
}),
a => {
return Err(KclError::InvalidExpression(a.clone()));
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![SourceRange([a.start(), a.end()])],
message: format!("{:?}", a),
}))
}
};
}
let current_token = &reverse_polish_notation_tokens[0];
if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::String
{
if current_token.token_type == TokenType::Number || current_token.token_type == TokenType::String {
let mut new_stack = stack;
new_stack.push(MathExpression::ExntendedLiteral(Box::new(
ExntendedLiteral {
value: if current_token.token_type == TokenType::Number {
if let Ok(value) = current_token.value.parse::<i64>() {
serde_json::Value::Number(value.into())
} else if let Ok(value) = current_token.value.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(value) {
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid float: {}", current_token.value),
}));
}
new_stack.push(MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
value: if current_token.token_type == TokenType::Number {
if let Ok(value) = current_token.value.parse::<i64>() {
serde_json::Value::Number(value.into())
} else if let Ok(value) = current_token.value.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(value) {
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid integer: {}", current_token.value),
message: format!("Invalid float: {}", current_token.value),
}));
}
} else {
let mut str_val = current_token.value.clone();
str_val.remove(0);
str_val.pop();
serde_json::Value::String(str_val)
},
start: current_token.start,
end: current_token.end,
raw: current_token.value.clone(),
end_extended: None,
start_extended: None,
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid integer: {}", current_token.value),
}));
}
} else {
let mut str_val = current_token.value.clone();
str_val.remove(0);
str_val.pop();
serde_json::Value::String(str_val)
},
)));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Word {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "("
{
let closing_brace = find_closing_brace(reverse_polish_notation_tokens, 1, 0, "")?;
let mut new_stack = stack;
new_stack.push(MathExpression::CallExpression(Box::new(
make_call_expression(reverse_polish_notation_tokens, 0)?.expression,
)));
return build_tree(
&reverse_polish_notation_tokens[closing_brace + 1..],
new_stack,
);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(),
start: current_token.start,
end: current_token.end,
raw: current_token.value.clone(),
end_extended: None,
start_extended: None,
})));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let mut new_stack = stack;
new_stack.push(MathExpression::ParenthesisToken(Box::new(
ParenthesisToken {
value: "(".to_string(),
} 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 == "("
{
let closing_brace = find_closing_brace(reverse_polish_notation_tokens, 1, 0, "")?;
let mut new_stack = stack;
new_stack.push(MathExpression::CallExpression(Box::new(
make_call_expression(reverse_polish_notation_tokens, 0)?.expression,
)));
return build_tree(&reverse_polish_notation_tokens[closing_brace + 1..], new_stack);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(),
start: current_token.start,
end: current_token.end,
token_type: MathTokenType::Parenthesis,
},
)));
})));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
}
} else if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let mut new_stack = stack;
new_stack.push(MathExpression::ParenthesisToken(Box::new(ParenthesisToken {
value: "(".to_string(),
start: current_token.start,
end: current_token.end,
token_type: MathTokenType::Parenthesis,
})));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Brace && current_token.value == ")" {
let inner_node: MathExpression = match &stack[stack.len() - 1] {
@ -340,14 +367,22 @@ fn build_tree(
end_extended: None,
}))
}
MathExpression::ExntendedLiteral(literal) => {
MathExpression::ExntendedLiteral(literal.clone())
MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(literal.clone()),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let paran = match &stack[stack.len() - 2] {
MathExpression::ParenthesisToken(paran) => paran.clone(),
a => return Err(KclError::InvalidExpression(a.clone())),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
};
let expression = match inner_node {
MathExpression::ExtendedBinaryExpression(bin_exp) => {
@ -372,22 +407,33 @@ fn build_tree(
end_extended: Some(current_token.end),
}))
}
MathExpression::ExntendedLiteral(literal) => {
MathExpression::ExntendedLiteral(Box::new(ExntendedLiteral {
value: literal.value.clone(),
start: literal.start,
end: literal.end,
raw: literal.raw.clone(),
end_extended: Some(current_token.end),
start_extended: Some(paran.start),
MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
value: literal.value.clone(),
start: literal.start,
end: literal.end,
raw: literal.raw.clone(),
end_extended: Some(current_token.end),
start_extended: Some(paran.start),
})),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let mut new_stack = stack[0..stack.len() - 2].to_vec();
new_stack.push(expression);
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
}
if stack.len() < 2 {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "unexpected end of expression".to_string(),
}));
}
let left: (BinaryPart, usize) = match &stack[stack.len() - 2] {
MathExpression::ExtendedBinaryExpression(bin_exp) => (
BinaryPart::BinaryExpression(Box::new(BinaryExpression {
@ -399,7 +445,7 @@ fn build_tree(
})),
bin_exp.start_extended.unwrap_or(bin_exp.start),
),
MathExpression::ExntendedLiteral(lit) => (
MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(),
start: lit.start,
@ -409,13 +455,14 @@ fn build_tree(
lit.start_extended.unwrap_or(lit.start),
),
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
MathExpression::CallExpression(call) => {
(BinaryPart::CallExpression(call.clone()), call.start)
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
MathExpression::BinaryExpression(bin_exp) => {
(BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start)
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let right = match &stack[stack.len() - 1] {
MathExpression::ExtendedBinaryExpression(bin_exp) => (
@ -428,7 +475,7 @@ fn build_tree(
})),
bin_exp.end_extended.unwrap_or(bin_exp.end),
),
MathExpression::ExntendedLiteral(lit) => (
MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(),
start: lit.start,
@ -438,13 +485,14 @@ fn build_tree(
lit.end_extended.unwrap_or(lit.end),
),
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
MathExpression::CallExpression(call) => {
(BinaryPart::CallExpression(call.clone()), call.end)
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
MathExpression::BinaryExpression(bin_exp) => {
(BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end)
}
a => return Err(KclError::InvalidExpression(a.clone())),
};
let right_end = match right.0.clone() {
@ -458,11 +506,7 @@ fn build_tree(
let tree = BinaryExpression {
operator: current_token.value.clone(),
start: left.1,
end: if right.1 > right_end {
right.1
} else {
right_end
},
end: if right.1 > right_end { right.1 } else { right_end },
left: left.0,
right: right.0,
};
@ -510,9 +554,10 @@ pub fn parse_expression(tokens: &[Token]) -> Result<BinaryExpression, KclError>
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_parse_expression() {
let tokens = crate::tokeniser::lexer("1 + 2");
@ -833,8 +878,7 @@ mod test {
#[test]
fn test_reverse_polish_notation_complex() {
let result =
reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
let result = reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
assert_eq!(
result,
vec![
@ -874,8 +918,7 @@ mod test {
#[test]
fn test_reverse_polish_notation_complex_with_parentheses() {
let result =
reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
let result = reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
assert_eq!(
result,
vec![

View File

@ -1,15 +1,16 @@
use std::collections::HashMap;
use std::{collections::HashMap, str::FromStr};
use crate::abstract_syntax_tree_types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject,
NoneCodeMeta, NoneCodeNode, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression,
PipeSubstitution, Program, ReturnStatement, UnaryExpression, Value, VariableDeclaration,
VariableDeclarator,
use crate::{
abstract_syntax_tree_types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NoneCodeMeta,
NoneCodeNode, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution, Program,
ReturnStatement, UnaryExpression, Value, VariableDeclaration, VariableDeclarator, VariableKind,
},
errors::{KclError, KclErrorDetails},
math_parser::parse_expression,
tokeniser::{Token, TokenType},
};
use crate::errors::{KclError, KclErrorDetails};
use crate::math_parser::parse_expression;
use crate::tokeniser::{Token, TokenType};
fn make_identifier(tokens: &[Token], index: usize) -> Identifier {
let current_token = &tokens[index];
@ -79,10 +80,7 @@ fn make_none_code_node(tokens: &[Token], index: usize) -> (Option<NoneCodeNode>,
find_end_of_non_code_node(tokens, index)
};
let non_code_tokens = tokens[index..end_index].to_vec();
let value = non_code_tokens
.iter()
.map(|t| t.value.clone())
.collect::<String>();
let value = non_code_tokens.iter().map(|t| t.value.clone()).collect::<String>();
let node = NoneCodeNode {
start: current_token.start,
@ -105,11 +103,7 @@ struct TokenReturnWithNonCode {
non_code_node: Option<NoneCodeNode>,
}
fn next_meaningful_token(
tokens: &[Token],
index: usize,
offset: Option<usize>,
) -> TokenReturnWithNonCode {
fn next_meaningful_token(tokens: &[Token], index: usize, offset: Option<usize>) -> TokenReturnWithNonCode {
let new_index = index + offset.unwrap_or(1);
let _token = tokens.get(new_index);
let token = if let Some(token) = _token {
@ -150,11 +144,13 @@ pub fn find_closing_brace(
brace_count: usize,
search_opening_brace: &str,
) -> Result<usize, KclError> {
let closing_brace_map: HashMap<&str, &str> = [("(", ")"), ("{", "}"), ("[", "]")]
.iter()
.cloned()
.collect();
let current_token = &tokens[index];
let closing_brace_map: HashMap<&str, &str> = [("(", ")"), ("{", "}"), ("[", "]")].iter().cloned().collect();
let Some(current_token) = tokens.get(index) else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![tokens.last().unwrap().into()],
message: "unexpected end".to_string(),
}));
};
let mut search_opening_brace = search_opening_brace;
let is_first_call = search_opening_brace.is_empty() && brace_count == 0;
if is_first_call {
@ -169,11 +165,9 @@ pub fn find_closing_brace(
}));
}
}
let found_closing_brace =
brace_count == 1 && current_token.value == closing_brace_map[search_opening_brace];
let found_closing_brace = brace_count == 1 && current_token.value == closing_brace_map[search_opening_brace];
let found_another_opening_brace = current_token.value == search_opening_brace;
let found_another_closing_brace =
current_token.value == closing_brace_map[search_opening_brace];
let found_another_closing_brace = current_token.value == closing_brace_map[search_opening_brace];
if found_closing_brace {
return Ok(index);
}
@ -218,9 +212,7 @@ fn find_next_declaration_keyword(tokens: &[Token], index: usize) -> Result<Token
});
}
if let Some(token_val) = next_token.token {
if token_val.token_type == TokenType::Word
&& (token_val.value == "const" || token_val.value == "fn")
{
if token_val.token_type == TokenType::Word && (token_val.value == "const" || token_val.value == "fn") {
return Ok(TokenReturn {
token: Some(token_val),
index: next_token.index,
@ -275,8 +267,7 @@ fn has_pipe_operator(
let current_token = &tokens[index];
if current_token.token_type == TokenType::Brace && current_token.value == "{" {
let closing_brace_index = find_closing_brace(tokens, index, 0, "")?;
let token_after_closing_brace =
next_meaningful_token(tokens, closing_brace_index, None);
let token_after_closing_brace = next_meaningful_token(tokens, closing_brace_index, None);
if let Some(token_after_closing_brace_val) = token_after_closing_brace.token {
if token_after_closing_brace_val.token_type == TokenType::Operator
&& token_after_closing_brace_val.value == "|>"
@ -393,10 +384,7 @@ pub struct MemberExpressionReturn {
pub last_index: usize,
}
fn make_member_expression(
tokens: &[Token],
index: usize,
) -> Result<MemberExpressionReturn, KclError> {
fn make_member_expression(tokens: &[Token], index: usize) -> Result<MemberExpressionReturn, KclError> {
let current_token = tokens[index].clone();
let mut keys_info = collect_object_keys(tokens, index, None)?;
let last_key = keys_info[keys_info.len() - 1].clone();
@ -464,9 +452,7 @@ fn find_end_of_binary_expression(tokens: &[Token], index: usize) -> Result<usize
}
let maybe_operator = next_meaningful_token(tokens, index, None);
if let Some(maybe_operator_token) = maybe_operator.token {
if maybe_operator_token.token_type != TokenType::Operator
|| maybe_operator_token.value == "|>"
{
if maybe_operator_token.token_type != TokenType::Operator || maybe_operator_token.value == "|>" {
return Ok(index);
}
let next_right = next_meaningful_token(tokens, maybe_operator.index, None);
@ -502,10 +488,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
} else {
return Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!(
"expression with token type {:?}",
current_token.token_type
),
message: format!("expression with token type {:?}", current_token.token_type),
}));
}
}
@ -564,9 +547,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
last_index: index,
});
}
if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::String
{
if current_token.token_type == TokenType::Number || current_token.token_type == TokenType::String {
let literal = make_literal(tokens, index)?;
return Ok(ValueReturn {
value: Value::Literal(Box::new(literal)),
@ -576,9 +557,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let closing_brace_index = find_closing_brace(tokens, index, 0, "")?;
return if let Some(arrow_token) =
next_meaningful_token(tokens, closing_brace_index, None).token
{
return if let Some(arrow_token) = next_meaningful_token(tokens, closing_brace_index, None).token {
if arrow_token.token_type == TokenType::Operator && arrow_token.value == "=>" {
let function_expression = make_function_expression(tokens, index)?;
Ok(ValueReturn {
@ -633,16 +612,12 @@ fn make_array_elements(
let current_element = make_value(tokens, index)?;
let next_token = next_meaningful_token(tokens, current_element.last_index, None);
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_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 {
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 comma or closing brace, found {:?}", next_token_token.value),
}));
}
let next_call_index = if is_closing_brace {
@ -711,10 +686,7 @@ fn make_pipe_body(
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!(
"Expected a pipe value, found {:?}",
current_token.token_type
),
message: format!("Expected a pipe value, found {:?}", current_token.token_type),
}));
}
let next_pipe = has_pipe_operator(tokens, index, None)?;
@ -730,20 +702,13 @@ fn make_pipe_body(
let mut _non_code_meta: NoneCodeMeta;
if let Some(node) = next_pipe.non_code_node {
_non_code_meta = non_code_meta;
_non_code_meta
.none_code_nodes
.insert(previous_values.len(), node);
_non_code_meta.none_code_nodes.insert(previous_values.len(), node);
} else {
_non_code_meta = non_code_meta;
}
let mut _previous_values = previous_values;
_previous_values.push(value);
make_pipe_body(
tokens,
next_pipe.index,
_previous_values,
Some(_non_code_meta),
)
make_pipe_body(tokens, next_pipe.index, _previous_values, Some(_non_code_meta))
}
struct BinaryExpressionReturn {
@ -751,10 +716,7 @@ struct BinaryExpressionReturn {
last_index: usize,
}
fn make_binary_expression(
tokens: &[Token],
index: usize,
) -> Result<BinaryExpressionReturn, KclError> {
fn make_binary_expression(tokens: &[Token], index: usize) -> Result<BinaryExpressionReturn, KclError> {
let end_index = find_end_of_binary_expression(tokens, index)?;
let expression = parse_expression(&tokens[index..end_index + 1])?;
Ok(BinaryExpressionReturn {
@ -768,11 +730,7 @@ struct ArgumentsReturn {
last_index: usize,
}
fn make_arguments(
tokens: &[Token],
index: usize,
previous_args: Vec<Value>,
) -> Result<ArgumentsReturn, KclError> {
fn make_arguments(tokens: &[Token], index: usize, previous_args: Vec<Value>) -> Result<ArgumentsReturn, KclError> {
let brace_or_comma_token = &tokens[index];
let should_finish_recursion =
brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")";
@ -788,40 +746,28 @@ fn make_arguments(
if let Some(next_brace_or_comma_token) = next_brace_or_comma.token {
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 == "["
{
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "[" {
let array_expression = make_array_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index =
next_meaningful_token(tokens, array_expression.last_index, None).index;
let mut _previous_args = previous_args;
_previous_args.push(Value::ArrayExpression(Box::new(
array_expression.expression,
)));
_previous_args.push(Value::ArrayExpression(Box::new(array_expression.expression)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
}
if argument_token_token.token_type == TokenType::Operator
&& argument_token_token.value == "-"
{
if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "-" {
let unary_expression = make_unary_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index =
next_meaningful_token(tokens, unary_expression.last_index, None).index;
let mut _previous_args = previous_args;
_previous_args.push(Value::UnaryExpression(Box::new(
unary_expression.expression,
)));
_previous_args.push(Value::UnaryExpression(Box::new(unary_expression.expression)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
}
if argument_token_token.token_type == TokenType::Brace
&& argument_token_token.value == "{"
{
if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "{" {
let object_expression = make_object_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index =
next_meaningful_token(tokens, object_expression.last_index, None).index;
let mut _previous_args = previous_args;
_previous_args.push(Value::ObjectExpression(Box::new(
object_expression.expression,
)));
_previous_args.push(Value::ObjectExpression(Box::new(object_expression.expression)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
}
if (argument_token_token.token_type == TokenType::Word
@ -833,23 +779,17 @@ fn make_arguments(
let next_comma_or_brace_token_index =
next_meaningful_token(tokens, binary_expression.last_index, None).index;
let mut _previous_args = previous_args;
_previous_args.push(Value::BinaryExpression(Box::new(
binary_expression.expression,
)));
_previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
}
if !is_identifier_or_literal {
let binary_expression = make_binary_expression(tokens, next_brace_or_comma.index)?;
let mut _previous_args = previous_args;
_previous_args.push(Value::BinaryExpression(Box::new(
binary_expression.expression,
)));
_previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
return make_arguments(tokens, binary_expression.last_index, _previous_args);
}
if argument_token_token.token_type == TokenType::Operator
&& argument_token_token.value == "%"
{
if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "%" {
let value = Value::PipeSubstitution(Box::new(PipeSubstitution {
start: argument_token_token.start,
end: argument_token_token.end,
@ -864,28 +804,23 @@ fn make_arguments(
&& next_brace_or_comma_token.value == "("
{
let closing_brace = find_closing_brace(tokens, next_brace_or_comma.index, 0, "")?;
return if let Some(token_after_closing_brace) =
next_meaningful_token(tokens, closing_brace, None).token
return if let Some(token_after_closing_brace) = next_meaningful_token(tokens, closing_brace, None).token
{
if token_after_closing_brace.token_type == TokenType::Operator
&& token_after_closing_brace.value != "|>"
{
let binary_expression =
make_binary_expression(tokens, argument_token.index)?;
let binary_expression = make_binary_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index =
next_meaningful_token(tokens, binary_expression.last_index, None).index;
let mut _previous_args = previous_args;
_previous_args.push(Value::BinaryExpression(Box::new(
binary_expression.expression,
)));
_previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
make_arguments(tokens, next_comma_or_brace_token_index, _previous_args)
} else {
let call_expression = make_call_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index =
next_meaningful_token(tokens, call_expression.last_index, None).index;
let mut _previous_args = previous_args;
_previous_args
.push(Value::CallExpression(Box::new(call_expression.expression)));
_previous_args.push(Value::CallExpression(Box::new(call_expression.expression)));
make_arguments(tokens, next_comma_or_brace_token_index, _previous_args)
}
} else {
@ -897,8 +832,7 @@ fn make_arguments(
}
if argument_token_token.token_type == TokenType::Word {
let identifier =
Value::Identifier(Box::new(make_identifier(tokens, argument_token.index)));
let identifier = Value::Identifier(Box::new(make_identifier(tokens, argument_token.index)));
let mut _previous_args = previous_args;
_previous_args.push(identifier);
return make_arguments(tokens, next_brace_or_comma.index, _previous_args);
@ -909,9 +843,7 @@ fn make_arguments(
let mut _previous_args = previous_args;
_previous_args.push(literal);
return make_arguments(tokens, next_brace_or_comma.index, _previous_args);
} else if argument_token_token.token_type == TokenType::Brace
&& argument_token_token.value == ")"
{
} else if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == ")" {
return make_arguments(tokens, argument_token.index, previous_args);
}
@ -938,10 +870,7 @@ pub struct CallExpressionResult {
last_index: usize,
}
pub fn make_call_expression(
tokens: &[Token],
index: usize,
) -> Result<CallExpressionResult, KclError> {
pub fn make_call_expression(tokens: &[Token], index: usize) -> Result<CallExpressionResult, KclError> {
let current_token = tokens[index].clone();
let brace_token = next_meaningful_token(tokens, index, None);
let callee = make_identifier(tokens, index);
@ -1034,27 +963,20 @@ struct VariableDeclarationResult {
last_index: usize,
}
fn make_variable_declaration(
tokens: &[Token],
index: usize,
) -> Result<VariableDeclarationResult, KclError> {
fn make_variable_declaration(tokens: &[Token], index: usize) -> Result<VariableDeclarationResult, KclError> {
let current_token = tokens[index].clone();
let declaration_start_token = next_meaningful_token(tokens, index, None);
let variable_declarators_result =
make_variable_declarators(tokens, declaration_start_token.index, vec![])?;
let variable_declarators_result = make_variable_declarators(tokens, declaration_start_token.index, vec![])?;
Ok(VariableDeclarationResult {
declaration: VariableDeclaration {
start: current_token.start,
end: variable_declarators_result.declarations
[variable_declarators_result.declarations.len() - 1]
.end,
kind: if current_token.value == "const" {
"const".to_string()
} else if current_token.value == "fn" {
"fn".to_string()
} else {
"unkown".to_string()
},
end: variable_declarators_result.declarations[variable_declarators_result.declarations.len() - 1].end,
kind: VariableKind::from_str(&current_token.value).map_err(|_| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "Unexpected token".to_string(),
})
})?,
declarations: variable_declarators_result.declarations,
},
last_index: variable_declarators_result.last_index,
@ -1066,18 +988,12 @@ pub struct ParamsResult {
pub last_index: usize,
}
fn make_params(
tokens: &[Token],
index: usize,
previous_params: Vec<Identifier>,
) -> Result<ParamsResult, KclError> {
fn make_params(tokens: &[Token], index: usize, previous_params: Vec<Identifier>) -> Result<ParamsResult, KclError> {
let brace_or_comma_token = &tokens[index];
let argument = next_meaningful_token(tokens, 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 == ")");
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,
@ -1102,10 +1018,7 @@ struct UnaryExpressionResult {
last_index: usize,
}
fn make_unary_expression(
tokens: &[Token],
index: usize,
) -> Result<UnaryExpressionResult, KclError> {
fn make_unary_expression(tokens: &[Token], index: usize) -> Result<UnaryExpressionResult, KclError> {
let current_token = &tokens[index];
let next_token = next_meaningful_token(tokens, index, None);
let argument = make_value(tokens, next_token.index)?;
@ -1120,17 +1033,11 @@ fn make_unary_expression(
start: current_token.start,
end: argument_token.end,
argument: match argument.value {
Value::BinaryExpression(binary_expression) => {
BinaryPart::BinaryExpression(binary_expression)
}
Value::BinaryExpression(binary_expression) => BinaryPart::BinaryExpression(binary_expression),
Value::Identifier(identifier) => BinaryPart::Identifier(identifier),
Value::Literal(literal) => BinaryPart::Literal(literal),
Value::UnaryExpression(unary_expression) => {
BinaryPart::UnaryExpression(unary_expression)
}
Value::CallExpression(call_expression) => {
BinaryPart::CallExpression(call_expression)
}
Value::UnaryExpression(unary_expression) => BinaryPart::UnaryExpression(unary_expression),
Value::CallExpression(call_expression) => BinaryPart::CallExpression(call_expression),
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
@ -1149,10 +1056,7 @@ struct ExpressionStatementResult {
last_index: usize,
}
fn make_expression_statement(
tokens: &[Token],
index: usize,
) -> Result<ExpressionStatementResult, KclError> {
fn make_expression_statement(tokens: &[Token], index: usize) -> Result<ExpressionStatementResult, KclError> {
let current_token = &tokens[index];
let next = next_meaningful_token(tokens, index, None);
if let Some(next_token) = &next.token {
@ -1252,10 +1156,7 @@ struct ObjectExpressionResult {
last_index: usize,
}
fn make_object_expression(
tokens: &[Token],
index: usize,
) -> Result<ObjectExpressionResult, KclError> {
fn make_object_expression(tokens: &[Token], index: usize) -> Result<ObjectExpressionResult, KclError> {
let opening_brace_token = &tokens[index];
let first_property_token = next_meaningful_token(tokens, index, None);
let object_properties = make_object_properties(tokens, first_property_token.index, vec![])?;
@ -1274,10 +1175,7 @@ struct ReturnStatementResult {
last_index: usize,
}
fn make_return_statement(
tokens: &[Token],
index: usize,
) -> Result<ReturnStatementResult, KclError> {
fn make_return_statement(tokens: &[Token], index: usize) -> Result<ReturnStatementResult, KclError> {
let current_token = &tokens[index];
let next_token = next_meaningful_token(tokens, index, None);
let val = make_value(tokens, next_token.index)?;
@ -1329,9 +1227,7 @@ fn make_body(
if previous_body.is_empty() {
non_code_meta.start = next_token.non_code_node;
} else {
non_code_meta
.none_code_nodes
.insert(previous_body.len(), node.clone());
non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
}
}
return make_body(tokens, next_token.index, previous_body, non_code_meta);
@ -1339,18 +1235,14 @@ fn make_body(
let next = next_meaningful_token(tokens, token_index, None);
if let Some(node) = &next.non_code_node {
non_code_meta
.none_code_nodes
.insert(previous_body.len(), node.clone());
non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
}
if token.token_type == TokenType::Word && (token.value == *"const" || token.value == "fn") {
let declaration = make_variable_declaration(tokens, token_index)?;
let next_thing = next_meaningful_token(tokens, declaration.last_index, None);
if let Some(node) = &next_thing.non_code_node {
non_code_meta
.none_code_nodes
.insert(previous_body.len(), node.clone());
non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
}
let mut _previous_body = previous_body;
_previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration {
@ -1371,9 +1263,7 @@ fn make_body(
let statement = make_return_statement(tokens, token_index)?;
let next_thing = next_meaningful_token(tokens, statement.last_index, None);
if let Some(node) = &next_thing.non_code_node {
non_code_meta
.none_code_nodes
.insert(previous_body.len(), node.clone());
non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
}
let mut _previous_body = previous_body;
_previous_body.push(BodyItem::ReturnStatement(ReturnStatement {
@ -1390,16 +1280,11 @@ fn make_body(
}
if let Some(next_token) = next.token {
if token.token_type == TokenType::Word
&& next_token.token_type == TokenType::Brace
&& next_token.value == "("
{
if token.token_type == TokenType::Word && next_token.token_type == TokenType::Brace && next_token.value == "(" {
let expression = make_expression_statement(tokens, token_index)?;
let next_thing = next_meaningful_token(tokens, expression.last_index, None);
if let Some(node) = &next_thing.non_code_node {
non_code_meta
.none_code_nodes
.insert(previous_body.len(), node.clone());
non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
}
let mut _previous_body = previous_body;
_previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement {
@ -1422,9 +1307,7 @@ fn make_body(
&& next_thing_token.token_type == TokenType::Operator
{
if let Some(node) = &next_thing.non_code_node {
non_code_meta
.none_code_nodes
.insert(previous_body.len(), node.clone());
non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
}
let expression = make_expression_statement(tokens, token_index)?;
let mut _previous_body = previous_body;
@ -1492,10 +1375,7 @@ struct FunctionExpressionResult {
last_index: usize,
}
fn make_function_expression(
tokens: &[Token],
index: usize,
) -> Result<FunctionExpressionResult, KclError> {
fn make_function_expression(tokens: &[Token], index: usize) -> Result<FunctionExpressionResult, KclError> {
let current_token = &tokens[index];
let closing_brace_index = find_closing_brace(tokens, index, 0, "")?;
let arrow_token = next_meaningful_token(tokens, closing_brace_index, None);
@ -1538,9 +1418,10 @@ pub fn abstract_syntax_tree(tokens: &[Token]) -> Result<Program, KclError> {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_make_identifier() {
let tokens = crate::tokeniser::lexer("a");
@ -1701,8 +1582,7 @@ const key = 'c'"#,
Some(NoneCodeNode {
start: 106,
end: 166,
value: " /* this is\n a comment\n spanning a few lines */\n "
.to_string(),
value: " /* this is\n a comment\n spanning a few lines */\n ".to_string(),
}),
59,
);
@ -2226,8 +2106,7 @@ const key = 'c'"#,
4
);
let handles_non_zero_index =
"(indexForBracketToRightOfThisIsTwo(shouldBeFour)AndNotThisSix)";
let handles_non_zero_index = "(indexForBracketToRightOfThisIsTwo(shouldBeFour)AndNotThisSix)";
assert_eq!(
find_closing_brace(&crate::tokeniser::lexer(handles_non_zero_index), 2, 0, "").unwrap(),
4
@ -2279,10 +2158,7 @@ const key = 'c'"#,
);
assert_eq!(
find_next_declaration_keyword(&tokens, 4).unwrap(),
TokenReturn {
token: None,
index: 92,
}
TokenReturn { token: None, index: 92 }
);
let tokens = crate::tokeniser::lexer(
@ -2304,10 +2180,7 @@ const newVar = myVar + 1
);
assert_eq!(
find_next_declaration_keyword(&tokens, 14).unwrap(),
TokenReturn {
token: None,
index: 19,
}
TokenReturn { token: None, index: 19 }
);
}
@ -2401,10 +2274,7 @@ const yo = myFunc(9()
} |> rx(90, %)
show(mySk1)"#;
let tokens = crate::tokeniser::lexer(code);
let token_with_my_path_index = tokens
.iter()
.position(|token| token.value == "myPath")
.unwrap();
let token_with_my_path_index = tokens.iter().position(|token| token.value == "myPath").unwrap();
// loop through getting the token and it's index
let token_with_line_to_index_for_var_dec_index = tokens
.iter()
@ -2613,7 +2483,7 @@ show(mySk1)"#;
|> close(%)"#,
);
let result = make_variable_declaration(&tokens, 0).unwrap();
assert_eq!(result.declaration.kind, "const");
assert_eq!(result.declaration.kind.to_string(), "const");
assert_eq!(result.declaration.declarations.len(), 1);
assert_eq!(result.declaration.declarations[0].id.name, "yo");
let declaration = result.declaration.declarations[0].clone();

View File

@ -2,18 +2,14 @@
//! The inverse of parsing (which generates an AST from the source code)
use crate::abstract_syntax_tree_types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression,
Literal, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression,
Program, UnaryExpression, Value,
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression, Literal,
LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression, Program, UnaryExpression,
Value,
};
fn recast_literal(literal: Literal) -> String {
if let serde_json::Value::String(value) = literal.value {
let quote = if literal.raw.trim().starts_with('"') {
'"'
} else {
'\''
};
let quote = if literal.raw.trim().starts_with('"') { '"' } else { '\'' };
format!("{}{}{}", quote, value, quote)
} else {
literal.value.to_string()
@ -39,16 +35,13 @@ fn recast_binary_expression(expression: BinaryExpression) -> String {
let should_wrap_right = match expression.right.clone() {
BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator)
|| expression.operator == "-"
precedence(&expression.operator) > precedence(&bin_exp.operator) || expression.operator == "-"
}
_ => false,
};
let should_wrap_left = match expression.left.clone() {
BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator)
}
BinaryPart::BinaryExpression(bin_exp) => precedence(&expression.operator) > precedence(&bin_exp.operator),
_ => false,
};
@ -64,12 +57,8 @@ fn recast_binary_part(part: BinaryPart) -> String {
match part {
BinaryPart::Literal(literal) => recast_literal(*literal),
BinaryPart::Identifier(identifier) => identifier.name,
BinaryPart::BinaryExpression(binary_expression) => {
recast_binary_expression(*binary_expression)
}
BinaryPart::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
BinaryPart::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
BinaryPart::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
_ => String::new(),
}
}
@ -79,15 +68,11 @@ fn recast_value(node: Value, _indentation: String, is_in_pipe_expression: bool)
match node {
Value::BinaryExpression(bin_exp) => recast_binary_expression(*bin_exp),
Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, &indentation),
Value::ObjectExpression(ref obj_exp) => {
recast_object_expression(obj_exp, &indentation, is_in_pipe_expression)
}
Value::ObjectExpression(ref obj_exp) => recast_object_expression(obj_exp, &indentation, is_in_pipe_expression),
Value::MemberExpression(mem_exp) => recast_member_expression(*mem_exp),
Value::Literal(literal) => recast_literal(*literal),
Value::FunctionExpression(func_exp) => recast_function(*func_exp),
Value::CallExpression(call_exp) => {
recast_call_expression(&call_exp, &indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => recast_call_expression(&call_exp, &indentation, is_in_pipe_expression),
Value::Identifier(ident) => ident.name,
Value::PipeExpression(pipe_exp) => recast_pipe_expression(&pipe_exp),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -124,11 +109,7 @@ fn recast_array_expression(expression: &ArrayExpression, indentation: &str) -> S
}
}
fn recast_object_expression(
expression: &ObjectExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
fn recast_object_expression(expression: &ObjectExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
let flat_recast = format!(
"{{ {} }}",
expression
@ -157,11 +138,7 @@ fn recast_object_expression(
format!(
"{}: {}",
prop.key.name,
recast_value(
prop.value.clone(),
_indentation.clone(),
is_in_pipe_expression
)
recast_value(prop.value.clone(), _indentation.clone(), is_in_pipe_expression)
)
})
.collect::<Vec<String>>()
@ -173,11 +150,7 @@ fn recast_object_expression(
}
}
fn recast_call_expression(
expression: &CallExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
fn recast_call_expression(expression: &CallExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
format!(
"{}({})",
expression.callee.name,
@ -199,9 +172,7 @@ fn recast_argument(argument: Value, indentation: &str, is_in_pipe_expression: bo
Value::ObjectExpression(object_exp) => {
recast_object_expression(&object_exp, indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => {
recast_call_expression(&call_exp, indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => recast_call_expression(&call_exp, indentation, is_in_pipe_expression),
Value::FunctionExpression(function_exp) => recast_function(*function_exp),
Value::PipeSubstitution(_) => "%".to_string(),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -222,9 +193,7 @@ fn recast_member_expression(expression: MemberExpression) -> String {
};
match expression.object {
MemberObject::MemberExpression(member_exp) => {
recast_member_expression(*member_exp) + key_str.as_str()
}
MemberObject::MemberExpression(member_exp) => recast_member_expression(*member_exp) + key_str.as_str(),
MemberObject::Identifier(identifier) => identifier.name + key_str.as_str(),
}
}
@ -261,9 +230,7 @@ fn recast_unary_expression(expression: UnaryExpression) -> String {
let bin_part_val = match expression.argument {
BinaryPart::Literal(literal) => Value::Literal(literal),
BinaryPart::Identifier(identifier) => Value::Identifier(identifier),
BinaryPart::BinaryExpression(binary_expression) => {
Value::BinaryExpression(binary_expression)
}
BinaryPart::BinaryExpression(binary_expression) => Value::BinaryExpression(binary_expression),
BinaryPart::CallExpression(call_expression) => Value::CallExpression(call_expression),
BinaryPart::UnaryExpression(unary_expression) => Value::UnaryExpression(unary_expression),
};
@ -278,23 +245,13 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
ast.body
.iter()
.map(|statement| match statement.clone() {
BodyItem::ExpressionStatement(expression_statement) => {
match expression_statement.expression {
Value::BinaryExpression(binary_expression) => {
recast_binary_expression(*binary_expression)
}
Value::ArrayExpression(array_expression) => {
recast_array_expression(&array_expression, "")
}
Value::ObjectExpression(object_expression) => {
recast_object_expression(&object_expression, "", false)
}
Value::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
_ => "Expression".to_string(),
}
}
BodyItem::ExpressionStatement(expression_statement) => match expression_statement.expression {
Value::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
Value::ArrayExpression(array_expression) => recast_array_expression(&array_expression, ""),
Value::ObjectExpression(object_expression) => recast_object_expression(&object_expression, "", false),
Value::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
_ => "Expression".to_string(),
},
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration
.declarations
.iter()
@ -308,22 +265,16 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
})
.collect::<String>(),
BodyItem::ReturnStatement(return_statement) => {
format!(
"return {}",
recast_argument(return_statement.argument, "", false)
)
format!("return {}", recast_argument(return_statement.argument, "", false))
}
})
.enumerate()
.map(|(index, recast_str)| {
let is_legit_custom_whitespace_or_comment =
|str: String| str != " " && str != "\n" && str != " ";
let is_legit_custom_whitespace_or_comment = |str: String| str != " " && str != "\n" && str != " ";
// determine the value of startString
let last_white_space_or_comment = if index > 0 {
let tmp = if let Some(non_code_node) =
ast.non_code_meta.none_code_nodes.get(&(index - 1))
{
let tmp = if let Some(non_code_node) = ast.non_code_meta.none_code_nodes.get(&(index - 1)) {
non_code_node.value.clone()
} else {
" ".to_string()
@ -333,12 +284,11 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
" ".to_string()
};
// indentation of this line will be covered by the previous if we're using a custom whitespace or comment
let mut start_string =
if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
String::new()
} else {
indentation.to_owned()
};
let mut start_string = if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
String::new()
} else {
indentation.to_owned()
};
if index == 0 {
if let Some(start) = ast.non_code_meta.start.clone() {
start_string = start.value;
@ -356,13 +306,10 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
} else {
"\n".to_string()
};
let mut custom_white_space_or_comment =
match ast.non_code_meta.none_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => {
custom_white_space_or_comment.value.clone()
}
None => String::new(),
};
let mut custom_white_space_or_comment = match ast.non_code_meta.none_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => custom_white_space_or_comment.value.clone(),
None => String::new(),
};
if !is_legit_custom_whitespace_or_comment(custom_white_space_or_comment.clone()) {
custom_white_space_or_comment = String::new();
}

View File

@ -1,15 +1,15 @@
//! Functions related to extruding.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, ExtrudeTransform, MemoryItem, SketchGroup},
std::Args,
};
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
/// Extrudes by a given amount.
pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
let (length, sketch_group) = args.get_number_sketch_group()?;
@ -23,11 +23,7 @@ pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "extrude"
}]
fn inner_extrude(
length: f64,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<ExtrudeGroup, KclError> {
fn inner_extrude(length: f64, sketch_group: SketchGroup, args: &mut Args) -> Result<ExtrudeGroup, KclError> {
let id = uuid::Uuid::new_v4();
let cmd = kittycad::types::ModelingCmd::Extrude {
@ -65,17 +61,15 @@ fn inner_get_extrude_wall_transform(
extrude_group: ExtrudeGroup,
args: &mut Args,
) -> Result<ExtrudeTransform, KclError> {
let surface = extrude_group
.get_path_by_name(surface_name)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
surface_name
),
source_ranges: vec![args.source_range],
})
})?;
let surface = extrude_group.get_path_by_name(surface_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
surface_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(ExtrudeTransform {
position: surface.get_position(),

View File

@ -27,8 +27,7 @@ pub type FnMap = HashMap<String, StdFn>;
pub type StdFn = fn(&mut Args) -> Result<MemoryItem, KclError>;
pub struct StdLib {
#[allow(dead_code)]
internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
pub internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
pub fns: FnMap,
}
@ -64,20 +63,16 @@ impl StdLib {
Box::new(crate::std::sketch::AngledLineThatIntersects),
Box::new(crate::std::sketch::StartSketchAt),
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::BezierCurve),
];
let mut fns = HashMap::new();
for internal_fn_name in &internal_fn_names {
fns.insert(
internal_fn_name.name().to_string(),
internal_fn_name.std_lib_fn(),
);
fns.insert(internal_fn_name.name().to_string(), internal_fn_name.std_lib_fn());
}
Self {
internal_fn_names,
fns,
}
Self { internal_fn_names, fns }
}
}
@ -95,22 +90,15 @@ pub struct Args<'a> {
}
impl<'a> Args<'a> {
pub fn new(
args: Vec<MemoryItem>,
source_range: SourceRange,
engine: &'a mut EngineConnection,
) -> Self {
pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, engine: &'a mut EngineConnection) -> Self {
Self {
args,
source_range,
engine,
}
}
pub fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
pub fn send_modeling_cmd(&mut self, id: uuid::Uuid, cmd: kittycad::types::ModelingCmd) -> Result<(), KclError> {
self.engine.send_modeling_cmd(id, self.source_range, cmd)
}
@ -124,14 +112,14 @@ impl<'a> Args<'a> {
}
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
self.make_user_val_from_json(serde_json::Value::Number(
serde_json::Number::from_f64(f).ok_or_else(|| {
self.make_user_val_from_json(serde_json::Value::Number(serde_json::Number::from_f64(f).ok_or_else(
|| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert `{}` to a number", f),
source_ranges: vec![self.source_range],
})
})?,
))
},
)?))
}
fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
@ -164,10 +152,7 @@ impl<'a> Args<'a> {
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a string as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -177,20 +162,14 @@ impl<'a> Args<'a> {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a string as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
@ -199,10 +178,7 @@ impl<'a> Args<'a> {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -213,10 +189,7 @@ impl<'a> Args<'a> {
fn get_sketch_group(&self) -> Result<SketchGroup, KclError> {
let first_value = self.args.first().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
@ -225,10 +198,7 @@ impl<'a> Args<'a> {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -242,10 +212,7 @@ impl<'a> Args<'a> {
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a struct as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -261,18 +228,13 @@ impl<'a> Args<'a> {
Ok(data)
}
fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>(
&self,
) -> Result<(T, SketchGroup), KclError> {
fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, SketchGroup), KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a struct as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -287,10 +249,7 @@ impl<'a> Args<'a> {
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
@ -299,10 +258,7 @@ impl<'a> Args<'a> {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -310,9 +266,7 @@ impl<'a> Args<'a> {
Ok((data, sketch_group))
}
fn get_segment_name_to_number_sketch_group(
&self,
) -> Result<(String, f64, SketchGroup), KclError> {
fn get_segment_name_to_number_sketch_group(&self) -> Result<(String, f64, SketchGroup), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a number.
// The third argument should be a SketchGroup.
@ -321,10 +275,7 @@ impl<'a> Args<'a> {
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a string as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -334,10 +285,7 @@ impl<'a> Args<'a> {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a string as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -347,10 +295,7 @@ impl<'a> Args<'a> {
.get(1)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a number as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a number as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -360,10 +305,7 @@ impl<'a> Args<'a> {
let third_value = self.args.get(2).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the third argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the third argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
@ -372,10 +314,7 @@ impl<'a> Args<'a> {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the third argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the third argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -391,10 +330,7 @@ impl<'a> Args<'a> {
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a number as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a number as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -404,10 +340,7 @@ impl<'a> Args<'a> {
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
@ -416,10 +349,7 @@ impl<'a> Args<'a> {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -435,10 +365,7 @@ impl<'a> Args<'a> {
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a string as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
@ -448,10 +375,7 @@ impl<'a> Args<'a> {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a string as the first argument, found `{:?}`",
self.args
),
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
@ -589,11 +513,7 @@ mod tests {
continue;
}
buf.push_str(&format!(
"\t* [`{}`](#{})\n",
internal_fn.name(),
internal_fn.name()
));
buf.push_str(&format!("\t* [`{}`](#{})\n", internal_fn.name(), internal_fn.name()));
}
buf.push_str("\n\n");
@ -617,25 +537,15 @@ mod tests {
fn_docs.push_str(&format!("{}\n\n", internal_fn.description()));
fn_docs.push_str("```\n");
fn_docs.push_str(&format!("{}(", internal_fn.name()));
for (i, arg) in internal_fn.args().iter().enumerate() {
if i > 0 {
fn_docs.push_str(", ");
}
fn_docs.push_str(&format!("{}: {}", arg.name, arg.type_));
}
fn_docs.push_str(") -> ");
fn_docs.push_str(&internal_fn.return_value().type_);
let signature = internal_fn.fn_signature();
fn_docs.push_str(&signature);
fn_docs.push_str("\n```\n\n");
fn_docs.push_str("#### Arguments\n\n");
for arg in internal_fn.args() {
let (format, should_be_indented) = arg.get_type_string().unwrap();
if let Some(description) = arg.description() {
fn_docs.push_str(&format!(
"* `{}`: `{}` - {}\n",
arg.name, arg.type_, description
));
fn_docs.push_str(&format!("* `{}`: `{}` - {}\n", arg.name, arg.type_, description));
} else {
fn_docs.push_str(&format!("* `{}`: `{}`\n", arg.name, arg.type_));
}

View File

@ -1,15 +1,15 @@
//! Functions related to line segments.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{MemoryItem, SketchGroup},
std::{utils::get_angle, Args},
};
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
/// Returns the segment end of x.
pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
@ -22,22 +22,16 @@ pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "segEndX",
}]
fn inner_segment_end_x(
segment_name: &str,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
let line = sketch_group
.get_base_by_name_or_start(segment_name)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
fn inner_segment_end_x(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let line = sketch_group.get_base_by_name_or_start(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(line.to[0])
}
@ -54,22 +48,16 @@ pub fn segment_end_y(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "segEndY",
}]
fn inner_segment_end_y(
segment_name: &str,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
let line = sketch_group
.get_base_by_name_or_start(segment_name)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
fn inner_segment_end_y(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let line = sketch_group.get_base_by_name_or_start(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(line.to[1])
}
@ -145,11 +133,7 @@ pub fn segment_length(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "segLen",
}]
fn inner_segment_length(
segment_name: &str,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
fn inner_segment_length(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
@ -178,11 +162,7 @@ pub fn segment_angle(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "segAng",
}]
fn inner_segment_angle(
segment_name: &str,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
fn inner_segment_angle(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(

View File

@ -1,5 +1,6 @@
//! Functions related to sketching.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::{ModelingCmd, Point3D};
use schemars::JsonSchema;
@ -9,13 +10,11 @@ use crate::{
errors::{KclError, KclErrorDetails},
executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup},
std::{
utils::{get_x_component, get_y_component, intersection_with_parallel_line},
utils::{arc_angles, arc_center_and_end, get_x_component, get_y_component, intersection_with_parallel_line},
Args,
},
};
use anyhow::Result;
/// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -44,11 +43,7 @@ pub fn line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "lineTo",
}]
fn inner_line_to(
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
fn inner_line_to(data: LineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let to = match data {
LineToData::PointWithTag { to, .. } => to,
@ -56,6 +51,21 @@ fn inner_line_to(
};
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Line {
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
@ -106,18 +116,11 @@ pub fn x_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "xLineTo",
}]
fn inner_x_line_to(
data: AxisLineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
fn inner_x_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag {
to: [to, from.y],
tag,
},
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [to, from.y], tag },
AxisLineToData::Point(data) => LineToData::Point([data, from.y]),
};
@ -138,18 +141,11 @@ pub fn y_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "yLineTo",
}]
fn inner_y_line_to(
data: AxisLineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
fn inner_y_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag {
to: [from.x, to],
tag,
},
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [from.x, to], tag },
AxisLineToData::Point(data) => LineToData::Point([from.x, data]),
};
@ -207,11 +203,7 @@ pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "line",
}]
fn inner_line(
data: LineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, 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];
@ -289,11 +281,7 @@ pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "xLine",
}]
fn inner_x_line(
data: AxisLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, 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]),
@ -318,11 +306,7 @@ pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "yLine",
}]
fn inner_y_line(
data: AxisLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, 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]),
@ -373,9 +357,7 @@ fn inner_angled_line(
let from = sketch_group.get_coords_from_paths()?;
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => {
(angle_and_length[0], angle_and_length[1])
}
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
@ -424,9 +406,7 @@ fn inner_angled_line_of_x_length(
) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => {
(angle_and_length[0], angle_and_length[1])
}
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_y_component(angle, length);
@ -494,10 +474,7 @@ fn inner_angled_line_to_x(
let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag {
to: [x_to, y_to],
tag,
}
LineToData::PointWithTag { to: [x_to, y_to], tag }
} else {
LineToData::Point([x_to, y_to])
},
@ -527,9 +504,7 @@ fn inner_angled_line_of_y_length(
) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => {
(angle_and_length[0], angle_and_length[1])
}
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_x_component(angle, length);
@ -579,10 +554,7 @@ fn inner_angled_line_to_y(
let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag {
to: [x_to, y_to],
tag,
}
LineToData::PointWithTag { to: [x_to, y_to], tag }
} else {
LineToData::Point([x_to, y_to])
},
@ -610,8 +582,7 @@ pub struct AngeledLineThatIntersectsData {
/// Draw an angled line that intersects with a given line.
pub fn angled_line_that_intersects(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) =
args.get_data_and_sketch_group()?;
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_that_intersects(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
@ -760,13 +731,255 @@ fn inner_close(sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup
Ok(new_sketch_group)
}
/// Data to draw an arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum ArcData {
/// Angles and radius with a tag.
AnglesAndRadiusWithTag {
/// The start angle.
angle_start: f64,
/// The end angle.
angle_end: f64,
/// The radius.
radius: f64,
/// The tag.
tag: String,
},
/// Angles and radius.
AnglesAndRadius {
/// The start angle.
angle_start: f64,
/// The end angle.
angle_end: f64,
/// The radius.
radius: f64,
},
/// Center, to and radius with a tag.
CenterToRadiusWithTag {
/// The center.
center: [f64; 2],
/// The to point.
to: [f64; 2],
/// The radius.
radius: f64,
/// The tag.
tag: String,
},
/// Center, to and radius.
CenterToRadius {
/// The center.
center: [f64; 2],
/// The to point.
to: [f64; 2],
/// The radius.
radius: f64,
},
}
/// Draw an arc.
pub fn arc(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (ArcData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_arc(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an arc.
#[stdlib {
name = "arc",
}]
fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (center, angle_start, angle_end, radius, end) = match &data {
ArcData::AnglesAndRadiusWithTag {
angle_start,
angle_end,
radius,
..
} => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius);
(center, *angle_start, *angle_end, *radius, end)
}
ArcData::AnglesAndRadius {
angle_start,
angle_end,
radius,
} => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius);
(center, *angle_start, *angle_end, *radius, end)
}
ArcData::CenterToRadiusWithTag { center, to, radius, .. } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into())
}
ArcData::CenterToRadius { center, to, radius } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into())
}
};
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc {
angle_start,
angle_end,
center: center.into(),
radius,
},
},
)?;
// Move the path pen to the end of the arc.
// Since that is where we want to draw the next path.
// TODO: the engine should automatically move the pen to the end of the arc.
// This just seems inefficient.
args.send_modeling_cmd(
id,
ModelingCmd::MovePathPen {
path: sketch_group.id,
to: Point3D {
x: end.x,
y: end.y,
z: 0.0,
},
},
)?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: end.into(),
name: match data {
ArcData::AnglesAndRadiusWithTag { tag, .. } => tag.to_string(),
ArcData::AnglesAndRadius { .. } => "".to_string(),
ArcData::CenterToRadiusWithTag { tag, .. } => tag.to_string(),
ArcData::CenterToRadius { .. } => "".to_string(),
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
/// Data to draw a bezier curve.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum BezierData {
/// Points with a tag.
PointsWithTag {
/// The to point.
to: [f64; 2],
/// The first control point.
control1: [f64; 2],
/// The second control point.
control2: [f64; 2],
/// The tag.
tag: String,
},
/// Points.
Points {
/// The to point.
to: [f64; 2],
/// The first control point.
control1: [f64; 2],
/// The second control point.
control2: [f64; 2],
},
}
/// Draw a bezier curve.
pub fn bezier_curve(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (BezierData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_bezier_curve(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a bezier curve.
#[stdlib {
name = "bezierCurve",
}]
fn inner_bezier_curve(data: BezierData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (to, control1, control2) = match &data {
BezierData::PointsWithTag {
to, control1, control2, ..
} => (to, control1, control2),
BezierData::Points { to, control1, control2 } => (to, control1, control2),
};
let to = [from.x + to[0], from.y + to[1]];
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Bezier {
control1: Point3D {
x: from.x + control1[0],
y: from.y + control1[1],
z: 0.0,
},
control2: Point3D {
x: from.x + control2[0],
y: from.y + control2[1],
z: 0.0,
},
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to,
name: if let BezierData::PointsWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
#[cfg(test)]
mod tests {
use crate::std::sketch::{LineData, PointOrDefault};
use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault};
#[test]
fn test_deserialize_line_data() {
let mut str_json = "\"default\"".to_string();

View File

@ -1,3 +1,8 @@
use crate::{
errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange},
};
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let x = b[0] - a[0];
let y = b[1] - a[1];
@ -44,7 +49,10 @@ pub fn normalize_rad(angle: f64) -> f64 {
/// # Examples
///
/// ```
/// assert_eq!(kcl::std::utils::delta_angle(std::f64::consts::PI/8.0, std::f64::consts::PI/4.0), std::f64::consts::PI/8.0);
/// assert_eq!(
/// kcl_lib::std::utils::delta_angle(std::f64::consts::PI / 8.0, std::f64::consts::PI / 4.0),
/// std::f64::consts::PI / 8.0
/// );
/// ```
#[allow(dead_code)]
pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
@ -69,8 +77,14 @@ pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
/// # Examples
///
/// ```
/// assert_eq!(kcl::std::utils::distance_between_points(&[0.0, 0.0], &[0.0, 5.0]), 5.0);
/// assert_eq!(kcl::std::utils::distance_between_points(&[0.0, 0.0], &[3.0, 4.0]), 5.0);
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[0.0, 5.0]),
/// 5.0
/// );
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[3.0, 4.0]),
/// 5.0
/// );
/// ```
#[allow(dead_code)]
pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
@ -82,11 +96,7 @@ pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
}
pub fn calculate_intersection_of_two_lines(
line1: &[[f64; 2]; 2],
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] {
let line2_point_b = [
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
@ -117,27 +127,17 @@ pub fn intersection_with_parallel_line(
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
calculate_intersection_of_two_lines(
&offset_line(line1_offset, line1[0], line1[1]),
line2_angle,
line2_point,
)
calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
}
fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] {
if p1[0] == p2[0] {
let direction = (p1[1] - p2[1]).signum();
return [
[p1[0] + offset * direction, p1[1]],
[p2[0] + offset * direction, p2[1]],
];
return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]];
}
if p1[1] == p2[1] {
let direction = (p2[0] - p1[0]).signum();
return [
[p1[0], p1[1] + offset * direction],
[p2[0], p2[1] + offset * direction],
];
return [[p1[0], p1[1] + offset * direction], [p2[0], p2[1] + offset * direction]];
}
let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0]));
[[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]]
@ -165,12 +165,81 @@ pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
[sign * x_component, sign * y_component]
}
pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f64, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle_deg * (std::f64::consts::PI / 180.0);
let end_angle = end_angle_deg * (std::f64::consts::PI / 180.0);
let center = Point2d {
x: -1.0 * (radius * start_angle.cos() - from.x),
y: -1.0 * (radius * start_angle.sin() - from.y),
};
let end = Point2d {
x: center.x + radius * end_angle.cos(),
y: center.y + radius * end_angle.sin(),
};
(center, end)
}
pub fn arc_angles(
from: &Point2d,
to: &Point2d,
center: &Point2d,
radius: f64,
source_range: SourceRange,
) -> Result<(f64, f64), KclError> {
// First make sure that the points are on the circumference of the circle.
// If not, we'll return an error.
if !is_on_circumference(center, from, radius) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
from, center, radius
),
source_ranges: vec![source_range],
}));
}
if !is_on_circumference(center, to, radius) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
to, center, radius
),
source_ranges: vec![source_range],
}));
}
let start_angle = (from.y - center.y).atan2(from.x - center.x);
let end_angle = (to.y - center.y).atan2(to.x - center.x);
let start_angle_deg = start_angle * (180.0 / std::f64::consts::PI);
let end_angle_deg = end_angle * (180.0 / std::f64::consts::PI);
Ok((start_angle_deg, end_angle_deg))
}
pub fn is_on_circumference(center: &Point2d, point: &Point2d, radius: f64) -> bool {
let dx = point.x - center.x;
let dy = point.y - center.y;
let distance_squared = dx.powi(2) + dy.powi(2);
// We'll check if the distance squared is approximately equal to radius squared.
// Due to potential floating point inaccuracies, we'll check if the difference
// is very small (e.g., 1e-9) rather than checking for strict equality.
(distance_squared - radius.powi(2)).abs() < 1e-9
}
#[cfg(test)]
mod tests {
// Here you can bring your functions into scope
use super::{get_x_component, get_y_component};
use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component};
use crate::executor::SourceRange;
static EACH_QUAD: [(i32, [i32; 2]); 12] = [
(-315, [1, 1]),
(-225, [-1, 1]),
@ -245,4 +314,77 @@ mod tests {
assert!((result[0] - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, -1);
}
#[test]
fn test_arc_center_and_end() {
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 90.0, 1.0);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -1.0);
assert_eq!(end.y, 1.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 1.0);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -2.0);
assert_eq!(end.y.round(), 0.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 10.0);
assert_eq!(center.x.round(), -10.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -20.0);
assert_eq!(end.y.round(), 0.0);
}
#[test]
fn test_arc_angles() {
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 1.0 },
&super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 90.0);
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -2.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -20.0, y: 0.0 },
&super::Point2d { x: -10.0, y: 0.0 },
10.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
let result = super::arc_angles(
&super::Point2d { x: 0.0, y: 5.0 },
&super::Point2d { x: 5.0, y: 5.0 },
&super::Point2d { x: 10.0, y: -10.0 },
10.0,
SourceRange(Default::default()),
);
if let Err(err) = result {
assert!(err.to_string().contains( "Point Point2d { x: 0.0, y: 5.0 } is not on the circumference of the circle with center Point2d { x: 10.0, y: -10.0 } and radius 10."));
} else {
panic!("Expected error");
}
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
}
}

View File

@ -46,8 +46,7 @@ lazy_static! {
static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref OPERATOR: Regex =
Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
static ref BLOCK_END: Regex = Regex::new(r"^\}").unwrap();
static ref PARAN_START: Regex = Regex::new(r"^\(").unwrap();
@ -114,9 +113,7 @@ fn is_block_comment(character: &str) -> bool {
}
fn match_first(str: &str, regex: &Regex) -> Option<String> {
regex
.find(str)
.map(|the_match| the_match.as_str().to_string())
regex.find(str).map(|the_match| the_match.as_str().to_string())
}
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
@ -251,11 +248,7 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
}
pub fn lexer(str: &str) -> Vec<Token> {
fn recursively_tokenise(
str: &str,
current_index: usize,
previous_tokens: Vec<Token>,
) -> Vec<Token> {
fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= str.len() {
return previous_tokens;
}
@ -273,9 +266,10 @@ pub fn lexer(str: &str) -> Vec<Token> {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn is_number_test() {
assert!(is_number("1"));

View File

@ -0,0 +1,6 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -9,25 +9,19 @@ use wasm_bindgen::prelude::*;
pub async fn execute_wasm(
program_str: &str,
memory_str: &str,
manager: kcl::engine::conn_wasm::EngineCommandManager,
manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
) -> Result<JsValue, String> {
// deserialize the ast from a stringified json
let program: kcl::abstract_syntax_tree_types::Program =
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut mem: kcl::executor::ProgramMemory =
serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let mut mem: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let mut engine = kcl::engine::EngineConnection::new(manager)
let mut engine = kcl_lib::engine::EngineConnection::new(manager)
.await
.map_err(|e| format!("{:?}", e))?;
let memory = kcl::executor::execute(
program,
&mut mem,
kcl::executor::BodyType::Root,
&mut engine,
)
.map_err(String::from)?;
let memory = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)
.map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&memory).map_err(|e| e.to_string())
@ -37,35 +31,31 @@ pub async fn execute_wasm(
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
if !ws_resp.success {
return Err(JsError::new(&format!(
"Server returned error: {:?}",
ws_resp.errors
)));
if let Some(success) = ws_resp.success {
if !success {
return Err(JsError::new(&format!("Server returned error: {:?}", ws_resp.errors)));
}
}
if let Some(kittycad::types::OkWebSocketResponseData::Export { files }) = ws_resp.resp {
return Ok(JsValue::from_serde(&files)?);
}
Err(JsError::new(&format!(
"Invalid response type, got: {:?}",
ws_resp
)))
Err(JsError::new(&format!("Invalid response type, got: {:?}", ws_resp)))
}
// wasm_bindgen wrapper for lexer
// test for this function and by extension lexer are done in javascript land src/lang/tokeniser.test.ts
#[wasm_bindgen]
pub fn lexer_js(js: &str) -> Result<JsValue, JsError> {
let tokens = kcl::tokeniser::lexer(js);
let tokens = kcl_lib::tokeniser::lexer(js);
Ok(JsValue::from_serde(&tokens)?)
}
#[wasm_bindgen]
pub fn parse_js(js: &str) -> Result<JsValue, String> {
let tokens = kcl::tokeniser::lexer(js);
let program = kcl::parser::abstract_syntax_tree(&tokens).map_err(String::from)?;
let tokens = kcl_lib::tokeniser::lexer(js);
let program = kcl_lib::parser::abstract_syntax_tree(&tokens).map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&program).map_err(|e| e.to_string())
@ -76,9 +66,9 @@ pub fn parse_js(js: &str) -> Result<JsValue, String> {
#[wasm_bindgen]
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
// deserialize the ast from a stringified json
let program: kcl::abstract_syntax_tree_types::Program =
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(json_str).map_err(JsError::from)?;
let result = kcl::recast::recast(&program, "", false);
let result = kcl_lib::recast::recast(&program, "", false);
Ok(JsValue::from_serde(&result)?)
}

View File

@ -1752,10 +1752,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.34":
version "0.0.34"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.34.tgz#c1f1021f6c77bd9f47caa685cfbff0ef358a0316"
integrity sha512-9pUUuspJB/rayW4adfF7UqRYLw1pugBy3t0+V6qK3sWttG9flgv54fPw3JKewn7VFoEjRtNtoREMAoWb4ZrUIw==
"@kittycad/lib@^0.0.35":
version "0.0.35"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
dependencies:
node-fetch "3.3.2"
openapi-types "^12.0.0"
@ -1986,6 +1986,70 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz#31b9c510d8cada9683549e1dbb4284cca5001faf"
integrity sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==
"@sentry-internal/tracing@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.65.0.tgz#f7c56885d10c753ef03a25405dae13728916c0f5"
integrity sha512-TEYkiq5vKr1Y79YIu+UYr1sO3vEMttQOBsOZLziDbqiC7TvKUARBR4W5XWfb9qBVDeon87EFNKluW0/+7rzYWw==
dependencies:
"@sentry/core" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/browser@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.65.0.tgz#fb2009d6f8f1e5e3e1c616ce0ea70dd728c46ce7"
integrity sha512-TUzZPAXNJ/Y1yakFODYhsEtdDpLdkgjTfrx5i9MOnXQLrcRR0C4TC1KitqbP6Tv7Xha9WiR0TDZkh7gS/9RxEA==
dependencies:
"@sentry-internal/tracing" "7.65.0"
"@sentry/core" "7.65.0"
"@sentry/replay" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/core@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.65.0.tgz#01c1320b4e7c62ccf757258c1622d07cc743468a"
integrity sha512-EwZABW8CtAbRGXV69FqeCqcNApA+Jbq308dko0W+MFdFe+9t2RGubUkpPxpJcbWy/dN2j4LiuENu1T7nWn0ZAQ==
dependencies:
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/react@^7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.65.0.tgz#98c044bc2d7a99da7dfdef2686c3214d8f2f4ee0"
integrity sha512-1ABxHwEHw5J4avUr8TBch3l7UszbNIroWergwiLPSy+EJU8WuB3Fdx0zSU+hS4Sujf8HNcRgu1JyWThZFTnIMA==
dependencies:
"@sentry/browser" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
hoist-non-react-statics "^3.3.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/replay@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.65.0.tgz#e73a8a577c8b492c3f18ab769db15993b96e77fe"
integrity sha512-vhlk5F9RrhMQ+gOjNlLoWXamAPLNIT6wNII1O9ae+DRhZFmiUYirP5ag6dH5lljvNZndKl+xw+lJGJ3YdjXKlQ==
dependencies:
"@sentry/core" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
"@sentry/types@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.65.0.tgz#f0f4e6583c631408d15ee5fb46901fd195fa1cc4"
integrity sha512-YYq7IDLLhpSBTmHoyWFtq/5ZDaEJ01r7xGuhB0aSIq33cm2I7im/B3ipzoOP/ukGZSIhuYVW9t531xZEO0+6og==
"@sentry/utils@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.65.0.tgz#a7929c5b019fa33e819b08a99744fa27cd38c85f"
integrity sha512-2JEBf4jzRSClhp+LJpX/E3QgHEeKvXqFMeNhmwQ07qqd6szhfH2ckYFj4gXk6YiGGY4Act3C6oxLfdZovG71bw==
dependencies:
"@sentry/types" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
@ -4031,7 +4095,7 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hoist-non-react-statics@^3.3.0:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -5763,6 +5827,11 @@ tslib@^2.0.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==
"tslib@^2.4.1 || ^1.9.3":
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@~2.4:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"