Compare commits

...

23 Commits

Author SHA1 Message Date
8c4543b333 void 2024-04-08 18:08:55 -05:00
ce36f09064 Merge branch 'main' into achalmers/update-h2 2024-01-22 10:45:59 +11:00
3ed263da6b Grackle: Tests for computed properties (#1303)
These tests don't pass, because Grackle doesn't support computed properties yet. But they're worth committing anyway, so I put "#[ignore]" on them.
2024-01-22 10:45:48 +11:00
bc7e1f4e19 Rust: Update h2 2024-01-22 10:29:06 +11:00
d59c4a2258 Grackle: Compile member expressions (#1290)
Member expressions like "obj.property" just look up "property" under the binding for "obj".
2024-01-12 14:42:42 -06:00
9c8351ea40 get off ts-rs fork (#1288)
* get off ts-rs fork

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-01-11 15:31:35 -08:00
db98bcf2a0 throttle scroll zoom (#1287) 2024-01-12 09:14:37 +11:00
15d96a072d Tiny refactors to Grackle (#1286)
- Move bindings into their own scope
- Remove visitor type
2024-01-11 12:38:08 -06:00
088968c664 Grackle (KCL to EP compiler) (#1270)
* Start Grackle (KCL-to-EP compiler)

This begins work on a second, different executor. The old executor is a tree-walk interpreter, this executor compiles the KCL programs into the Execution Plan virtual machine defined in its [own crate](https://github.com/KittyCAD/modeling-api/tree/main/execution-plan). This executor is called "Grackle", after an Austin bird, and it's got its own module in wasm-lib so that I can keep merging small PRs and developing incrementally, rather than building a complete executor which replaces the old executor in one PR.

Grackle's "Planner" walks the AST, like the tree-walk executor. But it doesn't actually execute code. Instead, as it walks each AST node, it outputs a sequence of Execution Plan instructions which, when run, can compute that node's value. It also notes which Execution Plan virtual machine address will eventually contain each KCL variable.

Done:
 - Storing KCL variables
 - Computing primitives, literals, binary expressions
 - Calling native (i.e. Rust) functions from KCL
 - Storing arrays

Todo:
- KCL functions (i.e. user-defined functions)
- Member expressions
- Port over existing executor's native funtions (e.g. `lineTo`, `extrude` and `startSketchAt`)
2024-01-11 09:25:10 -06:00
4bbf98bc34 Bump follow-redirects from 1.15.2 to 1.15.4 (#1278)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 15:00:24 -06:00
ca08f5b337 Ignore test that stack overflows (#1282)
Execution plans will eventually fix this bug.
2024-01-09 14:58:31 -06:00
a3649d09c0 no more need for ffmpeg (#1277)
twenty-twenty 0.7 makes the ffmpeg support optional and puts it behind a feature flag. We aren't using its ffmpeg support here.
2024-01-08 21:22:53 -06:00
635cb58036 Bump vite from 4.5.0 to 4.5.1 (#1180)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-01-04 22:33:03 -06:00
7f050b114f Bump unsafe-libyaml from 0.2.9 to 0.2.10 in /src/wasm-lib (#1247)
Bumps [unsafe-libyaml](https://github.com/dtolnay/unsafe-libyaml) from 0.2.9 to 0.2.10.
- [Release notes](https://github.com/dtolnay/unsafe-libyaml/releases)
- [Commits](https://github.com/dtolnay/unsafe-libyaml/compare/0.2.9...0.2.10)

---
updated-dependencies:
- dependency-name: unsafe-libyaml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 22:32:52 -06:00
c999819450 Tauri e2e coverage: check filesystem settings, create/open file (#1191)
* Create a file and expect stream to fail on Linux
Fixes #1190

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Try to add @franknoirot's suggestion

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Check settings first

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Working test

* Clean up

* Linux fix

* Linux fix attempt #2

* BUILD_RELEASE true temporarily

* Revert "BUILD_RELEASE true temporarily"

This reverts commit 42b2d5f6bb.

* Better comment

* Home checks, and proj name check

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Open proj

* Fix defaultDir in test

* WIP signout

* Workaround to recover from error

* Typo

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-04 04:54:07 -05:00
82905caad6 Bump kittycad (#1262) 2024-01-02 19:13:41 +00:00
519e6d74ac fix domain (#1263)
* fix domain

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-01-02 11:10:06 -08:00
edb7d68c05 A failed build-test-apps job on a specific OS should cancel all the other jobs (#1258)
Fixes #1257
2024-01-02 04:49:35 -05:00
345dd45caa Stop the upload of broken Linux builds (#1256)
* Stop the upload of broken Linux builds
Fixes #1255

* Back to Zoo
2024-01-02 04:43:18 -05:00
b6a5f133f3 Migrate env variables to zoo.dev (#1243) 2023-12-20 22:43:13 +00:00
bc6407be6e Cut release v0.14.0 (#1229)
* Cut release v0.14.0

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* To revert: add test-json stage

* Revert "To revert: add test-json stage"

This reverts commit cf04583e7a.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-20 11:26:11 -05:00
038409124a Frank branding tweaks (#1235)
* Naming tweaks

* Update heading font to be owners

* Update app icon

* Update Tauri App title

* Fix sign in page (#1232)

* Change to Zoo Modeling App, CI fixes (#1238)

* Replace website urls for dl.zoo.dev

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2023-12-19 14:19:34 -05:00
d5567f8602 Use derive-docs from crates.io (#1237) 2023-12-19 11:24:44 -06:00
79 changed files with 2992 additions and 336 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime
skip: **/target,node_modules,build
skip: **/target,node_modules,build,**/Cargo.lock

View File

@ -1,6 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_SENTRY_DSN=

View File

@ -1,6 +1,6 @@
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_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

@ -43,17 +43,6 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: Run clippy
run: |
cd "${{ matrix.dir }}"

View File

@ -44,16 +44,6 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: cargo test
shell: bash
run: |-

View File

@ -104,7 +104,7 @@ jobs:
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/nightly/last_update.json' \
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
@ -123,6 +123,7 @@ jobs:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
@ -243,6 +244,7 @@ jobs:
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
@ -257,7 +259,7 @@ jobs:
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
@ -271,26 +273,24 @@ jobs:
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*itty*
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
@ -304,10 +304,6 @@ jobs:
"signature": $darwin_sig,
"url": $darwin_url
},
"linux-x86_64": {
"signature": $linux_sig,
"url": $linux_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
@ -318,14 +314,13 @@ jobs:
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
@ -334,9 +329,6 @@ jobs:
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
@ -358,7 +350,7 @@ jobs:
uses: google-github-actions/upload-cloud-storage@v2.0.0
with:
path: artifact
glob: '*/*itty*'
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
@ -378,4 +370,4 @@ jobs:
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v1
with:
files: artifact/*/*itty*
files: 'artifact/*/Zoo*'

View File

@ -94,7 +94,6 @@ For running the rust (not tauri rust though) only, you can
cd src/wasm-lib
cargo test
```
but you will need to have install ffmpeg prior to.
## Tauri

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,19 +1,26 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
describe('KCMA (Tauri, Linux)', () => {
it('opens the auth page, signs in, and signs out', async () => {
// Clean up previous tests
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects`
const userCodeDir = '/tmp/kittycad_user_code'
async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
await browser.execute('arguments[0].click();', element)
}
describe('ZMA (Tauri, Linux)', () => {
it('opens the auth page and signs in', async () => {
// Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm('/tmp/kittycad_user_code', { force: true })
await browser.execute('window.localStorage.clear()')
await fs.rm(defaultDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true })
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await signInButton.waitForClickable()
await browser.execute('arguments[0].click();', signInButton)
await click(signInButton)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs
@ -49,14 +56,51 @@ describe('KCMA (Tauri, Linux)', () => {
// Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file')
})
// So let's sign out!
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await menuButton.waitForClickable()
await browser.execute('arguments[0].click();', menuButton)
await click(menuButton)
const settingsButton = await $('[data-testid="settings-button"]')
await click(settingsButton)
const defaultDirInput = await $('[data-testid="default-directory-input"]')
expect(await defaultDirInput.getValue()).toEqual(defaultDir)
const nameInput = await $('[data-testid="name-input"]')
expect(await nameInput.getValue()).toEqual('project-$nnn')
const closeButton = await $('[data-testid="close-button"]')
await click(closeButton)
})
it('checks that no file exists, creates a new file', async () => {
const homeSection = await $('[data-testid="home-section"]')
expect(await homeSection.getText()).toContain('No Projects found')
const newFileButton = await $('[data-testid="home-new-file"]')
await click(newFileButton)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await homeSection.getText()).toContain('project-000')
})
it('opens the new file and expects an error on Linux', async () => {
const projectLink = await $('[data-testid="project-link"]')
await click(projectLink)
const error = await $('h3')
expect(await error.getText()).toContain(
"Can't find variable: RTCPeerConnection"
)
await browser.execute('window.location.href = "tauri://localhost/home"')
})
it('signs out', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
await signoutButton.waitForClickable()
await browser.execute('arguments[0].click();', signoutButton)
await click(signoutButton)
const newSignInButton = await $('[data-testid="sign-in-button"]')
expect(await newSignInButton.getText()).toEqual('Sign in')
})

View File

@ -11,8 +11,13 @@
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
<title>Modeling App</title>
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
<script
defer
data-domain="app.zoo.dev"
src="https://plausible.corp.zoo.dev/js/script.js"
></script>
<title>Zoo Modeling App</title>
</head>
<body class="body-bg">
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.13.0",
"version": "0.14.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.10.2",
@ -136,7 +136,7 @@
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6",
"vite": "^4.5.0",
"vite": "^4.5.1",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.1",
"wait-on": "^7.2.0",

View File

@ -4,9 +4,9 @@
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
### KittyCAD Modeling App (KCMA)
### Zoo Modeling App (ZMA)
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
What we are introducing to you is our Zoo Modeling App (ZMA). ZMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. ZMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
### Why Code?
@ -18,11 +18,11 @@ Plenty of you have professional CAD experience, and may not understand why codin
- Reproducibility
- Easier integration with other tools
### Before You Use KCMA
### Before You Use ZMA
Before you dive straight into the app, we wanted to lay some expectations out for you.
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- ZMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. ZMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure its not a duplicate or its feasible for the current state of the app.
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:

View File

@ -1,6 +1,6 @@
{
"short_name": "KCMA",
"name": "KittyCAD Modeling App",
"short_name": "ZMA",
"name": "Zoo Modeling App",
"icons": [
{
"src": "favicon.ico",

6
src-tauri/Cargo.lock generated
View File

@ -1242,9 +1242,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.20"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
@ -1252,7 +1252,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 1.9.3",
"indexmap 2.0.0",
"slab",
"tokio",
"tokio-util",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -6,8 +6,8 @@
"distDir": "../build"
},
"package": {
"productName": "kittycad-modeling",
"version": "0.13.0"
"productName": "zoo-modeling-app",
"version": "0.14.0"
},
"tauri": {
"allowlist": {
@ -84,7 +84,7 @@
"fullscreen": false,
"height": 1200,
"resizable": true,
"title": "KittyCAD Modeling",
"title": "Zoo Modeling App",
"width": 1800
}
]

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "KittyCAD Modeling"
"productName": "Zoo Modeling App"
}
}

View File

@ -4,7 +4,7 @@
"updater": {
"active": true,
"endpoints": [
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "KittyCAD Modeling"
"productName": "Zoo Modeling App"
}
}

View File

@ -112,6 +112,7 @@ export type ProjectWithEntryPointMetadata = FileEntry & {
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
@ -259,6 +260,7 @@ const router = createBrowserRouter(
const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || ''
)
let newDefaultDirectory: string | undefined = undefined
if (projectDir !== persistedSettings.defaultDirectory) {
localStorage.setItem(
SETTINGS_PERSIST_KEY,
@ -267,6 +269,7 @@ const router = createBrowserRouter(
defaultDirectory: projectDir,
})
)
newDefaultDirectory = projectDir
}
const projectsNoMeta = (await readDir(projectDir)).filter(
isProjectDirectory
@ -282,6 +285,7 @@ const router = createBrowserRouter(
return {
projects,
newDefaultDirectory,
}
},
children: [

View File

@ -19,7 +19,7 @@ const DownloadAppBanner = () => {
<div className="max-w-3xl mx-auto">
<div className="flex gap-2 justify-between items-start">
<h2 className="text-xl font-bold mb-4">
Zoo Modeling App is better as a desktop app!
Modeling App is better as a desktop app!
</h2>
<ActionButton
Element="button"

View File

@ -107,6 +107,7 @@ function ProjectCard({
<Link
className="flex-1 text-liquid-100 after:content-[''] after:absolute after:inset-0"
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
data-testid="project-link"
>
{project.name?.replace(FILE_EXT, '')}
</Link>

View File

@ -7,7 +7,7 @@ import {
} from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils'
import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
@ -115,9 +115,9 @@ export const Stream = ({ className = '' }) => {
setClickCoords({ x, y })
}
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -126,7 +126,7 @@ export const Stream = ({ className = '' }) => {
},
cmd_id: uuidv4(),
})
}
}, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
clientX,

View File

@ -128,6 +128,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
: paths.HOME + paths.SETTINGS
navigate(targetPath)
}}
data-testid="settings-button"
>
Settings
</ActionButton>

View File

@ -6,9 +6,7 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
@apply font-sans;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-chalkboard-110;
@ -17,6 +15,15 @@ body {
scrollbar-color: var(--color-chalkboard-20) var(--color-chalkboard-40);
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-display;
}
.body-bg {
@apply bg-chalkboard-10;
}

View File

@ -45,7 +45,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
} else if (!currentPath) {
return [0, 0]
}
if (currentPath.type === 'toPoint') {
if (currentPath.type === 'topoint') {
return [currentPath.to[0], currentPath.to[1]]
}
return [0, 0]

View File

@ -10,7 +10,7 @@ import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api'
const PROJECT_FOLDER = 'kittycad-modeling-projects'
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
export const FILE_EXT = '.kcl'
export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT
const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
@ -38,7 +38,7 @@ export async function initializeProjectDirectory(directory: string) {
docDirectory = await documentDir()
} catch (e) {
console.log('error', e)
docDirectory = await homeDir() // seems to work better on Linux
docDirectory = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions)
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER

View File

@ -37,13 +37,20 @@ import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
const Home = () => {
const { commandBarSend } = useCommandsContext()
const navigate = useNavigate()
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
const { projects: loadedProjects, newDefaultDirectory } =
useLoaderData() as HomeLoaderData
const {
settings: {
context: { defaultDirectory, defaultProjectName },
send: sendToSettings,
},
} = useGlobalStateContext()
if (newDefaultDirectory) {
sendToSettings({
type: 'Set Default Directory',
data: { defaultDirectory: newDefaultDirectory },
})
}
const [state, send] = useMachine(homeMachine, {
context: {
@ -177,7 +184,7 @@ const Home = () => {
<AppHeader showToolbar={false} />
<div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
<section className="flex justify-between">
<h1 className="text-3xl text-bold">Your Projects</h1>
<h1 className="text-3xl font-bold">Your Projects</h1>
<div className="flex gap-2 items-center">
<small>Sort by</small>
<ActionButton
@ -222,7 +229,7 @@ const Home = () => {
</ActionButton>
</div>
</section>
<section>
<section data-testid="home-section">
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
Loaded from{' '}
<span className="text-energy-70 dark:text-energy-40">

View File

@ -18,7 +18,7 @@ export default function CmdK() {
(buttonDownInStream ? '' : ' pointer-events-auto')
}
>
<h2 className="text-2xl">Command Bar</h2>
<h2 className="text-2xl font-bold">Command Bar</h2>
<p className="my-4">
Press{' '}
{platformName === 'darwin' ? (

View File

@ -22,7 +22,7 @@ export default function CodeEditor() {
}
>
<section className="flex-1">
<h2 className="text-2xl">
<h2 className="text-2xl font-bold">
Editing code with <code>kcl</code>
</h2>
<p className="my-4">

View File

@ -18,7 +18,7 @@ export default function Export() {
}
>
<section className="flex-1">
<h2 className="text-2xl">Export</h2>
<h2 className="text-2xl font-bold">Export</h2>
<p className="my-4">
Try opening the project menu and clicking "Export Model".
</p>

View File

@ -22,7 +22,7 @@ export default function InteractiveNumbers() {
}
>
<section className="flex-1 overflow-y-auto mb-6">
<h2 className="text-2xl">Interactive Numbers</h2>
<h2 className="text-2xl font-bold">Interactive Numbers</h2>
<p className="my-4">
Let's do a little bit of hybrid editing to this part.
</p>

View File

@ -34,7 +34,9 @@ export default function ParametricModeling() {
}
>
<section className="flex-1 overflow-y-auto mb-6">
<h2 className="text-2xl">Towards true parametric modeling</h2>
<h2 className="text-2xl font-bold">
Towards true parametric modeling
</h2>
<p className="my-4">
This example script shows how having access to the code
representation of a part can allow us to do things that are tedious

View File

@ -18,12 +18,12 @@ export default function ProjectMenu() {
}
>
<section className="flex-1">
<h2 className="text-2xl">Project Menu</h2>
<h2 className="text-2xl font-bold">Project Menu</h2>
<p className="my-4">
Click on Kitt in the upper left to open the project menu. You can
only {isTauri() && 'go home or '}export your modelwhich we'll talk
about next—for now. We'll add more options here soon, especially as
we add support for multi-file assemblies.
Click on the Zoo logo in the upper left to open the project menu.
You can only {isTauri() && 'go home or '}export your modelwhich
we'll talk about next—for now. We'll add more options here soon,
especially as we add support for multi-file assemblies.
</p>
</section>
<OnboardingButtons

View File

@ -17,23 +17,23 @@ export default function Streaming() {
}
>
<section className="flex-1">
<h2 className="text-2xl">Streaming Video</h2>
<h2 className="text-2xl font-bold">Streaming Video</h2>
<p className="my-4">
The 3D view is not running on your computer. Instead, our
infrastructure spins up the Zoo Geometry Engine on a remote GPU, Zoo
infrastructure spins up our Geometry Engine on a remote GPU,
Modeling App sends it a series of commands via Websockets and
WebRTC, and the Geometry Engine sends back a video stream of the 3D
view.
</p>
<p className="my-4">
This means that you could run Zoo Modeling App on a Chromebook, a
This means that you could run our Modeling App on a Chromebook, a
tablet, or even a phone, as long as you have a good internet
connection.
</p>
<p className="my-4">
It also means that whatever tools you build on top of the Zoo
Geometry Engine will be able to run on any device with a browser,
and you won't have to worry about the performance of the device.
It also means that whatever tools you build on top of our Geometry
Engine will be able to run on any device with a browser, and you
won't have to worry about the performance of the device.
</p>
</section>
<OnboardingButtons

View File

@ -17,7 +17,7 @@ export default function UserMenu() {
}
>
<section className="flex-1">
<h2 className="text-2xl">User Menu</h2>
<h2 className="text-2xl font-bold">User Menu</h2>
<p className="my-4">
Click your avatar on the upper right to open the user menu. You can
change your settings, sign out, or request a feature.

View File

@ -113,6 +113,7 @@ export const Settings = () => {
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="hover:border-destroy-40"
data-testid="close-button"
>
Close
</ActionButton>
@ -178,6 +179,7 @@ export const Settings = () => {
className="flex-1 px-2 bg-transparent"
value={defaultDirectory}
disabled
data-testid="default-directory-input"
/>
<ActionButton
Element="button"
@ -209,6 +211,7 @@ export const Settings = () => {
}}
autoCapitalize="off"
autoComplete="off"
data-testid="name-input"
/>
</SettingsSection>
</>
@ -326,7 +329,7 @@ export function SettingsSection({
}
>
<div className="w-80">
<h2 className="text-2xl">{title}</h2>
<h2 className="text-2xl font-bold">{title}</h2>
<p className="mt-2 text-sm">{description}</p>
</div>
<div>{children}</div>

View File

@ -8,6 +8,11 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { APP_NAME } from 'lib/constants'
const SignIn = () => {
const getLogoTheme = () =>
theme === Themes.Light ||
(theme === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
const {
auth: { send },
settings: {
@ -35,15 +40,10 @@ const SignIn = () => {
<div className="max-w-2xl mx-auto">
<div>
<img
src={`/kittycad-logomark${
appliedTheme === Themes.Dark ? '-light' : ''
}.svg`}
alt="KittyCAD"
src={`/zma-logomark${getLogoTheme()}.svg`}
alt="Zoo Modeling App"
className="w-48 inline-block"
/>
<span className="text-3xl leading-none w-auto inline-block align-middle ml-2">
Modeling App
</span>
</div>
<h1 className="font-bold text-2xl mt-12 mb-6">
Sign in to get started with the {APP_NAME}

1733
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -25,8 +25,8 @@ image = "0.24.7"
kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0"
reqwest = { version = "0.11.22", default-features = false }
tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.6.1"
tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.7"
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
@ -52,12 +52,17 @@ debug = true
[workspace]
members = [
"derive-docs",
"grackle",
"kcl",
"kcl-macros",
]
[workspace.dependencies]
kittycad = { version = "0.2.43", default-features = false, features = ["js"] }
kittycad = { version = "0.2.45", default-features = false, features = ["js"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = "0.1.2"
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
[[test]]
name = "executor"

View File

@ -0,0 +1,18 @@
[package]
name = "grackle"
version = "0.1.0"
edition = "2021"
description = "A new executor for KCL which compiles to Execution Plans"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
kcl-lib = { path = "../kcl" }
kittycad-execution-plan = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
kittycad-modeling-session = { workspace = true }
thiserror = "1.0.56"
[dev-dependencies]
pretty_assertions = "1"

View File

@ -0,0 +1,145 @@
use kcl_lib::ast::types::LiteralIdentifier;
use kcl_lib::ast::types::LiteralValue;
use crate::CompileError;
use super::native_functions;
use super::Address;
use super::KclFunction;
use super::String2;
use std::collections::HashMap;
/// KCL values which can be written to KCEP memory.
/// This is recursive. For example, the bound value might be an array, which itself contains bound values.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum EpBinding {
/// A KCL value which gets stored in a particular address in KCEP memory.
Single(Address),
/// A sequence of KCL values, indexed by their position in the sequence.
Sequence(Vec<EpBinding>),
/// A sequence of KCL values, indexed by their identifier.
Map(HashMap<String, EpBinding>),
}
impl EpBinding {
/// Look up the given property of this binding.
pub fn property_of(&self, property: LiteralIdentifier) -> Result<&Self, CompileError> {
match property {
LiteralIdentifier::Identifier(_) => todo!("Support identifier properties"),
LiteralIdentifier::Literal(litval) => match litval.value {
// Arrays can be indexed by integers.
LiteralValue::IInteger(i) => match self {
EpBinding::Single(_) => Err(CompileError::CannotIndex),
EpBinding::Sequence(seq) => {
let i = usize::try_from(i).map_err(|_| CompileError::InvalidIndex(i.to_string()))?;
seq.get(i).ok_or(CompileError::IndexOutOfBounds { i, len: seq.len() })
}
EpBinding::Map(_) => Err(CompileError::CannotIndex),
},
// Objects can be indexed by string properties.
LiteralValue::String(property) => match self {
EpBinding::Single(_) => Err(CompileError::NoProperties),
EpBinding::Sequence(_) => Err(CompileError::ArrayDoesNotHaveProperties),
EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }),
},
// It's never valid to index by a fractional number.
LiteralValue::Fractional(num) => Err(CompileError::InvalidIndex(num.to_string())),
},
}
}
}
/// A set of bindings in a particular scope.
/// Bindings are KCL values that get "compiled" into KCEP values, which are stored in KCEP memory
/// at a particular KCEP address.
/// Bindings are referenced by the name of their KCL identifier.
///
/// KCL has multiple scopes -- each function has a scope for its own local variables and parameters.
/// So when referencing a variable, it might be in this scope, or the parent scope. So, each environment
/// has to keep track of parent environments. The root environment has no parent, and is used for KCL globals
/// (e.g. the prelude of stdlib functions).
///
/// These are called "Environments" in the "Crafting Interpreters" book.
#[derive(Debug)]
pub struct BindingScope {
// KCL value which are stored in EP memory.
ep_bindings: HashMap<String, EpBinding>,
/// KCL functions. They do NOT get stored in EP memory.
function_bindings: HashMap<String2, Box<dyn KclFunction>>,
parent: Option<Box<BindingScope>>,
}
impl BindingScope {
/// The parent scope for every program, before the user has defined anything.
/// Only includes some stdlib functions.
/// This is usually known as the "prelude" in other languages. It's the stdlib functions that
/// are already imported for you when you start coding.
pub fn prelude() -> Self {
Self {
// TODO: Actually put the stdlib prelude in here,
// things like `startSketchAt` and `line`.
function_bindings: HashMap::from([
("id".into(), Box::new(native_functions::Id) as _),
("add".into(), Box::new(native_functions::Add) as _),
]),
ep_bindings: Default::default(),
parent: None,
}
}
/// Add a new scope, e.g. for new function calls.
#[allow(dead_code)] // TODO: when we implement function expressions.
pub fn add_scope(self) -> Self {
Self {
function_bindings: Default::default(),
ep_bindings: Default::default(),
parent: Some(Box::new(self)),
}
}
//// Remove a scope, e.g. when exiting a function call.
#[allow(dead_code)] // TODO: when we implement function expressions.
pub fn remove_scope(self) -> Self {
*self.parent.unwrap()
}
/// Add a binding (e.g. defining a new variable)
pub fn bind(&mut self, identifier: String, binding: EpBinding) {
self.ep_bindings.insert(identifier, binding);
}
/// Look up a binding.
pub fn get(&self, identifier: &str) -> Option<&EpBinding> {
if let Some(b) = self.ep_bindings.get(identifier) {
// The name was found in this scope.
Some(b)
} else if let Some(ref parent) = self.parent {
// Check the next scope outwards.
parent.get(identifier)
} else {
// There's no outer scope, and it wasn't found, so there's nowhere else to look.
None
}
}
/// Look up a function bound to the given identifier.
pub fn get_fn(&self, identifier: &str) -> GetFnResult {
if let Some(f) = self.function_bindings.get(identifier) {
GetFnResult::Found(f.as_ref())
} else if self.get(identifier).is_some() {
GetFnResult::NonCallable
} else if let Some(ref parent) = self.parent {
parent.get_fn(identifier)
} else {
GetFnResult::NotFound
}
}
}
pub enum GetFnResult<'a> {
Found(&'a dyn KclFunction),
NonCallable,
NotFound,
}

View File

@ -0,0 +1,84 @@
use kcl_lib::ast::{self, types::BinaryPart};
/// Basically the same enum as `kcl_lib::ast::types::Value`, but grouped according to whether the
/// value is singular or composite.
/// You can convert losslessly between KclValueGroup and `kcl_lib::ast::types::Value` with From/Into.
pub enum KclValueGroup {
Single(SingleValue),
ArrayExpression(Box<ast::types::ArrayExpression>),
ObjectExpression(Box<ast::types::ObjectExpression>),
}
pub enum SingleValue {
Literal(Box<ast::types::Literal>),
Identifier(Box<ast::types::Identifier>),
BinaryExpression(Box<ast::types::BinaryExpression>),
CallExpression(Box<ast::types::CallExpression>),
PipeExpression(Box<ast::types::PipeExpression>),
UnaryExpression(Box<ast::types::UnaryExpression>),
KclNoneExpression(ast::types::KclNone),
MemberExpression(Box<ast::types::MemberExpression>),
}
impl From<ast::types::BinaryPart> for KclValueGroup {
fn from(value: ast::types::BinaryPart) -> Self {
match value {
BinaryPart::Literal(e) => Self::Single(SingleValue::Literal(e)),
BinaryPart::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
BinaryPart::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
BinaryPart::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
BinaryPart::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
BinaryPart::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
}
}
}
impl From<ast::types::BinaryPart> for SingleValue {
fn from(value: ast::types::BinaryPart) -> Self {
match value {
BinaryPart::Literal(e) => Self::Literal(e),
BinaryPart::Identifier(e) => Self::Identifier(e),
BinaryPart::BinaryExpression(e) => Self::BinaryExpression(e),
BinaryPart::CallExpression(e) => Self::CallExpression(e),
BinaryPart::UnaryExpression(e) => Self::UnaryExpression(e),
BinaryPart::MemberExpression(e) => Self::MemberExpression(e),
}
}
}
impl From<ast::types::Value> for KclValueGroup {
fn from(value: ast::types::Value) -> Self {
match value {
ast::types::Value::Literal(e) => Self::Single(SingleValue::Literal(e)),
ast::types::Value::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
ast::types::Value::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
ast::types::Value::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
ast::types::Value::PipeExpression(e) => Self::Single(SingleValue::PipeExpression(e)),
ast::types::Value::None(e) => Self::Single(SingleValue::KclNoneExpression(e)),
ast::types::Value::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e),
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
ast::types::Value::PipeSubstitution(_) | ast::types::Value::FunctionExpression(_) => todo!(),
}
}
}
impl From<KclValueGroup> for ast::types::Value {
fn from(value: KclValueGroup) -> Self {
match value {
KclValueGroup::Single(e) => match e {
SingleValue::Literal(e) => ast::types::Value::Literal(e),
SingleValue::Identifier(e) => ast::types::Value::Identifier(e),
SingleValue::BinaryExpression(e) => ast::types::Value::BinaryExpression(e),
SingleValue::CallExpression(e) => ast::types::Value::CallExpression(e),
SingleValue::PipeExpression(e) => ast::types::Value::PipeExpression(e),
SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e),
SingleValue::KclNoneExpression(e) => ast::types::Value::None(e),
SingleValue::MemberExpression(e) => ast::types::Value::MemberExpression(e),
},
KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
}
}
}

View File

@ -0,0 +1,425 @@
mod binding_scope;
mod kcl_value_group;
mod native_functions;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use kcl_lib::{
ast,
ast::types::{BodyItem, KclNone, LiteralValue, Program},
};
use kittycad_execution_plan as ep;
use kittycad_execution_plan::{Address, ExecutionError, Instruction};
use kittycad_execution_plan_traits as ept;
use kittycad_execution_plan_traits::NumericPrimitive;
use kittycad_modeling_session::Session;
use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
use self::kcl_value_group::{KclValueGroup, SingleValue};
/// Execute a KCL program by compiling into an execution plan, then running that.
pub async fn execute(ast: Program, session: Session) -> Result<(), Error> {
let mut planner = Planner::new();
let plan = planner.build_plan(ast)?;
let mut mem = kittycad_execution_plan::Memory::default();
kittycad_execution_plan::execute(&mut mem, plan, session).await?;
Ok(())
}
/// Compiles KCL programs into Execution Plans.
struct Planner {
/// Maps KCL identifiers to what they hold, and where in KCEP virtual memory they'll be written to.
binding_scope: BindingScope,
/// Next available KCEP virtual machine memory address.
next_addr: Address,
}
impl Planner {
pub fn new() -> Self {
Self {
binding_scope: BindingScope::prelude(),
next_addr: Address::ZERO,
}
}
fn build_plan(&mut self, program: Program) -> PlanRes {
program.body.into_iter().try_fold(Vec::new(), |mut instructions, item| {
let instructions_for_this_node = match item {
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
KclValueGroup::Single(value) => self.plan_to_compute_single(value)?.instructions,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
},
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
BodyItem::ReturnStatement(_) => todo!(),
};
instructions.extend(instructions_for_this_node);
Ok(instructions)
})
}
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
/// Returns the instructions, and the destination address of the value.
fn plan_to_compute_single(&mut self, value: SingleValue) -> Result<EvalPlan, CompileError> {
match value {
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => {
let address = self.next_addr.offset_by(1);
Ok(EvalPlan {
instructions: vec![Instruction::SetPrimitive {
address,
value: ept::Primitive::Nil,
}],
binding: EpBinding::Single(address),
})
}
SingleValue::Literal(expr) => {
let kcep_val = kcl_literal_to_kcep_literal(expr.value);
// KCEP primitives always have size of 1, because each address holds 1 primitive.
let size = 1;
let address = self.next_addr.offset_by(size);
Ok(EvalPlan {
instructions: vec![Instruction::SetPrimitive {
address,
value: kcep_val,
}],
binding: EpBinding::Single(address),
})
}
SingleValue::Identifier(expr) => {
// This is just duplicating a binding.
// So, don't emit any instructions, because the value has already been computed.
// Just return the address that it was stored at after being computed.
let previously_bound_to = self
.binding_scope
.get(&expr.name)
.ok_or(CompileError::Undefined { name: expr.name })?;
Ok(EvalPlan {
instructions: Vec::new(),
binding: previously_bound_to.clone(),
})
}
SingleValue::BinaryExpression(expr) => {
let l = self.plan_to_compute_single(SingleValue::from(expr.left))?;
let r = self.plan_to_compute_single(SingleValue::from(expr.right))?;
let EpBinding::Single(l_binding) = l.binding else {
return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math",
));
};
let EpBinding::Single(r_binding) = r.binding else {
return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math",
));
};
let destination = self.next_addr.offset_by(1);
let mut plan = Vec::with_capacity(l.instructions.len() + r.instructions.len() + 1);
plan.extend(l.instructions);
plan.extend(r.instructions);
plan.push(Instruction::Arithmetic {
arithmetic: ep::Arithmetic {
operation: match expr.operator {
ast::types::BinaryOperator::Add => ep::Operation::Add,
ast::types::BinaryOperator::Sub => ep::Operation::Sub,
ast::types::BinaryOperator::Mul => ep::Operation::Mul,
ast::types::BinaryOperator::Div => ep::Operation::Div,
ast::types::BinaryOperator::Mod => {
todo!("execution plan instruction set doesn't support Mod yet")
}
ast::types::BinaryOperator::Pow => {
todo!("execution plan instruction set doesn't support Pow yet")
}
},
operand0: ep::Operand::Reference(l_binding),
operand1: ep::Operand::Reference(r_binding),
},
destination,
});
Ok(EvalPlan {
instructions: plan,
binding: EpBinding::Single(destination),
})
}
SingleValue::CallExpression(expr) => {
let (mut instructions, args) = expr.arguments.into_iter().try_fold(
(Vec::new(), Vec::new()),
|(mut acc_instrs, mut acc_args), argument| {
let EvalPlan {
instructions: new_instructions,
binding: arg,
} = match KclValueGroup::from(argument) {
KclValueGroup::Single(value) => self.plan_to_compute_single(value)?,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
};
acc_instrs.extend(new_instructions);
acc_args.push(arg);
Ok((acc_instrs, acc_args))
},
)?;
let callee = match self.binding_scope.get_fn(&expr.callee.name) {
GetFnResult::Found(f) => f,
GetFnResult::NonCallable => {
return Err(CompileError::NotCallable {
name: expr.callee.name.clone(),
});
}
GetFnResult::NotFound => {
return Err(CompileError::Undefined {
name: expr.callee.name.clone(),
})
}
};
let EvalPlan {
instructions: eval_instrs,
binding,
} = callee.call(&mut self.next_addr, args)?;
instructions.extend(eval_instrs);
Ok(EvalPlan { instructions, binding })
}
SingleValue::MemberExpression(mut expr) => {
let parse = move || {
let mut stack = Vec::new();
loop {
stack.push((expr.property, expr.computed));
match expr.object {
ast::types::MemberObject::MemberExpression(subexpr) => {
expr = subexpr;
}
ast::types::MemberObject::Identifier(id) => return (stack, id),
}
}
};
let (properties, id) = parse();
let name = id.name;
let mut binding = self.binding_scope.get(&name).ok_or(CompileError::Undefined { name })?;
for (property, computed) in properties {
if computed {
todo!("Support computed properties like '{:?}'", property);
} else {
binding = binding.property_of(property)?;
}
}
Ok(EvalPlan {
instructions: Vec::new(),
binding: binding.clone(),
})
}
SingleValue::PipeExpression(_) => todo!(),
SingleValue::UnaryExpression(_) => todo!(),
}
}
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
/// Returns the instructions.
/// Also binds the value to a name.
fn plan_to_bind(
&mut self,
declarations: ast::types::VariableDeclaration,
) -> Result<Vec<Instruction>, CompileError> {
declarations
.declarations
.into_iter()
.try_fold(Vec::new(), |mut acc, declaration| {
let (instrs, binding) = self.plan_to_bind_one(declaration.init)?;
self.binding_scope.bind(declaration.id.name, binding);
acc.extend(instrs);
Ok(acc)
})
}
fn plan_to_bind_one(
&mut self,
value_being_bound: ast::types::Value,
) -> Result<(Vec<Instruction>, EpBinding), CompileError> {
match KclValueGroup::from(value_being_bound) {
KclValueGroup::Single(init_value) => {
// Simple! Just evaluate it, note where the final value will be stored in KCEP memory,
// and bind it to the KCL identifier.
let EvalPlan { instructions, binding } = self.plan_to_compute_single(init_value)?;
Ok((instructions, binding))
}
KclValueGroup::ArrayExpression(expr) => {
// First, emit a plan to compute each element of the array.
// Collect all the bindings from each element too.
let (instructions, bindings) = expr.elements.into_iter().try_fold(
(Vec::new(), Vec::new()),
|(mut acc_instrs, mut acc_bindings), element| {
match KclValueGroup::from(element) {
KclValueGroup::Single(value) => {
// If this element of the array is a single value, then binding it is
// straightforward -- you got a single binding, no need to change anything.
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
acc_instrs.extend(instructions);
acc_bindings.push(binding);
}
KclValueGroup::ArrayExpression(expr) => {
// If this element of the array is _itself_ an array, then we need to
// emit a plan to calculate each element of this child array.
// Then we collect the child array's bindings, and bind them to one
// element of the parent array.
let binding = expr
.elements
.into_iter()
.try_fold(Vec::new(), |mut seq, child_element| {
let (instructions, binding) = self.plan_to_bind_one(child_element)?;
acc_instrs.extend(instructions);
seq.push(binding);
Ok(seq)
})
.map(EpBinding::Sequence)?;
acc_bindings.push(binding);
}
KclValueGroup::ObjectExpression(expr) => {
// If this element of the array is an object, then we need to
// emit a plan to calculate each value of each property of the object.
// Then we collect the bindings for each child value, and bind them to one
// element of the parent array.
let map = HashMap::with_capacity(expr.properties.len());
let binding = expr
.properties
.into_iter()
.try_fold(map, |mut map, property| {
let (instructions, binding) = self.plan_to_bind_one(property.value)?;
map.insert(property.key.name, binding);
acc_instrs.extend(instructions);
Ok(map)
})
.map(EpBinding::Map)?;
acc_bindings.push(binding);
}
};
Ok((acc_instrs, acc_bindings))
},
)?;
Ok((instructions, EpBinding::Sequence(bindings)))
}
KclValueGroup::ObjectExpression(expr) => {
// Convert the object to a sequence of key-value pairs.
let mut kvs = expr.properties.into_iter().map(|prop| (prop.key, prop.value));
let (instructions, each_property_binding) = kvs.try_fold(
(Vec::new(), HashMap::new()),
|(mut acc_instrs, mut acc_bindings), (key, value)| {
match KclValueGroup::from(value) {
KclValueGroup::Single(value) => {
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
acc_instrs.extend(instructions);
acc_bindings.insert(key.name, binding);
}
KclValueGroup::ArrayExpression(expr) => {
// If this value of the object is an array, then emit a plan to calculate
// each element of that array. Collect their bindings, and bind them all
// under one property of the parent object.
let n = expr.elements.len();
let binding = expr
.elements
.into_iter()
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
let (instructions, binding) = self.plan_to_bind_one(child_element)?;
seq.push(binding);
acc_instrs.extend(instructions);
Ok(seq)
})
.map(EpBinding::Sequence)?;
acc_bindings.insert(key.name, binding);
}
KclValueGroup::ObjectExpression(expr) => {
// If this value of the object is _itself_ an object, then we need to
// emit a plan to calculate each value of each property of the child object.
// Then we collect the bindings for each child value, and bind them to one
// property of the parent object.
let n = expr.properties.len();
let binding = expr
.properties
.into_iter()
.try_fold(HashMap::with_capacity(n), |mut map, property| {
let (instructions, binding) = self.plan_to_bind_one(property.value)?;
map.insert(property.key.name, binding);
acc_instrs.extend(instructions);
Ok(map)
})
.map(EpBinding::Map)?;
acc_bindings.insert(key.name, binding);
}
};
Ok((acc_instrs, acc_bindings))
},
)?;
Ok((instructions, EpBinding::Map(each_property_binding)))
}
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Clone)]
pub enum CompileError {
#[error("the name {name} was not defined")]
Undefined { name: String },
#[error("the function {fn_name} requires at least {required} arguments but you only supplied {actual}")]
NotEnoughArgs {
fn_name: String2,
required: usize,
actual: usize,
},
#[error("the function {fn_name} accepts at most {maximum} arguments but you supplied {actual}")]
TooManyArgs {
fn_name: String2,
maximum: usize,
actual: usize,
},
#[error("you tried to call {name} but it's not a function")]
NotCallable { name: String },
#[error("you're trying to use an operand that isn't compatible with the given arithmetic operator: {0}")]
InvalidOperand(&'static str),
#[error("you cannot use the value {0} as an index")]
InvalidIndex(String),
#[error("you tried to index into a value that isn't an array. Only arrays have numeric indices!")]
CannotIndex,
#[error("you tried to get the element {i} but that index is out of bounds. The array only has a length of {len}")]
IndexOutOfBounds { i: usize, len: usize },
#[error("you tried to access the property of a value that doesn't have any properties")]
NoProperties,
#[error("you tried to access a property of an array, but arrays don't have properties. They do have numeric indexes though, try using an index e.g. [0]")]
ArrayDoesNotHaveProperties,
#[error(
"you tried to read the '.{property}' of an object, but the object doesn't have any properties with that key"
)]
UndefinedProperty { property: String },
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Compile(#[from] CompileError),
#[error("{0}")]
Execution(#[from] ExecutionError),
}
type PlanRes = Result<Vec<Instruction>, CompileError>;
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be
/// bound to some KCL name and Execution Plan address.
fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive {
match expr {
LiteralValue::IInteger(x) => ept::Primitive::NumericValue(NumericPrimitive::Integer(x)),
LiteralValue::Fractional(x) => ept::Primitive::NumericValue(NumericPrimitive::Float(x)),
LiteralValue::String(x) => ept::Primitive::String(x),
}
}
/// Instructions that can compute some value.
struct EvalPlan {
/// The instructions which will compute the value.
instructions: Vec<Instruction>,
/// Where the value will be stored.
binding: EpBinding,
}
trait KclFunction: std::fmt::Debug {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
}
/// Either an owned string, or a static string. Either way it can be read and moved around.
pub type String2 = std::borrow::Cow<'static, str>;

View File

@ -0,0 +1,105 @@
//! Defines functions which are written in Rust, but called from KCL.
//! This includes some of the stdlib, e.g. `startSketchAt`.
//! But some other stdlib functions will be written in KCL.
use kcl_lib::std::sketch::PlaneData;
use kittycad_execution_plan::{Address, Arithmetic, Instruction};
use kittycad_execution_plan_traits::Value;
use crate::{CompileError, EpBinding, EvalPlan, KclFunction};
/// The identity function. Always returns its first input.
#[derive(Debug)]
pub struct Id;
impl KclFunction for Id {
fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
if args.len() > 1 {
return Err(CompileError::TooManyArgs {
fn_name: "id".into(),
maximum: 1,
actual: args.len(),
});
}
let arg = args
.first()
.ok_or(CompileError::NotEnoughArgs {
fn_name: "id".into(),
required: 1,
actual: 0,
})?
.clone();
Ok(EvalPlan {
instructions: Vec::new(),
binding: arg,
})
}
}
#[derive(Debug)]
pub struct StartSketchAt;
impl KclFunction for StartSketchAt {
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
// Store the plane.
let plane = PlaneData::XY.into_parts();
instructions.push(Instruction::SetValue {
address: next_addr.offset_by(plane.len()),
value_parts: plane,
});
// TODO: Get the plane ID from global context.
// TODO: Send this command:
// ModelingCmd::SketchModeEnable {
// animated: false,
// ortho: false,
// plane_id: plane.id,
// // We pass in the normal for the plane here.
// disable_camera_with_plane: Some(plane.z_axis.clone().into()),
// },
// TODO: Send ModelingCmd::StartPath at the given point.
// TODO (maybe): Store the SketchGroup in KCEP memory.
todo!()
}
}
/// A test function that adds two numbers.
#[derive(Debug)]
pub struct Add;
impl KclFunction for Add {
fn call(&self, next_address: &mut Address, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let len = args.len();
if len > 2 {
return Err(CompileError::TooManyArgs {
fn_name: "add".into(),
maximum: 2,
actual: len,
});
}
let not_enough_args = CompileError::NotEnoughArgs {
fn_name: "add".into(),
required: 2,
actual: len,
};
const ERR: &str = "cannot use composite values (e.g. array) as arguments to Add";
let EpBinding::Single(arg1) = args.pop().ok_or(not_enough_args.clone())? else {
return Err(CompileError::InvalidOperand(ERR));
};
let EpBinding::Single(arg0) = args.pop().ok_or(not_enough_args)? else {
return Err(CompileError::InvalidOperand(ERR));
};
let destination = next_address.offset_by(1);
Ok(EvalPlan {
instructions: vec![Instruction::Arithmetic {
arithmetic: Arithmetic {
operation: kittycad_execution_plan::Operation::Add,
operand0: kittycad_execution_plan::Operand::Reference(arg0),
operand1: kittycad_execution_plan::Operand::Reference(arg1),
},
destination,
}],
binding: EpBinding::Single(destination),
})
}
}

View File

@ -0,0 +1,463 @@
use pretty_assertions::assert_eq;
use super::*;
fn must_plan(program: &str) -> (Vec<Instruction>, BindingScope) {
let tokens = kcl_lib::token::lexer(program);
let parser = kcl_lib::parser::Parser::new(tokens);
let ast = parser.ast().unwrap();
let mut p = Planner::new();
let instrs = p.build_plan(ast).unwrap();
(instrs, p.binding_scope)
}
fn should_not_compile(program: &str) -> CompileError {
let tokens = kcl_lib::token::lexer(program);
let parser = kcl_lib::parser::Parser::new(tokens);
let ast = parser.ast().unwrap();
let mut p = Planner::new();
p.build_plan(ast).unwrap_err()
}
#[test]
fn assignments() {
let program = "
let x = 1
let y = 2";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into(),
}
]
);
}
#[test]
fn bind_array() {
let program = r#"let x = [44, 55, "sixty-six"]"#;
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 44i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 55i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: "sixty-six".to_owned().into(),
}
]
);
}
#[test]
fn bind_nested_array() {
let program = r#"let x = [44, [55, "sixty-six"]]"#;
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 44i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 55i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: "sixty-six".to_owned().into(),
}
]
);
}
#[test]
fn bind_arrays_with_objects_elements() {
let program = r#"let x = [44, {a: 55, b: "sixty-six"}]"#;
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 44i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 55i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: "sixty-six".to_owned().into(),
}
]
);
}
#[test]
fn name_not_found() {
// Users can't assign `y` to anything because `y` is undefined.
let err = should_not_compile("let x = y");
assert_eq!(err, CompileError::Undefined { name: "y".to_owned() });
}
#[test]
fn aliases() {
let program = "
let x = 1
let y = x";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
}]
);
}
#[test]
fn use_native_function_add() {
let program = "let x = add(1,2)";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into()
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into()
},
Instruction::Arithmetic {
arithmetic: ep::Arithmetic {
operation: ep::Operation::Add,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1))
},
destination: Address::ZERO.offset(2),
}
]
);
}
#[test]
fn use_native_function_id() {
let program = "let x = id(2)";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 2i64.into()
}]
);
}
#[test]
#[ignore = "haven't done computed properties yet"]
fn computed_array_index() {
let program = r#"
let array = ["a", "b", "c"]
let index = 1+1
let prop = array[index]
"#;
let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1);
}
other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
}
}
}
#[test]
#[ignore = "haven't done computed properties yet"]
fn computed_member_expressions() {
let program = r#"
let obj = {x: 1, y: 2}
let index = "x"
let prop = obj[index]
"#;
let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1);
}
other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
}
}
}
#[test]
fn member_expressions_object() {
let program = r#"
let obj = {x: 1, y: 2}
let prop = obj["y"]
"#;
let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1);
}
other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
}
}
}
#[test]
fn member_expressions_array() {
let program = "
let array = [[1,2],[3,4]]
let first = array[0][0]
let last = array[1][1]
";
let (_plan, scope) = must_plan(program);
match scope.get("first").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO);
}
other => {
panic!("expected 'number' bound to 0x0 but it was bound to {other:?}");
}
}
match scope.get("last").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 3);
}
other => {
panic!("expected 'number' bound to 0x3 but it was bound to {other:?}");
}
}
}
#[test]
fn add_literals() {
let program = "let x = 1 + 2";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into()
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into()
},
Instruction::Arithmetic {
arithmetic: ep::Arithmetic {
operation: ep::Operation::Add,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1)),
},
destination: Address::ZERO.offset(2),
}
]
);
}
#[test]
fn add_vars() {
let program = "
let one = 1
let two = 2
let x = one + two";
let (plan, _bindings) = must_plan(program);
let addr0 = Address::ZERO;
let addr1 = Address::ZERO.offset(1);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: addr0,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: addr1,
value: 2i64.into(),
},
Instruction::Arithmetic {
arithmetic: ep::Arithmetic {
operation: ep::Operation::Add,
operand0: ep::Operand::Reference(addr0),
operand1: ep::Operand::Reference(addr1),
},
destination: Address::ZERO.offset(2),
}
]
);
}
#[test]
fn composite_binary_exprs() {
let program = "
let x = 1
let y = 2
let z = 3
let six = x + y + z
";
let (plan, _bindings) = must_plan(program);
let addr0 = Address::ZERO;
let addr1 = Address::ZERO.offset(1);
let addr2 = Address::ZERO.offset(2);
let addr3 = Address::ZERO.offset(3);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: addr0,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: addr1,
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: addr2,
value: 3i64.into(),
},
// Adds 1 + 2
Instruction::Arithmetic {
arithmetic: ep::Arithmetic {
operation: ep::Operation::Add,
operand0: ep::Operand::Reference(addr0),
operand1: ep::Operand::Reference(addr1),
},
destination: addr3,
},
// Adds `x` + 3, where `x` is (1 + 2)
Instruction::Arithmetic {
arithmetic: ep::Arithmetic {
operation: ep::Operation::Add,
operand0: ep::Operand::Reference(addr3),
operand1: ep::Operand::Reference(addr2),
},
destination: Address::ZERO.offset(4),
}
]
);
}
#[test]
fn aliases_dont_affect_plans() {
let (plan1, _) = must_plan(
"let one = 1
let two = 2
let x = one + two",
);
let (plan2, _) = must_plan(
"let one = 1
let two = 2
let y = two
let x = one + y",
);
assert_eq!(plan1, plan2);
}
#[test]
fn store_object() {
let program = "const x0 = {a: 1, b: 2, c: {d: 3}}";
let (actual, bindings) = must_plan(program);
let expected = vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: 3i64.into(),
},
];
assert_eq!(actual, expected);
eprintln!("{bindings:#?}");
assert_eq!(
bindings.get("x0").unwrap(),
&EpBinding::Map(HashMap::from([
("a".to_owned(), EpBinding::Single(Address::ZERO),),
("b".to_owned(), EpBinding::Single(Address::ZERO.offset(1))),
(
"c".to_owned(),
EpBinding::Map(HashMap::from([(
"d".to_owned(),
EpBinding::Single(Address::ZERO.offset(2))
)]))
),
]))
)
}
#[test]
fn store_object_with_array_property() {
let program = "const x0 = {a: 1, b: [2, 3]}";
let (actual, bindings) = must_plan(program);
let expected = vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: 3i64.into(),
},
];
assert_eq!(actual, expected);
eprintln!("{bindings:#?}");
assert_eq!(
bindings.get("x0").unwrap(),
&EpBinding::Map(HashMap::from([
("a".to_owned(), EpBinding::Single(Address::ZERO),),
(
"b".to_owned(),
EpBinding::Sequence(vec![
EpBinding::Single(Address::ZERO.offset(1)),
EpBinding::Single(Address::ZERO.offset(2)),
])
),
]))
)
}
#[ignore = "haven't done API calls or stdlib yet"]
#[test]
fn stdlib_api_calls() {
let program = "const x0 = startSketchAt([0, 0])
const x1 = line([0, 10], x0)
const x2 = line([10, 0], x1)
const x3 = line([0, -10], x2)
const x4 = line([0, 0], x3)
const x5 = close(x4)
const x6 = extrude(20, x5)
show(x6)";
must_plan(program);
}

View File

@ -18,16 +18,18 @@ async-trait = "0.1.73"
clap = { version = "4.4.8", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
# derive-docs = { version = "0.1.4" }
derive-docs = { path = "../derive-docs" }
derive-docs = { version = "0.1.5" }
# derive-docs = { path = "../derive-docs" }
kittycad = { workspace = true }
kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
lazy_static = "1.4.0"
parse-display = "0.8.2"
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
thiserror = "1.0.50"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
ts-rs = { version = "7", features = ["uuid-impl"] }
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
winnow = "0.5.18"

View File

@ -42,6 +42,7 @@ pub struct StdLibFnArg {
/// The type of the argument.
pub type_: String,
/// The schema of the argument.
#[ts(type = "any")]
pub schema: schemars::schema::Schema,
/// If the argument is required.
pub required: bool,

View File

@ -5,6 +5,7 @@ use std::{collections::HashMap, sync::Arc};
use anyhow::Result;
use async_recursion::async_recursion;
use kittycad::types::{Color, ModelingCmd, Point3D};
use kittycad_execution_plan_macros::ExecutionPlanValue;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -286,6 +287,7 @@ impl DefaultPlanes {
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct UserVal {
#[ts(type = "any")]
pub value: serde_json::Value,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
@ -610,7 +612,7 @@ impl Point2d {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
#[ts(export)]
pub struct Point3d {
pub x: f64,

View File

@ -2721,7 +2721,7 @@ show(b1)
show(b2)"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
parser.ast().unwrap();
dbg!(parser.ast().unwrap());
}
#[test]

View File

@ -3,6 +3,7 @@
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::{Angle, ModelingCmd, Point3D};
use kittycad_execution_plan_macros::ExecutionPlanValue;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -648,7 +649,7 @@ async fn inner_start_sketch_at(data: LineData, args: Args) -> Result<Box<SketchG
}
/// Data for a plane.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum PlaneData {
@ -1031,10 +1032,8 @@ async fn inner_arc(data: ArcData, sketch_group: Box<SketchGroup>, args: Args) ->
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc {
angle_start: angle_start.degrees(),
angle_end: angle_end.degrees(),
start: Some(angle_start),
end: Some(angle_end),
start: angle_start,
end: angle_end,
center: center.into(),
radius,
relative: false,

View File

@ -237,6 +237,7 @@ async fn serial_test_execute_cylinder() {
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "currently stack overflows"]
async fn serial_test_execute_kittycad_svg() {
let code = include_str!("inputs/kittycad_svg.kcl");

View File

@ -34,6 +34,14 @@ module.exports = {
colors: {
...themeColors,
},
fontFamily: {
display: `'owners', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif`,
sans: `-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif`,
},
},
},
darkMode: 'class',

View File

@ -5,7 +5,7 @@ import { spawn, ChildProcess } from 'child_process'
let tauriDriver: ChildProcess
const application =
process.env.E2E_APPLICATION || `./src-tauri/target/release/kittycad-modeling`
process.env.E2E_APPLICATION || `./src-tauri/target/release/zoo-modeling-app`
export const config = {
port: 4444,

View File

@ -4590,15 +4590,10 @@ flux@^4.0.1:
fbemitter "^3.0.0"
fbjs "^3.0.1"
follow-redirects@^1.0.0, follow-redirects@^1.14.8:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
follow-redirects@^1.15.0:
version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0:
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3:
version "0.3.3"
@ -8193,10 +8188,10 @@ vite-tsconfig-paths@^4.2.1:
globrex "^0.1.2"
tsconfck "^2.1.0"
"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a"
integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"