Compare commits

...

51 Commits

Author SHA1 Message Date
f79f11fd82 wip 2025-01-14 17:36:03 -05:00
5ada05063b Make cargo clippy happy 2025-01-14 14:11:05 -05:00
aab4d91b8c Ensure efficiency of updating editor vs execution 2025-01-14 13:56:03 -05:00
cfe472c066 Fix circle button not being activated 2025-01-14 11:56:23 -05:00
e822fbe0e9 Make points easier to select (enlarge) 2025-01-14 11:56:23 -05:00
87f0da36d0 Dont use isNaN 2025-01-14 11:56:23 -05:00
7ba2781a4a Fix clippy 2025-01-14 11:56:23 -05:00
fd81086936 Address Jon's comments 2025-01-14 11:56:23 -05:00
f933ab4434 cargo fmt 2025-01-14 11:56:23 -05:00
aae09bcf0e Cargo fmt 2025-01-14 11:56:23 -05:00
80d853f1c3 Add dragging behavior to 3 point circle
Uses our talked about technique of calling Rust functions to calculate new
geometry coordinates and parameters. It works very well!

Need to have the modeling app show "edit sketch" still.
2025-01-14 11:56:21 -05:00
bf9d01a8dd Bump xstate from 5.17.4 to 5.19.2 (#5027) 2025-01-14 15:39:05 +00:00
702e322f90 ci: Add yarn test of packages/codemirror-lang-kcl (#5035)
* ci: Add yarn test of packages/codemirror-lang-kcl

* Fix CI error running tests

* Fix postcss config error
2025-01-14 09:30:08 -05:00
e82830754d turns on helix from edge (#5036)
* updates for new lib

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

* autocomplete

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

* bump version

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

* bump all the things

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

* new samples

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

* docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-13 23:34:43 +00:00
7806377a5a Disable auto-updater on non-versioned builds (#5042) 2025-01-13 17:40:51 -05:00
859afa2fd8 Upgrade all wasm-bindgen dependencies together (#5037) 2025-01-13 13:24:23 -08:00
0a5f3093fc Fix Cargo.lock to not have changes (#5034) 2025-01-13 15:38:24 -05:00
b65f7939f6 Fix artifact types to be more accurate (#5022) 2025-01-13 15:02:55 -05:00
c35dea5e07 Bump syn from 2.0.95 to 2.0.96 in /src/wasm-lib (#5015)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.95 to 2.0.96.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.95...2.0.96)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 10:04:30 -08:00
fc66d4745f Bump handlebars from 6.2.0 to 6.3.0 in /src/wasm-lib (#5012)
Bumps [handlebars](https://github.com/sunng87/handlebars-rust) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/sunng87/handlebars-rust/releases)
- [Changelog](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunng87/handlebars-rust/compare/v6.2.0...v6.3.0)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 13:03:34 -05:00
b313d26c2a Bump @lezer/generator from 1.7.1 to 1.7.2 (#5018)
Bumps [@lezer/generator](https://github.com/lezer-parser/generator) from 1.7.1 to 1.7.2.
- [Changelog](https://github.com/lezer-parser/generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lezer-parser/generator/compare/1.7.1...1.7.2)

---
updated-dependencies:
- dependency-name: "@lezer/generator"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 11:48:13 -05:00
00b94ead62 Add packages to Dependabot updates (#5024) 2025-01-13 11:29:00 -05:00
0531ea1ce9 Change Dependabot PRs to always be made on Mondays (#5025) 2025-01-13 15:30:33 +00:00
5f9a4887c1 Developer workflow: added auto generated workspace file from vitest extension in vscode (#4997)
* chore: added auto generated workspace file from vitest extension in vscode

* fix: auto fmt fixes
2025-01-13 09:57:12 -05:00
da7dfa16d8 Fix lost lints and add new ones (#5011)
* Add eslint-plugin-jsx-a11y dependency

* Add jsx-a11y lint

* Add eslint-plugin-react-hooks dependency

* Add react hooks lints

* Ignore new react hooks lint in tests

* Add eslint-plugin-testing-library dependency

* Add testing-library lint

* Fix yarn lint to use all files recursively
2025-01-13 09:30:14 -05:00
363ae10658 Upgrade typescript-eslint from 5.62.0 to 8.19.1 and remove eslint-config-react-app (#5006) 2025-01-11 09:59:09 -05:00
ac4a6c84cf Point-and-click Sweep (first PR) (#4989)
* Refactor 'Delete selection' as actor
Will fix #4662

* WIP logging

* WIP: working Solid3dGetExtrusionFaceInfo for loft

* Working wall deletion of loft

* Add offset plane deletion

* Add feature tree deletion of shell

* Clean up

* Revert "Clean up"

This reverts commit 214763cc2b.

* Clean up rust changes, taking the sketch with the most paths

* Working cap selection and deletion

* Clean up

* Add test for loft and offset plane deletion via selection

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* Set reenter: false as it was originally

* Passing test

* Add shell deletion via feature tree test

* Revert the migration to promise actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Use cmd.id as solid_id after latest engine merge

* Add feature tree deletion of offset plane and fix lint

* Add feature tree deletion of loft

* Clean up

* Better comment

* Lint fix

* Remove sketch sorting

* WIP: sweep point-and-click

* Working sweep

* Add test

* Make sweep a development command

* Fix tsc error

* Clean up for review

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-11 08:20:49 -05:00
c6fad2e2dc Add new lint to disallow use of confusing isNaN (#4999) 2025-01-11 05:28:12 +00:00
013cb10961 Fix so that all artifact commands are returned regardless of caching (#5005)
* Fix so that all artifact commands are returned regardless of caching

* Add some more docs and fix up old ones
2025-01-10 22:33:05 -05:00
6261083cb1 Make the test executor a bit more patient (#5004) 2025-01-10 20:05:27 -05:00
2b0ba37ed0 Use Chromium instead of Chrome for Playwright Electron (#5001)
* Use Chromium instead of Chrome for Playwright Electron

* Remove channel
2025-01-10 13:37:26 -05:00
96174f3cf6 Increase playwright retries to 5 (#5000) 2025-01-10 13:34:27 -05:00
aed62ff912 Fix flaky playwright test 'Shell point-and-click sketch on face' (#5002)
Fixes #4998
2025-01-10 13:32:31 -05:00
9334d64608 Allow under-development commands in Nightly builds (#4995)
* Allow under-development commands in Nightly builds
Fixes #4994

* Fix warning

* Add back status: development to Revolve
2025-01-10 16:24:07 +00:00
4fa7d2d8c8 Feature: new axis and edge selection workflow for point and click revolve (#4939)
* feat: implemented axis or edge selection workflow in the commandbar

* fix: removing comment

* fix: removing console logs from testing

* fix: fixing lint and tsc errors

* fix: changed copy

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-01-10 08:52:04 -06:00
3e615dfdbc Update Katie's name reference and link in onboarding (#4967) 2025-01-09 22:07:40 -05:00
c9860af29f Fix Shell point-and-click picking the wrong face with piped extrudes (#4981)
* [BUG] Shell point and click references the wrong feature
Fixes #4961

* Add test for sketch on face based on extrudes in pipe

* Add no extrude in pipe case

* Lint

* Add scene.waitForExecutionDone()

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Update src/lang/modifyAst/addShell.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-01-10 01:20:07 +00:00
23a42f0195 Bump @kittycad/lib to v2.0.13 (#4988) 2025-01-09 16:02:05 -05:00
a77fa639f3 Point-and-click deletion of lofts, shells, and offset planes (#4898)
* Refactor 'Delete selection' as actor
Will fix #4662

* WIP logging

* WIP: working Solid3dGetExtrusionFaceInfo for loft

* Working wall deletion of loft

* Add offset plane deletion

* Add feature tree deletion of shell

* Clean up

* Revert "Clean up"

This reverts commit 214763cc2b.

* Clean up rust changes, taking the sketch with the most paths

* Working cap selection and deletion

* Clean up

* Add test for loft and offset plane deletion via selection

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* Set reenter: false as it was originally

* Passing test

* Add shell deletion via feature tree test

* Revert the migration to promise actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Use cmd.id as solid_id after latest engine merge

* Add feature tree deletion of offset plane and fix lint

* Add feature tree deletion of loft

* Clean up

* Better comment

* Lint fix

* Remove sketch sorting

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-09 15:36:50 -05:00
0a5ad7c95b Show deprecated indicator in CodeMirror autocomplete (#4983) 2025-01-09 09:15:00 -05:00
4a654523d2 Prevent toSync from clobbering stack traces (#4980)
* Prevent toSync from clobbering stack traces

* Capture error on the outside of the toSync catch

* fmt

* Actually fix it 🤦
2025-01-09 03:40:42 +00:00
73a7e2bfd6 Return modeling commands from KCL execution (#4912)
* Add Rust side artifacts for startSketchOn face or plane

* Add Rust-generated artifacts to ExecOutcome

* Add output of artifact commands

* Add new output files

* Wire the artifact commands to the artifact graph creation

* Fix to use real PartialEq implemented in modeling commands

* Fix modeling commands with zero fields to work

* Fix missing artifactCommands field in errors

* Change artifact graph to be built from artifact commands

* Wire up ExecState artifacts, but not using them yet

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Remove unneeded local var

* Fix test to fail with a helpful error message when command isn't found

* Rename and deprecate orderedCommands

* Update comment about borrowing

* Move ArtifactCommand tracking to the EngineManager trait

* Update artifact commands since tracking in the engine

* Upgrade kittycad-modeling-cmds from 0.2.85 to 0.2.86

* Remove unneeded JsonSchema derive to speed up build

* Fix to not fail on floating point differences in CI

* Update artifact commands output since truncating floating point numbers

* Fix to ensure artifact commands get cleared after a clear scene

* Update artifact commands snapshot after clearing them on clear scene

* Remove all remnants of OrderedCommands

* Update output for new simulation tests

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2025-01-09 01:02:30 +00:00
eb0850fea9 Make codespell smooth when run locally (#4978)
* Skip the out/ directory produced by yarn tron:package

* Skip all dist/ dirs

Produced when building rollup packages like codemirror-lang-kcl.

* Skip typescript build info

* Fix typo instead of excluding file

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
2025-01-08 10:36:37 -06:00
029f76f273 Nadro/4857/wasm panic catching errors (#4901)
* chore: skeleton code to initialize and detect the global WASM panic

* chore: implementing a reimport method to fix the wasm instance being bricked

* fix: cleaning up tsc/lint

* fix: renaming file to be more accurate

* fix: added toast message

* fix: types...

* fix: typed the functions with arg spreads
2025-01-08 15:58:41 +00:00
max
28b5f7080c Refactor Fillet AST Mod to Async Actor (#4803) 2025-01-08 16:05:24 +01:00
5b1dcfecd6 Open updater toast changelog links externally (#4970)
* fix: Hook into markdown-generated anchors to avoid e.g breaking the desktop app

* add comment

* Disable eslint on copied line from ts-stack

---------

Co-authored-by: marc2332 <mespinsanz@gmail.com>
2025-01-08 09:15:18 -05:00
f89d191425 add a test for foreign characters in project name (#4976)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-08 05:34:08 -05:00
2f4e4b62a8 Don't wait for !isExecuting to play the stream (#4971)
* Add failing test for current behavior

* Change stream behavior so that stream is played regardless of `isExecuting`

* Change expected pixel color

* Widen possible pixel color diff because local and CI produce slightly different colors
2025-01-08 04:34:57 -05:00
5ebd5c8dbb Enhance helixes (#4973)
* updates

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

* updates

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

* updates

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

* allow a helix to go into a sweep

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

* fix clippy

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

* updates

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

* udpates

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

* updates

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

* snapshots

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

* docs

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

* docs

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

* fix

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

* updates

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

* updates

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

* updates

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* updates

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

* updates

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

* docs

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

* updates

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* em,pty

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* updates

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>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-08 03:10:53 +00:00
a9ceaf2678 fix: make error for missing a closing bracket clearer (#4974)
* fix: make error for missing a closing bracket clearer

* Fix test for error message

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2025-01-08 01:55:07 +00:00
c8afd3399b Dead code clean up (smol PR) (#4653)
* general clean up

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

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

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

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

* trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-08 01:46:05 +00:00
244 changed files with 115327 additions and 2824 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts,./packages/codemirror-lang-kcl/test/all.test.ts
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo

View File

@ -5,16 +5,32 @@
},
"plugins": [
"css-modules",
"jest",
"jsx-a11y",
"react",
"react-hooks",
"suggest-no-throw",
"testing-library",
"@typescript-eslint"
],
"extends": [
"react-app",
"react-app/jest",
"plugin:css-modules/recommended"
"plugin:css-modules/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
}
],
"semi": [
"error",
"never"
@ -25,6 +41,9 @@
"overrides": [
{
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"extends": [
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off",
@ -33,6 +52,9 @@
},
{
"files": ["src/**/*.test.ts"],
"extends": [
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
}

View File

@ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
fi
retry=1
max_retrys=4
max_retrys=5
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do

View File

@ -6,23 +6,29 @@
version: 2
updates:
- package-ecosystem: 'npm' # See documentation for possible values
directory: '/' # Location of package manifests
directories:
- '/'
- '/packages/codemirror-lang-kcl/'
- '/packages/codemirror-lsp-client/'
schedule:
interval: 'weekly'
interval: weekly
day: monday
reviewers:
- franknoirot
- irev-dev
- package-ecosystem: 'github-actions' # See documentation for possible values
directory: '/' # Location of package manifests
schedule:
interval: 'weekly'
interval: weekly
day: monday
reviewers:
- adamchalmers
- jessfraz
- package-ecosystem: 'cargo' # See documentation for possible values
directory: '/src/wasm-lib/' # Location of package manifests
schedule:
interval: 'weekly'
interval: weekly
day: monday
reviewers:
- adamchalmers
- jessfraz
@ -30,3 +36,6 @@ updates:
serde-dependencies:
patterns:
- "serde*"
wasm-bindgen-deps:
patterns:
- "wasm-bindgen*"

View File

@ -0,0 +1,32 @@
name: CodeMirror Lang KCL
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
yarn-unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
working-directory: packages/codemirror-lang-kcl
- run: yarn tsc
working-directory: packages/codemirror-lang-kcl
- name: run unit tests
run: yarn test
working-directory: packages/codemirror-lang-kcl

View File

@ -24,3 +24,5 @@ once fixed in engine will just start working here with no language changes.
chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.
- **Helix**: Currently sweeping a helix does not work.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,7 @@ layout: manual
* [`getOppositeEdge`](kcl/getOppositeEdge)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix)
* [`helixRevolutions`](kcl/helixRevolutions)
* [`hole`](kcl/hole)
* [`hollow`](kcl/hollow)
* [`import`](kcl/import)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,19 @@
---
title: "AxisOrEdgeReference"
excerpt: "Axis or tagged edge."
title: "Axis2dOrEdgeReference"
excerpt: "A 2D axis or tagged edge."
layout: manual
---
Axis or tagged edge.
A 2D axis or tagged edge.
**This schema accepts any of the following:**
Axis and origin.
2D axis and origin.
[`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin)
[`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d)

View File

@ -0,0 +1,42 @@
---
title: "Axis3dOrEdgeReference"
excerpt: "A 3D axis or tagged edge."
layout: manual
---
A 3D axis or tagged edge.
**This schema accepts any of the following:**
3D axis and origin.
[`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d)
----
Tagged edge.
[`EdgeReference`](/docs/kcl/types/EdgeReference)
----

View File

@ -1,10 +1,10 @@
---
title: "AxisAndOrigin"
excerpt: "Axis and origin."
title: "AxisAndOrigin2d"
excerpt: "A 2D axis and origin."
layout: manual
---
Axis and origin.
A 2D axis and origin.

View File

@ -0,0 +1,105 @@
---
title: "AxisAndOrigin3d"
excerpt: "A 3D axis and origin."
layout: manual
---
A 3D axis and origin.
**This schema accepts exactly one of the following:**
X-axis.
**enum:** `X`
----
Y-axis.
**enum:** `Y`
----
Z-axis.
**enum:** `Z`
----
Flip the X-axis.
**enum:** `-X`
----
Flip the Y-axis.
**enum:** `-Y`
----
Flip the Z-axis.
**enum:** `-Z`
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `custom` |`object`| | No |
----

25
docs/kcl/types/Helix.md Normal file
View File

@ -0,0 +1,25 @@
---
title: "Helix"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -1,10 +1,10 @@
---
title: "HelixData"
excerpt: "Data for helices."
excerpt: "Data for a helix."
layout: manual
---
Data for helices.
Data for a helix.
**Type:** `object`
@ -19,6 +19,8 @@ Data for helices.
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |
| `length` |`number`| Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used. | No |
| `radius` |`number`| Radius of the helix. | No |
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -0,0 +1,24 @@
---
title: "HelixRevolutionsData"
excerpt: "Data for helix revolutions."
layout: manual
---
Data for helix revolutions.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |

View File

@ -0,0 +1,25 @@
---
title: "HelixValue"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -285,6 +285,27 @@ An solid is a collection of extrude surfaces.
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
----
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
Data for an imported geometry.

View File

@ -16,6 +16,6 @@ Data for a mirror.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No |
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -17,7 +17,7 @@ Data for revolution surfaces.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No |
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No |
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No |
| `tolerance` |`number`| Tolerance for the revolve operation. | No |

View File

@ -16,7 +16,7 @@ Data for a sweep.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
| `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No |
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` |`number`| Tolerance for the sweep operation. | No |

View File

@ -0,0 +1,42 @@
---
title: "SweepPath"
excerpt: "A path to sweep along."
layout: manual
---
A path to sweep along.
**This schema accepts any of the following:**
A path to sweep along.
[`Sketch`](/docs/kcl/types/Sketch)
----
A path to sweep along.
[`Helix`](/docs/kcl/types/Helix)
----

View File

@ -121,18 +121,23 @@ export class AuthenticatedTronApp {
export const fixtures = {
cmdBar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new CmdBarFixture(page))
},
editor: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new EditorFixture(page))
},
toolbar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new ToolbarFixture(page))
},
scene: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new SceneFixture(page))
},
homePage: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new HomePageFixture(page))
},
}

View File

@ -36,7 +36,8 @@ type DragFromHandler = (
export class SceneFixture {
public page: Page
public streamWrapper!: Locator
public loadingIndicator!: Locator
private exeIndicator!: Locator
constructor(page: Page) {
@ -64,6 +65,8 @@ export class SceneFixture {
this.page = page
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.streamWrapper = page.getByTestId('stream')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
}
makeMouseHelpers = (

View File

@ -14,6 +14,7 @@ export class ToolbarFixture {
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
@ -40,6 +41,7 @@ export class ToolbarFixture {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')

View File

@ -756,6 +756,17 @@ test(`Offset plane point-and-click`, async ({
})
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
})
await test.step('Delete offset plane via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation(
'Offset Plane',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
})
})
const loftPointAndClickCases = [
@ -851,6 +862,173 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
})
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
})
await test.step('Delete loft via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
})
// TODO: merge with above test. Right now we're not able to delete a loft
// right after creation via selection for some reason, so we go with a new instance
test('Loft and offset plane deletion via selection', async ({
context,
page,
homePage,
scene,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
loft001 = loft([sketch001, sketch002])
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
await test.step(`Delete loft`, async () => {
// Check for loft
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
await clickOnSketch1()
await expect(page.locator('.cm-activeLine')).toHaveText(`
|> circle({ center = [0, 0], radius = 30 }, %)
`)
await page.keyboard.press('Backspace')
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
await test.step('Delete sketch002', async () => {
await page.waitForTimeout(1000)
await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(`
|> circle({ center = [0, 0], radius = 20 }, %)
`)
await page.keyboard.press('Backspace')
// Check for plane001
await scene.expectPixelColor([228, 228, 228], testPoint, 15)
})
await test.step('Delete plane001', async () => {
await page.waitForTimeout(1000)
await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(`
plane001 = offsetPlane('XZ', 50)
`)
await page.keyboard.press('Backspace')
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
test(`Sweep point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-500, %)
|> tangentialArcTo([-2000, 500], %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'profile',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '',
},
highlightedHeaderArg: 'profile',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '1 face',
},
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await clickOnSketch2()
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [sweepDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
})
await test.step('Delete sweep via feature tree selection', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
})
@ -1030,4 +1208,104 @@ extrude001 = extrude(40, sketch001)
})
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
})
await test.step('Delete shell via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
})
const shellSketchOnFacesCases = [
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
|> extrude(100, %)
sketch002 = startSketchOn(sketch001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
|> extrude(50, %)
`,
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
extrude001 = extrude(100, sketch001)
sketch002 = startSketchOn(extrude001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
extrude002 = extrude(50, sketch002)
`,
]
shellSketchOnFacesCases.forEach((initialCode, index) => {
const hasExtrudesInPipe = index === 0
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
})`
await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
})
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
})
})
})

View File

@ -115,7 +115,7 @@ test(
)
test(
'yyyyyyyyy open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
@ -199,7 +199,7 @@ test(
)
test(
'aaayyyyyyyy open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
@ -276,7 +276,7 @@ test(
)
test(
'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene',
'open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
@ -1885,3 +1885,48 @@ test.fixme(
})
}
)
test(
'project name with foreign characters should open',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'اَلْعَرَبِيَّةُ')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl')
)
await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the اَلْعَرَبِيَّةُ project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('اَلْعَرَبِيَّةُ')).toBeVisible()
await page.getByText('اَلْعَرَبِيَّةُ').click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
}
)

View File

@ -614,6 +614,38 @@ extrude001 = extrude(50, sketch001)
await expect(gizmo).toBeVisible()
})
})
test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({
context,
homePage,
scene,
toolbar,
viewport,
}) => {
await context.folderSetupFn(async (dir) => {
const legoDir = path.join(dir, 'lego')
await fsp.mkdir(legoDir, { recursive: true })
await fsp.copyFile(
executorInputPath('lego.kcl'),
path.join(legoDir, 'main.kcl')
)
})
await test.step(`Test setup`, async () => {
await homePage.openProject('lego')
await toolbar.closePane('code')
})
await test.step(`Waiting for the loading spinner to disappear`, async () => {
await scene.loadingIndicator.waitFor({ state: 'detached' })
})
await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => {
await scene.expectPixelColor(
[143, 143, 143],
{ x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 },
15
)
})
})
})
async function clickExportButton(page: Page) {

View File

@ -39,8 +39,8 @@ test.describe('Sketch tests', () => {
${startProfileAt1}
|> arc({
radius = screwRadius,
angle_start = 0,
angle_end = 360
angleStart = 0,
angleEnd = 360
}, %)
part001 = startSketchOn('XY')
@ -60,8 +60,8 @@ test.describe('Sketch tests', () => {
|> yLine(wireOffset, %)
|> arc({
radius = wireRadius,
angle_start = 0,
angle_end = 180
angleStart = 0,
angleEnd = 180
}, %)
|> yLine(-wireOffset, %)
|> xLine(-width / 4, %)

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -389,25 +389,25 @@ test.describe('Testing selections', () => {
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
plane = {
origin = { x = 0, y = -50, z = 0 },
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 0, z = 1 },
z_axis = { x = 0, y = -1, z = 0 }
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = -1, z = 0 }
}
})`)
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
plane = {
origin = { x = 116.53, y = 0, z = 163.25 },
x_axis = { x = -0.81, y = 0, z = 0.58 },
y_axis = { x = 0, y = -1, z = 0 },
z_axis = { x = 0.58, y = 0, z = 0.81 }
xAxis = { x = -0.81, y = 0, z = 0.58 },
yAxis = { x = 0, y = -1, z = 0 },
zAxis = { x = 0.58, y = 0, z = 0.81 }
}
})`)
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
plane = {
origin = { x = -91.74, y = 0, z = 80.89 },
x_axis = { x = -0.66, y = 0, z = -0.75 },
y_axis = { x = 0, y = -1, z = 0 },
z_axis = { x = -0.75, y = 0, z = 0.66 }
xAxis = { x = -0.66, y = 0, z = -0.75 },
yAxis = { x = 0, y = -1, z = 0 },
zAxis = { x = -0.75, y = 0, z = 0.66 }
}
})`)

18
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1721933792,
"narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
@ -18,11 +18,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
@ -43,11 +43,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1721960387,
"narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=",
"lastModified": 1736476219,
"narHash": "sha256-+qyv3QqdZCdZ3cSO/cbpEY6tntyYjfe1bB12mdpNFaY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "9cbf831c5b20a53354fc12758abd05966f9f1699",
"rev": "de30cc5963da22e9742bbbbb9a3344570ed237b9",
"type": "github"
},
"original": {

1
interface.d.ts vendored
View File

@ -93,5 +93,6 @@ export interface IElectronAPI {
declare global {
interface Window {
electron: IElectronAPI
openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
}
}

View File

@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.12",
"@kittycad/lib": "2.0.13",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",
@ -65,7 +65,7 @@
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2",
"xstate": "^5.17.4",
"xstate": "^5.19.2",
"yargs": "^17.7.2"
},
"scripts": {
@ -91,8 +91,8 @@
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint-fix": "eslint --fix src e2e packages/codemirror-lsp-client",
"lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
@ -149,7 +149,7 @@
"@electron-forge/plugin-vite": "7.4.0",
"@electron/fuses": "1.8.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@lezer/generator": "^1.7.2",
"@nabla/vite-plugin-eslint": "^2.0.5",
"@playwright/test": "^1.49.0",
"@testing-library/jest-dom": "^5.14.1",
@ -171,8 +171,6 @@
"@types/uuid": "^9.0.8",
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17",
@ -182,10 +180,14 @@
"electron-builder": "24.13.3",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"eslint-plugin-testing-library": "^7.1.1",
"happy-dom": "^16.3.0",
"http-server": "^14.1.1",
"husky": "^9.1.5",
@ -200,6 +202,7 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -4,4 +4,5 @@ dist
tsconfig.tsbuildinfo
*.d.ts
*.js
!postcss.config.js
!rollup.config.js

View File

@ -28,6 +28,7 @@
"@rollup/plugin-typescript": "^12.1.2",
"rollup": "^4.29.1",
"rollup-plugin-dts": "^6.1.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.8"
},
"files": [

View File

@ -0,0 +1 @@
// This is here to prevent using the one in the root of the project.

View File

@ -398,7 +398,7 @@ check-error@^2.1.1:
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
debug@^4.3.7:
debug@^4.1.1, debug@^4.3.7:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@ -471,6 +471,11 @@ function-bind@^1.1.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
globrex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
@ -647,6 +652,11 @@ tinyspy@^3.0.2:
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
tsconfck@^3.0.3:
version "3.1.4"
resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.4.tgz#de01a15334962e2feb526824339b51be26712229"
integrity sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
@ -663,6 +673,15 @@ vite-node@2.1.8:
pathe "^1.1.2"
vite "^5.0.0"
vite-tsconfig-paths@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"
integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==
dependencies:
debug "^4.1.1"
globrex "^0.1.2"
tsconfck "^3.0.3"
vite@^5.0.0:
version "5.4.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"

View File

@ -42,7 +42,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
// try to parse the content-length from the headers
const length = parseInt(match[1])
if (isNaN(length))
if (Number.isNaN(length))
return Promise.reject(new Error('invalid content length'))
// slice the headers since we now have the content length

View File

@ -368,13 +368,20 @@ export class LanguageServerPlugin implements PluginValue {
sortText,
filterText,
}) => {
const detailText = [
deprecated ? 'Deprecated' : undefined,
labelDetails ? labelDetails.detail : detail,
]
// Don't let undefined appear.
.filter(Boolean)
.join(' ')
const completion: Completion & {
filterText: string
sortText?: string
apply: string
} = {
label,
detail: labelDetails ? labelDetails.detail : detail,
detail: detailText,
apply: label,
type: kind && CompletionItemKindMap[kind].toLowerCase(),
sortText: sortText ?? label,
@ -382,7 +389,11 @@ export class LanguageServerPlugin implements PluginValue {
}
if (documentation) {
completion.info = () => {
const htmlString = formatMarkdownContents(documentation)
const deprecatedHtml = deprecated
? '<p><strong>Deprecated</strong></p>'
: ''
const htmlString =
deprecatedHtml + formatMarkdownContents(documentation)
const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString

View File

@ -32,10 +32,9 @@ export default defineConfig({
},
projects: [
{
name: 'Google Chrome',
name: 'chromium',
use: {
...devices['Desktop Chrome'],
channel: 'chrome',
contextOptions: {
/* Chromium is the only one with these permission types */
permissions: ['clipboard-write', 'clipboard-read'],

View File

@ -1,21 +1,21 @@
[
{
"file": "80-20-rail.kcl",
"file": "main.kcl",
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "a-parametric-bearing-pillow-block.kcl",
"file": "main.kcl",
"title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
},
{
"file": "ball-bearing.kcl",
"file": "main.kcl",
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "bracket.kcl",
"file": "main.kcl",
"title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
},
@ -25,148 +25,128 @@
"description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed."
},
{
"file": "car-wheel.kcl",
"title": "Car Wheel",
"description": "A sports car wheel with a circular lug pattern and spokes."
},
{
"file": "car-wheel-assembly.kcl",
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
},
{
"file": "dodecahedron.kcl",
"file": "main.kcl",
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
},
{
"file": "enclosure.kcl",
"file": "main.kcl",
"title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items"
},
{
"file": "flange-with-patterns.kcl",
"file": "main.kcl",
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "flange-xy.kcl",
"file": "main.kcl",
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "focusrite-scarlett-mounting-bracket.kcl",
"file": "main.kcl",
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "food-service-spatula.kcl",
"file": "main.kcl",
"title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping."
},
{
"file": "french-press.kcl",
"file": "main.kcl",
"title": "French Press",
"description": "A french press immersion coffee maker"
},
{
"file": "gear.kcl",
"file": "main.kcl",
"title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{
"file": "gear-rack.kcl",
"file": "main.kcl",
"title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
},
{
"file": "hex-nut.kcl",
"file": "main.kcl",
"title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
},
{
"file": "i-beam.kcl",
"file": "main.kcl",
"title": "I-beam",
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
},
{
"file": "kitt.kcl",
"file": "main.kcl",
"title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style."
},
{
"file": "lego.kcl",
"file": "main.kcl",
"title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
},
{
"file": "lug-nut.kcl",
"title": "Lug Nut",
"description": "lug Nuts are essential components used to create secure connections, whether for electrical purposes, like terminating wires or grounding, or for mechanical purposes, such as providing mounting points or reinforcing structural joints."
},
{
"file": "mounting-plate.kcl",
"file": "main.kcl",
"title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
},
{
"file": "multi-axis-robot.kcl",
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
"file": "globals.kcl",
"title": "Global constants for the multi-axis robot",
"description": ""
},
{
"file": "pipe.kcl",
"file": "main.kcl",
"title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "pipe-flange-assembly.kcl",
"file": "main.kcl",
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "pipe-with-bend.kcl",
"file": "main.kcl",
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "poopy-shoe.kcl",
"file": "main.kcl",
"title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing."
},
{
"file": "router-template-cross-bar.kcl",
"file": "main.kcl",
"title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar."
},
{
"file": "router-template-slate.kcl",
"file": "main.kcl",
"title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar."
},
{
"file": "sheet-metal-bracket.kcl",
"file": "main.kcl",
"title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
},
{
"file": "socket-head-cap-screw.kcl",
"file": "main.kcl",
"title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
},
{
"file": "tire.kcl",
"title": "Tire",
"description": "A tire is a critical component of a vehicle that provides the necessary traction and grip between the car and the road. It supports the vehicle's weight and absorbs shocks from road irregularities."
"file": "antenna.kcl",
"title": "Antenna",
"description": ""
},
{
"file": "washer.kcl",
"file": "main.kcl",
"title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
},
{
"file": "wheel-rotor.kcl",
"title": "Wheel rotor",
"description": "A component of a disc brake system. It provides a surface for brake pads to press against, generating the friction needed to slow or stop the vehicle."
}
]

View File

@ -1,5 +1,6 @@
import {
BoxGeometry,
Color,
DoubleSide,
Group,
Intersection,
@ -59,6 +60,7 @@ import {
resultIsOk,
SourceRange,
} from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import {
engineCommandManager,
kclManager,
@ -70,7 +72,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, ToolTip } from 'lang/langHelpers'
import {
createProfileStartHandle,
createArcGeometry,
createCircleGeometry,
SegmentUtils,
segmentUtils,
} from './segments'
@ -109,6 +111,8 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
import { radToDeg } from 'three/src/math/MathUtils'
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
@ -1261,110 +1265,98 @@ export class SceneEntities {
const groupOfDrafts = new Group()
groupOfDrafts.name = 'circle-3-point-group'
groupOfDrafts.position.copy(sketchOrigin)
// lee: I'm keeping this here as a developer gotchya:
// Do not reorient your surfaces to the intersection plane. Your points are
// already in 3D space, not 2D. If you intersect say XZ, you want the points
// to continue to live at the 3D intersection point, not be rotated to end
// up elsewhere!
// groupOfDrafts.setRotationFromQuaternion(orientation)
// If you use 3D points, do not rotate anything.
// If you use 2D points (easier to deal with, generally do this!), then
// rotate the group just like this! Remember to rotate other groups too!
groupOfDrafts.setRotationFromQuaternion(orientation)
this.scene.add(groupOfDrafts)
const DRAFT_POINT_RADIUS = 6
// How large the points on the circle will render as
const DRAFT_POINT_RADIUS = 10 // px
const createPoint = (center: Vector3): number => {
// The target of our dragging
let target: Object3D | undefined = undefined
// The KCL this will generate.
const kclCircle3Point = parse(`circleThreePoint(
p1 = [0.0, 0.0],
p2 = [0.0, 0.0],
p3 = [0.0, 0.0],
)`)
const createPoint = (
center: Vector3,
opts?: { noInteraction?: boolean }
): Mesh => {
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
const color = getThemeColorForThreeJs(sceneInfra._theme)
const material = new MeshBasicMaterial({ color })
const material = new MeshBasicMaterial({
color: opts?.noInteraction
? sceneInfra._theme === 'light'
? new Color(color).multiplyScalar(0.15)
: new Color(0x010101).multiplyScalar(2000)
: color,
})
const mesh = new Mesh(geometry, material)
mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT }
mesh.userData = {
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
}
mesh.renderOrder = 1000
mesh.layers.set(SKETCH_LAYER)
mesh.position.copy(center)
mesh.scale.set(scale, scale, scale)
mesh.renderOrder = 100
groupOfDrafts.add(mesh)
return mesh.id
return mesh
}
const circle3Point = (
points: Vector2[]
): undefined | { center: Vector3; radius: number } => {
// A 3-point circle is undefined if it doesn't have 3 points :)
if (points.length !== 3) return undefined
// y = (i/j)(x-h) + b
// i and j variables for the slopes
const i = [points[1].x - points[0].x, points[2].x - points[1].x]
const j = [points[1].y - points[0].y, points[2].y - points[1].y]
// Our / threejs coordinate system affects this a lot. If you take this
// code into a different code base, you may have to adjust a/b to being
// -1/a/b, b/a, etc! In this case, a/-b did the trick.
const m = [i[0] / -j[0], i[1] / -j[1]]
const h = [
(points[0].x + points[1].x) / 2,
(points[1].x + points[2].x) / 2,
]
const b = [
(points[0].y + points[1].y) / 2,
(points[1].y + points[2].y) / 2,
]
// Algebraically derived
const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0])
const y = m[0] * (x - h[0]) + b[0]
const center = new Vector3(x, y, 0)
const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2)
return {
center,
radius,
}
}
// TO BE SHORT LIVED: unused function to draw the circle and lines.
// @ts-ignore
// eslint-disable-next-line
const createCircle3Point = (points: Vector2[]) => {
const circleParams = circle3Point(points)
// A circle cannot be created for these points.
if (!circleParams) return
const createCircle3PointGraphic = async (
points: Vector2[],
center: Vector2,
radius: number
) => {
if (
Number.isNaN(radius) ||
Number.isNaN(center.x) ||
Number.isNaN(center.y)
)
return
const color = getThemeColorForThreeJs(sceneInfra._theme)
const geometryCircle = createArcGeometry({
center: [circleParams.center.x, circleParams.center.y],
radius: circleParams.radius,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
isDashed: true,
scale,
const lineCircle = createCircleGeometry({
center: [center.x, center.y],
radius,
color,
isDashed: false,
scale: 1,
})
const materialCircle = new MeshBasicMaterial({ color })
lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
lineCircle.layers.set(SKETCH_LAYER)
// devnote: it's a mistake to use these with EllipseCurve :)
// lineCircle.position.set(center.x, center.y, 0)
// lineCircle.scale.set(scale, scale, scale)
if (groupCircle) groupOfDrafts.remove(groupCircle)
groupCircle = new Group()
groupCircle.renderOrder = 1
groupCircle.add(lineCircle)
const meshCircle = new Mesh(geometryCircle, materialCircle)
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
meshCircle.layers.set(SKETCH_LAYER)
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0)
meshCircle.scale.set(scale, scale, scale)
groupCircle.add(meshCircle)
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
noInteraction: true,
})
groupCircle.add(pointMesh)
const geometryPolyLine = new BufferGeometry().setFromPoints([
...points,
points[0],
...points.map((p) => new Vector3(p.x, p.y, 0)),
new Vector3(points[0].x, points[0].y, 0),
])
const materialPolyLine = new LineDashedMaterial({
color,
scale,
scale: 1 / scale,
dashSize: 6,
gapSize: 6,
})
@ -1375,13 +1367,146 @@ export class SceneEntities {
groupOfDrafts.add(groupCircle)
}
// The target of our dragging
let target: Object3D | undefined = undefined
const insertCircle3PointKclIntoAstSnapshot = (
points: Vector2[]
): Program => {
if (err(kclCircle3Point) || kclCircle3Point.program === null)
return kclManager.ast
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return kclManager.ast
if (
kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw'
)
return kclManager.ast
const arg = (x: LabeledArg): Literal[] | undefined => {
if (
'arg' in x &&
'elements' in x.arg &&
x.arg.type === 'ArrayExpression'
) {
if (x.arg.elements.every((x) => x.type === 'Literal')) {
return x.arg.elements
}
}
return undefined
}
const kclCircle3PointArgs =
kclCircle3Point.program.body[0].expression.arguments
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = points[0].x
arg0[0].raw = points[0].x.toString()
arg0[1].value = points[0].y
arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = points[1].x
arg1[0].raw = points[1].x.toString()
arg1[1].value = points[1].y
arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = points[2].x
arg2[0].raw = points[2].x.toString()
arg2[1].value = points[2].y
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(startSketchOnASTNode)) return astSnapshot
// It's possible we're already dealing with a PipeExpression.
// Modify the current one.
if (
startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' &&
startSketchOnASTNode.node.declaration.init.body[1].type ===
'CallExpressionKw' &&
startSketchOnASTNode.node.declaration.init.body.length >= 2
) {
startSketchOnASTNode.node.declaration.init.body[1].arguments =
kclCircle3Point.program.body[0].expression.arguments
} else {
// Clone a new node based on the old, and replace the old with the new.
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
}
// Return the `Program`
return astSnapshot
}
const updateCircle3Point = async (opts?: { execute?: true }) => {
const points_ = Array.from(points.values())
const circleParams = calculate_circle_from_3_points(
points_[0].x,
points_[0].y,
points_[1].x,
points_[1].y,
points_[2].x,
points_[2].y
)
if (Number.isNaN(circleParams.radius)) return
await createCircle3PointGraphic(
points_,
new Vector2(circleParams.center_x, circleParams.center_y),
circleParams.radius
)
const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_)
const codeAsString = recast(astWithNewCode)
if (err(codeAsString)) return
codeManager.updateCodeStateEditor(codeAsString)
}
const cleanupFn = () => {
this.scene.remove(groupOfDrafts)
}
// The AST node we extracted earlier may already have a circleThreePoint!
// Use the points in the AST as starting points.
const astSnapshot = structuredClone(kclManager.ast)
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(maybeVariableDeclaration))
return () => {
done()
}
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
if (
maybeCallExpressionKw.type === 'PipeExpression' &&
maybeCallExpressionKw.body[1].type === 'CallExpressionKw' &&
maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint'
) {
maybeCallExpressionKw?.body[1].arguments
.map(
({ arg }: any) =>
new Vector2(arg.elements[0].value, arg.elements[1].value)
)
.forEach((point: Vector2) => {
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, point)
})
void updateCircle3Point()
}
sceneInfra.setCallbacks({
async onDrag(args) {
const draftPointsIntersected = args.intersects.filter(
@ -1397,8 +1522,18 @@ export class SceneEntities {
// The user was off their mark! Missed the object to select.
if (!target) return
target.position.copy(args.intersectionPoint.threeD)
target.position.copy(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
points.set(target.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
},
async onDragEnd(_args) {
target = undefined
@ -1407,45 +1542,19 @@ export class SceneEntities {
if (points.size >= 3) return
if (!args.intersectionPoint) return
const id = createPoint(args.intersectionPoint.threeD)
points.set(id, args.intersectionPoint.twoD)
if (points.size < 2) return
// We've now got 3 points, let's create our circle!
const astSnapshot = structuredClone(kclManager.ast)
let nodeQueryResult
nodeQueryResult = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
const pointMesh = createPoint(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult)
const startSketchOnASTNode = nodeQueryResult
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, args.intersectionPoint.twoD)
const circleParams = circle3Point(Array.from(points.values()))
if (points.size <= 2) return
if (!circleParams) return
const kclCircle3Point = parse(`circle({
center = [${circleParams.center.x}, ${circleParams.center.y}],
radius = ${circleParams.radius},
}, %)`)
if (err(kclCircle3Point) || kclCircle3Point.program === null) return
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
await kclManager.executeAstMock(astSnapshot)
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
done()
await updateCircle3Point()
},
})

View File

@ -9,6 +9,9 @@ import {
ExtrudeGeometry,
Group,
LineCurve3,
LineBasicMaterial,
LineDashedMaterial,
Line,
Mesh,
MeshBasicMaterial,
NormalBufferAttributes,
@ -1003,6 +1006,49 @@ export function createArcGeometry({
return geo
}
// (lee) The above is much more complex than necessary.
// I've derived the new code from:
// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve
// I'm not sure why it wasn't done like this in the first place?
// I don't touch the code above because it may break something else.
export function createCircleGeometry({
center,
radius,
color,
isDashed = false,
scale = 1,
}: {
center: Coords2d
radius: number
color: number
isDashed?: boolean
scale?: number
}): Line {
const circle = new EllipseCurve(
center[0],
center[1],
radius,
radius,
0,
Math.PI * 2,
true,
scale
)
const points = circle.getPoints(75) // just enough points to not see edges.
const geometry = new BufferGeometry().setFromPoints(points)
const material = !isDashed
? new LineBasicMaterial({ color })
: new LineDashedMaterial({
color,
scale,
dashSize: 6,
gapSize: 6,
})
const line = new Line(geometry, material)
line.computeLineDistances()
return line
}
export function dashedStraight(
from: Coords2d,
to: Coords2d,

View File

@ -148,6 +148,7 @@ function HelpMenuItem({
return (
<li className="p-0 m-0">
{as === 'a' ? (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<a
{...(props as React.ComponentProps<'a'>)}
onClick={openExternalBrowserIfDesktop(

View File

@ -157,39 +157,38 @@ export const ModelingMachineProvider = ({
'enable copilot': () => {
editorManager.setCopilotEnabled(true)
},
// tsc reports this typing as perfectly fine, but eslint is complaining.
// It's actually nonsensical, so I'm quieting.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
'sketch exit execute': async ({
context: { store },
}): Promise<void> => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
'sketch exit execute': ({ context: { store } }) => {
// TODO: Remove this async callback. For some reason eslint wouldn't
// let me disable @typescript-eslint/no-misused-promises for the line.
;(async () => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
.catch(reportRejection)
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
})
.catch(reportRejection)
})().catch(reportRejection)
},
'Set mouse state': assign(({ context, event }) => {
if (event.type !== 'Set mouse state') return {}
@ -271,6 +270,7 @@ export const ModelingMachineProvider = ({
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_center_to_selection',
camera_movement: 'vantage',
},
})
.catch(reportRejection)

View File

@ -18,6 +18,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
return (
<Menu>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className="relative"
onClick={(e) => {

View File

@ -218,20 +218,6 @@ export const Stream = () => {
}
}, [IDLE, streamState])
/**
* Play the vid
*/
useEffect(() => {
if (!kclManager.isExecuting) {
setTimeout(() => {
// execute in the next event loop
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
}
}, [kclManager.isExecuting])
useEffect(() => {
if (
typeof window === 'undefined' ||
@ -243,9 +229,15 @@ export const Stream = () => {
// The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream!
// we instead use a setTimeout to play the stream in the next event loop
try {
videoRef.current.srcObject = mediaStream
videoRef.current.pause()
setTimeout(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
} catch (e) {
console.warn('Attempted to pause stream while play was still loading', e)
}
@ -321,6 +313,7 @@ export const Stream = () => {
}
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
ref={videoWrapperRef}
className="absolute inset-0 z-0"

View File

@ -150,4 +150,31 @@ describe('ToastUpdate tests', () => {
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
test('Happy path: external links render correctly', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- [Zoo](https://zoo.dev/)
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const zooDev = screen.getByText('Zoo', {
selector: 'a',
})
expect(zooDev).toHaveAttribute('href', 'https://zoo.dev/')
expect(zooDev).toHaveAttribute('target', '_blank')
expect(zooDev).toHaveAttribute('onClick')
})
})

View File

@ -1,8 +1,9 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
import { escape, Marked, MarkedOptions, unescape } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings'
import { SafeRenderer } from 'lib/markdown'
export function ToastUpdate({
version,
@ -19,6 +20,14 @@ export function ToastUpdate({
?.toLocaleLowerCase()
.includes('breaking')
const markedOptions: MarkedOptions = {
gfm: true,
breaks: true,
sanitize: true,
unescape,
escape,
}
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -58,9 +67,8 @@ export function ToastUpdate({
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
renderer: new SafeRenderer(markedOptions),
...markedOptions,
}),
}}
></div>

View File

@ -10,8 +10,11 @@ import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate'
import { markOnce } from 'lib/performance'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
import { initializeWindowExceptionHandler } from 'lib/exceptions'
markOnce('code/willAuth')
initializeWindowExceptionHandler()
// uncomment for xstate inspector
// import { DEV } from 'env'
// import { inspect } from '@xstate/inspect'

View File

@ -376,7 +376,11 @@ export class KclManager {
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast)
await this.engineCommandManager.updateArtifactGraph(
this.ast,
execState.artifactCommands,
execState.artifacts
)
this._executeCallback()
if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' })
@ -390,6 +394,24 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId)
markOnce('code/endExecuteAst')
}
/**
* This cleanup function is external and internal to the KclSingleton class.
* Since the WASM runtime can panic and the error cannot be caught in executeAst
* we need a global exception handler in exceptions.ts
* This file will interface with this cleanup as if it caught the original error
* to properly restore the TS application state.
*/
executeAstCleanUp() {
this.isExecuting = false
this.executeIsStale = null
this.engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,
})
markOnce('code/endExecuteAst')
}
// NOTE: this always updates the code state and editor.
// DO NOT CALL THIS from codemirror ever.
async executeAstMock(

View File

@ -47,7 +47,7 @@ describe('parsing errors', () => {
const result = parse(code)
if (err(result)) throw result
const error = result.errors[0]
expect(error.message).toBe('Unexpected token: (')
expect(error.sourceRange).toEqual([27, 28, 0])
expect(error.message).toBe('Array is missing a closing bracket(`]`)')
expect(error.sourceRange).toEqual([28, 29, 0])
})
})

View File

@ -10,6 +10,7 @@ describe('test kclErrToDiagnostic', () => {
msg: 'Semantic error',
sourceRange: [0, 1, true],
operations: [],
artifactCommands: [],
},
{
name: '',
@ -18,6 +19,7 @@ describe('test kclErrToDiagnostic', () => {
msg: 'Type error',
sourceRange: [4, 5, true],
operations: [],
artifactCommands: [],
},
]
const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -5,7 +5,7 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror'
import { SourceRange } from 'lang/wasm'
import { ArtifactCommand, SourceRange } from 'lang/wasm'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
type ExtractKind<T> = T extends { kind: infer K } ? K : never
@ -14,86 +14,141 @@ export class KCLError extends Error {
sourceRange: SourceRange
msg: string
operations: Operation[]
artifactCommands: ArtifactCommand[]
constructor(
kind: ExtractKind<RustKclError> | 'name',
msg: string,
sourceRange: SourceRange,
operations: Operation[]
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super()
this.kind = kind
this.msg = msg
this.sourceRange = sourceRange
this.operations = operations
this.artifactCommands = artifactCommands
Object.setPrototypeOf(this, KCLError.prototype)
}
}
export class KCLLexicalError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('lexical', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('lexical', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLInternalError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('internal', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('internal', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('syntax', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('syntax', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSemanticError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('semantic', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('semantic', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSemanticError.prototype)
}
}
export class KCLTypeError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('type', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('type', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLTypeError.prototype)
}
}
export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('unimplemented', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('unimplemented', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
}
export class KCLUnexpectedError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('unexpected', msg, sourceRange, operations)
constructor(
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('unexpected', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
}
}
export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRange: SourceRange, operations: Operation[]) {
constructor(
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super(
'name',
`Key ${key} was already defined elsewhere`,
sourceRange,
operations
operations,
artifactCommands
)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
}
export class KCLUndefinedValueError extends KCLError {
constructor(key: string, sourceRange: SourceRange, operations: Operation[]) {
super('name', `Key ${key} has not been defined`, sourceRange, operations)
constructor(
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super(
'name',
`Key ${key} has not been defined`,
sourceRange,
operations,
artifactCommands
)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
}
@ -113,6 +168,7 @@ export function lspDiagnosticsToKclErrors(
'unexpected',
message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
[],
[]
)
)

View File

@ -481,6 +481,7 @@ const theExtrude = startSketchOn('XY')
'undefined_value',
'memory item key `myVarZ` is not defined',
[129, 135, true],
[],
[]
)
)

View File

@ -1,79 +1,72 @@
import { assertParse, initPromise, programMemoryInit } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
// These unit tests makes web requests to a public github repository.
import path from 'node:path'
import fs from 'node:fs/promises'
import child_process from 'node:child_process'
// The purpose of these tests is to act as a first line of defense
// if something gets real screwy with our KCL ecosystem.
// THESE TESTS ONLY RUN UNDER A NODEJS ENVIRONMENT. They DO NOT
// test under our application.
const DIR_KCL_SAMPLES = 'kcl-samples'
const URL_GIT_KCL_SAMPLES = 'https://github.com/KittyCAD/kcl-samples.git'
interface KclSampleFile {
file: string
pathFromProjectDirectoryToFirstFile: string
title: string
filename: string
description: string
}
try {
await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
} catch (e) {
console.log(e)
}
child_process.spawnSync('git', ['clone', URL_GIT_KCL_SAMPLES, DIR_KCL_SAMPLES])
let files = await fs.readdir(DIR_KCL_SAMPLES)
const manifestJsonStr = await fs.readFile(
path.resolve(DIR_KCL_SAMPLES, 'manifest.json'),
'utf-8'
)
const manifest = JSON.parse(manifestJsonStr)
process.chdir(DIR_KCL_SAMPLES)
beforeAll(async () => {
await initPromise
})
// Only used to actually fetch an older version of KCL code that will break in the parser.
/* eslint-disable @typescript-eslint/no-unused-vars */
async function getBrokenSampleCodeForLocalTesting() {
const result = await fetch(
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/5ccd04a1773ebdbfd02684057917ce5dbe0eaab3/80-20-rail.kcl'
)
const text = await result.text()
return text
}
afterAll(async () => {
try {
process.chdir('..')
await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
} catch (e) {}
})
async function getKclSampleCodeFromGithub(file: string): Promise<string> {
const result = await fetch(
`https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/${file}/${file}.kcl`
)
const text = await result.text()
return text
}
async function getFileNamesFromManifestJSON(): Promise<KclSampleFile[]> {
const result = await fetch(
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/manifest.json'
)
const json = await result.json()
json.forEach((file: KclSampleFile) => {
const filenameWithoutExtension = file.file.split('.')[0]
file.filename = filenameWithoutExtension
})
return json
}
// Value to use across all tests!
let files: KclSampleFile[] = []
describe('Test KCL Samples from public Github repository', () => {
describe('When parsing source code', () => {
// THIS RUNS ACROSS OTHER TESTS!
it('should fetch files', async () => {
files = await getFileNamesFromManifestJSON()
})
// Run through all of the files in the manifest json. This will allow us to be automatically updated
// with the latest changes in github. We won't be hard coding the filenames
files.forEach((file: KclSampleFile) => {
it(`should parse ${file.filename} without errors`, async () => {
const code = await getKclSampleCodeFromGithub(file.filename)
assertParse(code)
}, 1000)
})
})
describe('when performing enginelessExecutor', () => {
it(
'should run through all the files',
async () => {
for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename)
// The tests have to be sequential because we need to change directories
// to support `import` working properly.
describe.sequential('Test KCL Samples from public Github repository', () => {
describe.sequential('when performing enginelessExecutor', () => {
manifest.forEach((file: KclSampleFile) => {
it.sequential(
`should execute ${file.title} (${file.file}) successfully`,
async () => {
const [dirProject, fileKcl] =
file.pathFromProjectDirectoryToFirstFile.split('/')
process.chdir(dirProject)
const code = await fs.readFile(fileKcl, 'utf-8')
const ast = assertParse(code)
await enginelessExecutor(ast, programMemoryInit())
}
},
files.length * 1000
)
process.chdir('..')
},
files.length * 1000
)
})
})
})

View File

@ -1,6 +1,6 @@
import {
Program,
_executor,
executor,
ProgramMemory,
kclLint,
emptyExecState,
@ -64,7 +64,7 @@ export async function executeAst({
try {
const execState = await (programMemoryOverride
? enginelessExecutor(ast, programMemoryOverride)
: _executor(ast, engineCommandManager))
: executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands()

View File

@ -806,9 +806,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({
plane = {
origin = { x = 1, y = 2, z = 3 },
x_axis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 }
xAxis = { x = 4, y = 5, z = 6 },
yAxis = { x = 7, y = 8, z = 9 },
zAxis = { x = 10, y = 11, z = 12 }
}
})
|> startProfileAt([-12.55, 2.89], %)
@ -862,9 +862,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({
plane = {
origin = { x = 1, y = 2, z = 3 },
x_axis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 }
xAxis = { x = 4, y = 5, z = 6 },
yAxis = { x = 7, y = 8, z = 9 },
zAxis = { x = 10, y = 11, z = 12 }
}
})
|> startProfileAt([-12.55, 2.89], %)

View File

@ -374,6 +374,37 @@ export function loftSketches(
}
}
export function addSweep(
node: Node<Program>,
profileDeclarator: VariableDeclarator,
pathDeclarator: VariableDeclarator
): {
modifiedAst: Node<Program>
pathToNode: PathToNode
} {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const sweep = createCallExpressionStdLib('sweep', [
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
createIdentifier(profileDeclarator.id.name),
])
const declaration = createVariableDeclaration(name, sweep)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}
export function revolveSketch(
node: Node<Program>,
pathToNode: PathToNode,
@ -1149,11 +1180,17 @@ export async function deleteFromSelection(
((selection?.artifact?.type === 'wall' ||
selection?.artifact?.type === 'cap') &&
varDec.node.init.type === 'PipeExpression') ||
selection.artifact?.type === 'sweep'
selection.artifact?.type === 'sweep' ||
selection.artifact?.type === 'plane' ||
!selection.artifact // aka expected to be a shell at this point
) {
let extrudeNameToDelete = ''
let pathToNode: PathToNode | null = null
if (selection.artifact?.type !== 'sweep') {
if (
selection.artifact &&
selection.artifact.type !== 'sweep' &&
selection.artifact.type !== 'plane'
) {
const varDecName = varDec.node.id.name
traverse(astClone, {
enter: (node, path) => {
@ -1169,6 +1206,17 @@ export async function deleteFromSelection(
pathToNode = path
extrudeNameToDelete = dec.id.name
}
if (
dec.init.type === 'CallExpression' &&
dec.init.callee.name === 'loft' &&
dec.init.arguments?.[0].type === 'ArrayExpression' &&
dec.init.arguments?.[0].elements.some(
(a) => a.type === 'Identifier' && a.name === varDecName
)
) {
pathToNode = path
extrudeNameToDelete = dec.id.name
}
}
},
})
@ -1278,17 +1326,17 @@ export async function deleteFromSelection(
y: roundLiteral(faceDetails.origin.y),
z: roundLiteral(faceDetails.origin.z),
}),
x_axis: createObjectExpression({
xAxis: createObjectExpression({
x: roundLiteral(faceDetails.x_axis.x),
y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z),
}),
y_axis: createObjectExpression({
yAxis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z),
}),
z_axis: createObjectExpression({
zAxis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z),

View File

@ -61,19 +61,18 @@ export interface FilletParameters {
export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
// Apply Edge Treatment (Fillet or Chamfer) To Selection
export function applyEdgeTreatmentToSelection(
export async function applyEdgeTreatmentToSelection(
ast: Node<Program>,
selection: Selections,
parameters: EdgeTreatmentParameters
): void | Error {
): Promise<void | Error> {
// 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast
// eslint-disable-next-line @typescript-eslint/no-floating-promises
updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
}
export function modifyAstWithEdgeTreatmentAndTag(
@ -291,7 +290,7 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus(
modifiedAst: Node<Program>,
pathToEdgeTreatmentNode: Array<PathToNode>
) {
): Promise<void> {
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToEdgeTreatmentNode,
})

View File

@ -29,7 +29,9 @@ export function revolveSketch(
pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4),
axis: Selections
axisOrEdge: string,
axis: string,
edge: Selections
):
| {
modifiedAst: Node<Program>
@ -41,31 +43,34 @@ export function revolveSketch(
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode
// testing code
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
axis.graphSelections[0]?.codeRef.range
)
let generatedAxis
const lineNode = getNodeFromPath<CallExpression>(
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
if (axisOrEdge === 'Edge') {
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
edge.graphSelections[0]?.codeRef.range
)
const lineNode = getNodeFromPath<CallExpression>(
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
// TODO Kevin: What if |> close(%)?
// TODO Kevin: What if opposite edge
// TODO Kevin: What if the edge isn't planar to the sketch?
// TODO Kevin: add a tag.
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
const axisSelection = edge?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Generated axis selection is missing.')
generatedAxis = getEdgeTagCall(tag, axisSelection)
} else {
generatedAxis = createLiteral(axis)
}
/* Original Code */
const { node: sketchExpression } = sketchNode
@ -91,14 +96,12 @@ export function revolveSketch(
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const axisSelection = axis?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Axis selection is missing.')
if (!generatedAxis) return new Error('Generated axis selection is missing.')
const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({
angle: angle,
axis: getEdgeTagCall(tag, axisSelection),
axis: generatedAxis,
}),
createIdentifier(sketchVariableDeclarator.id.name),
])

View File

@ -49,17 +49,27 @@ export function addShell({
return new Error("Couldn't find extrude")
}
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
// Get the sketch ref from the selection
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
// We must find a technique for these situations that is robust to intermediate declarations
const sketchNode = getNodeFromPath<VariableDeclarator>(
const extrudeNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
graphSelection.codeRef.pathToNode,
extrudeLookupResult.pathToExtrudeNode,
'VariableDeclarator'
)
if (err(sketchNode)) {
return sketchNode
const segmentNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
extrudeLookupResult.pathToSegmentNode,
'VariableDeclarator'
)
if (err(extrudeNode) || err(segmentNode)) {
return new Error("Couldn't find extrude")
}
if (extrudeNode.node.init.type === 'CallExpression') {
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
} else if (segmentNode.node.init.type === 'PipeExpression') {
pathToExtrudeNode = extrudeLookupResult.pathToSegmentNode
} else {
return new Error("Couldn't find extrude")
}
const selectedArtifact = graphSelection.artifact

View File

@ -1,7 +1,13 @@
import { makeDefaultPlanes, assertParse, initPromise, Program } from 'lang/wasm'
import {
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ArtifactCommand,
ExecState,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
OrderedCommand,
ResponseMap,
createArtifactGraph,
filterArtifacts,
@ -22,6 +28,7 @@ import * as d3 from 'd3-force'
import path from 'path'
import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/*
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
@ -108,7 +115,7 @@ sketch002 = startSketchOn(offsetPlane001)
|> line([6.78, 15.01], %)
`
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
// add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests
const codeToWriteCacheFor = {
exampleCode1,
sketchOnFaceOnFaceEtc,
@ -120,8 +127,9 @@ type CodeKey = keyof typeof codeToWriteCacheFor
type CacheShape = {
[key in CodeKey]: {
orderedCommands: OrderedCommand[]
artifactCommands: ArtifactCommand[]
responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
}
}
@ -151,8 +159,9 @@ beforeAll(async () => {
await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = {
orderedCommands: engineCommandManager.orderedCommands,
artifactCommands: kclManager.execState.artifactCommands,
responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
}
}
const cache = JSON.stringify(cacheToWriteToFileTemp)
@ -171,18 +180,24 @@ afterAll(() => {
describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
orderedCommands,
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it(`there should be one sketch`, () => {
@ -217,17 +232,23 @@ describe('testing createArtifactGraph', () => {
})
})
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
orderedCommands,
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes for the extrusion and the sketch on face', () => {
@ -312,17 +333,23 @@ describe('testing createArtifactGraph', () => {
})
describe(`code with sketches but no extrusions or other 3D elements`, () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
orderedCommands,
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes, one for each sketch path', () => {
@ -377,17 +404,23 @@ describe('testing createArtifactGraph', () => {
describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
orderedCommands,
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
@ -399,17 +432,21 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
})
})
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
function getCommands(
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in
const orderedCommands = parsed[codeKey].orderedCommands
const artifactCommands = parsed[codeKey].artifactCommands
const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts
return {
orderedCommands,
artifactCommands,
responseMap,
ast,
execStateArtifacts,
}
}
@ -635,20 +672,30 @@ async function GraphTheGraph(
describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => {
const { orderedCommands, responseMap, ast } = getCommands('exampleCode1')
const map = createArtifactGraph({ orderedCommands, responseMap, ast })
const { artifactCommands, responseMap, ast, execStateArtifacts } =
getCommands('exampleCode1')
const map = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
const artifactCommand = artifactCommands.find(
(a) => a.command.type === type
)
if (!artifactCommand) {
throw new Error(`No artifactCommand found for ${type}`)
}
const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand: orderedCommands.find(
(a) =>
a.command.type === 'modeling_cmd_req' && a.command.cmd.type === type
)!,
artifactCommand,
responseMap,
getArtifact,
currentPlaneId,
ast,
execStateArtifacts,
})
return artifactsToUpdate.map(({ artifact }) => artifact)
}
@ -658,7 +705,7 @@ describe('testing getArtifactsToUpdate', () => {
segIds: [],
id: expect.any(String),
planeId: 'UUID-1',
sweepId: '',
sweepId: undefined,
codeRef: {
pathToNode: [['body', '']],
range: [37, 64, true],
@ -696,7 +743,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: '',
surfaceId: undefined,
edgeIds: [],
codeRef: {
range: [70, 86, true],
@ -723,7 +770,7 @@ describe('testing getArtifactsToUpdate', () => {
id: expect.any(String),
consumedEdgeId: expect.any(String),
edgeIds: [],
surfaceId: '',
surfaceId: undefined,
codeRef: {
range: [260, 299, true],
pathToNode: [['body', '']],

View File

@ -1,7 +1,15 @@
import { PathToNode, Program, SourceRange } from 'lang/wasm'
import {
ArtifactCommand,
ExecState,
PathToNode,
Program,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string
@ -29,7 +37,7 @@ export interface PathArtifact extends BaseArtifact {
type: 'path'
planeId: ArtifactId
segIds: Array<ArtifactId>
sweepId: ArtifactId
sweepId?: ArtifactId
solid2dId?: ArtifactId
codeRef: CodeRef
}
@ -52,7 +60,7 @@ export interface PathArtifactRich extends BaseArtifact {
export interface SegmentArtifact extends BaseArtifact {
type: 'segment'
pathId: ArtifactId
surfaceId: ArtifactId
surfaceId?: ArtifactId
edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId
codeRef: CodeRef
@ -60,7 +68,7 @@ export interface SegmentArtifact extends BaseArtifact {
interface SegmentArtifactRich extends BaseArtifact {
type: 'segment'
path: PathArtifact
surf: WallArtifact
surf?: WallArtifact
edges: Array<SweepEdge>
edgeCut?: EdgeCut
codeRef: CodeRef
@ -69,7 +77,7 @@ interface SegmentArtifactRich extends BaseArtifact {
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
@ -77,7 +85,7 @@ interface SweepArtifact extends BaseArtifact {
}
interface SweepArtifactRich extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge>
@ -112,7 +120,7 @@ interface EdgeCut extends BaseArtifact {
subType: 'fillet' | 'chamfer'
consumedEdgeId: ArtifactId
edgeIds: Array<ArtifactId>
surfaceId: ArtifactId
surfaceId?: ArtifactId
codeRef: CodeRef
}
@ -143,50 +151,47 @@ type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
export interface ResponseMap {
[commandId: string]: OkWebSocketResponseData
}
export interface OrderedCommand {
command: EngineCommand
range: SourceRange
}
/** Creates a graph of artifacts from a list of ordered commands and their responses
* muting the Map should happen entirely this function, other functions called within
* should return data on how to update the map, and not do so directly.
*/
export function createArtifactGraph({
orderedCommands,
artifactCommands,
responseMap,
ast,
execStateArtifacts,
}: {
orderedCommands: Array<OrderedCommand>
artifactCommands: Array<ArtifactCommand>
responseMap: ResponseMap
ast: Program
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}) {
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
orderedCommands.forEach((orderedCommand) => {
if (orderedCommand.command?.type === 'modeling_cmd_req') {
if (orderedCommand.command.cmd.type === 'enable_sketch_mode') {
currentPlaneId = orderedCommand.command.cmd.entity_id
}
if (orderedCommand.command.cmd.type === 'sketch_mode_disable') {
currentPlaneId = ''
}
for (const artifactCommand of artifactCommands) {
if (artifactCommand.command.type === 'enable_sketch_mode') {
currentPlaneId = artifactCommand.command.entity_id
}
if (artifactCommand.command.type === 'sketch_mode_disable') {
currentPlaneId = ''
}
const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand,
artifactCommand,
responseMap,
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
execStateArtifacts,
})
artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
myMap.set(id, mergedArtifact)
})
})
}
return myMap
}
@ -227,30 +232,30 @@ function mergeArtifacts(
* can remove this.
*/
export function getArtifactsToUpdate({
orderedCommand: { command, range },
artifactCommand,
getArtifact,
responseMap,
currentPlaneId,
ast,
execStateArtifacts,
}: {
orderedCommand: OrderedCommand
artifactCommand: ArtifactCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Program
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}): Array<{
id: ArtifactId
artifact: Artifact
}> {
const range = sourceRangeFromRust(artifactCommand.range)
const pathToNode = getNodePathFromSourceRange(ast, range)
// expect all to be `modeling_cmd_req` as batch commands have
// already been expanded before being added to orderedCommands
if (command.type !== 'modeling_cmd_req') return []
const id = command.cmd_id
const id = artifactCommand.cmdId
const response = responseMap[id]
const cmd = command.cmd
const cmd = artifactCommand.command
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr
if (cmd.type === 'make_plane' && range[1] !== 0) {
@ -303,7 +308,7 @@ export function getArtifactsToUpdate({
id,
segIds: [],
planeId: currentPlaneId,
sweepId: '',
sweepId: undefined,
codeRef: { range, pathToNode },
},
})
@ -338,7 +343,7 @@ export function getArtifactsToUpdate({
type: 'segment',
id,
pathId,
surfaceId: '',
surfaceId: undefined,
edgeIds: [],
codeRef: { range, pathToNode },
},
@ -372,7 +377,11 @@ export function getArtifactsToUpdate({
})
}
return returnArr
} else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
} else if (
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({
id,
@ -393,6 +402,33 @@ export function getArtifactsToUpdate({
artifact: { ...path, sweepId: id },
})
return returnArr
} else if (
cmd.type === 'loft' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'loft'
) {
returnArr.push({
id,
artifact: {
type: 'sweep',
subType: 'loft',
id,
// TODO: make sure to revisit this choice, don't think it matters for now
pathId: cmd.section_ids[0],
surfaceIds: [],
edgeIds: [],
codeRef: { range, pathToNode },
},
})
for (const sectionId of cmd.section_ids) {
const path = getArtifact(sectionId)
if (path?.type === 'path')
returnArr.push({
id: sectionId,
artifact: { ...path, sweepId: id },
})
}
return returnArr
} else if (
cmd.type === 'solid3d_get_extrusion_face_info' &&
response?.type === 'modeling' &&
@ -414,7 +450,8 @@ export function getArtifactsToUpdate({
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
sweepId: path.sweepId,
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
pathIds: [],
},
})
@ -422,15 +459,17 @@ export function getArtifactsToUpdate({
id: curve_id,
artifact: { ...seg, surfaceId: face_id },
})
const sweep = getArtifact(path.sweepId)
if (sweep?.type === 'sweep') {
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
if (path.sweepId) {
const sweep = getArtifact(path.sweepId)
if (sweep?.type === 'sweep') {
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
}
}
}
}
@ -447,19 +486,22 @@ export function getArtifactsToUpdate({
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
sweepId: path.sweepId,
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
pathIds: [],
},
})
const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
if (path.sweepId) {
const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
}
}
}
})
@ -497,7 +539,8 @@ export function getArtifactsToUpdate({
? 'adjacent'
: 'opposite',
segId: cmd.edge_id,
sweepId: path.sweepId,
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
},
},
{
@ -508,7 +551,7 @@ export function getArtifactsToUpdate({
},
},
{
id: path.sweepId,
id: sweep.id,
artifact: {
...sweep,
edgeIds: [response.data.modeling_response.data.edge],
@ -524,7 +567,7 @@ export function getArtifactsToUpdate({
subType: cmd.cut_type,
consumedEdgeId: cmd.edge_id,
edgeIds: [],
surfaceId: '',
surfaceId: undefined,
codeRef: { range, pathToNode },
},
})
@ -686,10 +729,12 @@ export function expandSegment(
{ key: segment.pathId, types: ['path'] },
artifactGraph
)
const surf = getArtifactOfTypes(
{ key: segment.surfaceId, types: ['wall'] },
artifactGraph
)
const surf = segment.surfaceId
? getArtifactOfTypes(
{ key: segment.surfaceId, types: ['wall'] },
artifactGraph
)
: undefined
const edges = getArtifactsOfTypes(
{ keys: segment.edgeIds, types: ['sweepEdge'] },
artifactGraph
@ -806,6 +851,7 @@ export function getSweepFromSuspectedSweepSurface(
artifactGraph
)
if (err(path)) return path
if (!path.sweepId) return new Error('Path does not have a sweepId')
return getArtifactOfTypes(
{ key: path.sweepId, types: ['sweep'] },
artifactGraph
@ -823,6 +869,7 @@ export function getSweepFromSuspectedPath(
): SweepArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
if (err(path)) return path
if (!path.sweepId) return new Error('Path does not have a sweepId')
return getArtifactOfTypes(
{ key: path.sweepId, types: ['sweep'] },
artifactGraph

View File

@ -1,10 +1,10 @@
import {
ArtifactCommand,
defaultRustSourceRange,
defaultSourceRange,
ExecState,
Program,
RustSourceRange,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib'
@ -20,7 +20,6 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
ArtifactGraph,
EngineCommand,
OrderedCommand,
ResponseMap,
createArtifactGraph,
} from 'lang/std/artifactGraph'
@ -37,6 +36,7 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -1303,7 +1303,7 @@ export enum EngineCommandManagerEvents {
*
* As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response.
*
* Also all commands that are sent are kept track of in {@link orderedCommands} and their responses are kept in {@link responseMap}
* Also all commands that are sent are kept track of in WASM artifactCommands and their responses are kept in {@link responseMap}
* Both of these data structures are used to process the {@link artifactGraph}.
*/
@ -1329,12 +1329,7 @@ export class EngineCommandManager extends EventTarget {
[commandId: string]: PendingMessage
} = {}
/**
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
* list of the individual commands, this is used to process all the commands into the artifactGraph
*/
orderedCommands: Array<OrderedCommand> = []
/**
* A map of the responses to the {@link orderedCommands}, when processing the commands into the artifactGraph, this response map allow
* A map of the responses to the WASM artifactCommands, when processing the commands into the artifactGraph, this response map allow
* us to look up the response by command id
*/
responseMap: ResponseMap = {}
@ -1830,7 +1825,6 @@ export class EngineCommandManager extends EventTarget {
}
}
async startNewSession() {
this.orderedCommands = []
this.responseMap = {}
await this.initPlanes()
}
@ -2073,28 +2067,6 @@ export class EngineCommandManager extends EventTarget {
isSceneCommand,
}
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
range: sourceRangeFromRust(message.range),
})
} else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => {
const cmdId = req.cmd_id || ''
const range = cmdId
? sourceRangeFromRust(message.idToRangeMap[cmdId])
: defaultSourceRange()
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: req.cmd_id,
cmd: req.cmd,
}
this.orderedCommands.push({
command: cmd,
range,
})
})
}
this.engineConnection?.send(message.command)
return promise
}
@ -2115,11 +2087,16 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(ast: Program) {
updateArtifactGraph(
ast: Node<Program>,
artifactCommands: ArtifactCommand[],
execStateArtifacts: ExecState['artifacts']
) {
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
artifactCommands,
responseMap: this.responseMap,
ast,
execStateArtifacts,
})
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {

View File

@ -1,5 +1,23 @@
import { isDesktop } from 'lib/isDesktop'
// Polyfill window.electron fs functions as needed when in a nodejs context
// (INTENDED FOR VITEST SHINANGANS)
if (window?.electron === undefined) {
;(async () => {
const fs = await import('node:fs/promises')
const path = await import('node:path')
Object.assign(window, {
electron: {
readFile: fs.readFile,
stat: fs.stat,
readdir: fs.readdir,
path,
process: {},
},
})
})().catch(console.error)
}
/// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib
/// when executing code.
@ -19,13 +37,9 @@ class FileSystemManager {
}
async readFile(path: string): Promise<Uint8Array> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(
new Error(
'This function can only be called from the desktop application'
)
)
// Using local file system only works from desktop and nodejs
if (!window?.electron?.readFile) {
return Promise.reject(new Error('No polyfill found for this function'))
}
return this.join(this.dir, path).then((filePath) => {
@ -35,12 +49,8 @@ class FileSystemManager {
async exists(path: string): Promise<boolean | void> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(
new Error(
'This function can only be called from the desktop application'
)
)
if (!window?.electron?.stat) {
return Promise.reject(new Error('No polyfill found for this function'))
}
return this.join(this.dir, path).then(async (file) => {
@ -57,12 +67,8 @@ class FileSystemManager {
async getAllFiles(path: string): Promise<string[] | void> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(
new Error(
'This function can only be called from the desktop application'
)
)
if (!window?.electron?.readdir) {
return Promise.reject(new Error('No polyfill found for this function'))
}
return this.join(this.dir, path).then((filepath) => {

View File

@ -1,4 +1,5 @@
import init, {
import {
init,
parse_wasm,
recast_wasm,
execute,
@ -16,7 +17,9 @@ import init, {
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
} from '../wasm-lib/pkg/wasm_lib'
reloadModule,
} from 'lib/wasm_lib_wrapper'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection'
@ -45,7 +48,13 @@ import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRang
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -144,6 +153,7 @@ export const wasmUrl = () => {
// Initialise the wasm module.
const initialise = async () => {
try {
await reloadModule()
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
@ -223,6 +233,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
)
}
@ -247,6 +258,8 @@ export const isPathToNodeNumber = (
export interface ExecState {
memory: ProgramMemory
operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact }
artifactCommands: ArtifactCommand[]
}
/**
@ -257,6 +270,8 @@ export function emptyExecState(): ExecState {
return {
memory: ProgramMemory.empty(),
operations: [],
artifacts: {},
artifactCommands: [],
}
}
@ -264,6 +279,8 @@ function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
return {
memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
}
}
@ -506,22 +523,6 @@ export const executor = async (
return Promise.reject(programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession()
const _programMemory = await _executor(
node,
engineCommandManager,
programMemoryOverride
)
await engineCommandManager.waitForAllCommands()
return _programMemory
}
export const _executor = async (
node: Node<Program>,
engineCommandManager: EngineCommandManager,
programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => {
if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride)
@ -550,7 +551,8 @@ export const _executor = async (
parsed.error.kind,
parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations
parsed.operations,
parsed.artifactCommands
)
return Promise.reject(kclError)
@ -610,6 +612,7 @@ export const modifyAstForSketch = async (
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
)
@ -679,6 +682,7 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
)
}

View File

@ -37,6 +37,10 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue
}
Sweep: {
path: Selections
profile: Selections
}
Loft: {
selection: Selections
}
@ -47,7 +51,9 @@ export type ModelingCommandSchema = {
Revolve: {
selection: Selections
angle: KclCommandValue
axis: Selections
axisOrEdge: string
axis: string
edge: Selections
}
Fillet: {
// todo
@ -290,6 +296,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
Sweep: {
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep',
status: 'development',
needsReview: true,
args: {
profile: {
inputType: 'selection',
selectionTypes: ['solid2D'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
},
path: {
inputType: 'selection',
selectionTypes: ['segment', 'path'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
},
},
},
Loft: {
description: 'Create a 3D body by blending between two or more sketches',
icon: 'loft',
@ -324,10 +357,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve',
status: 'development',
needsReview: true,
args: {
selection: {
@ -336,9 +369,34 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection
required: true,
skip: true,
warningMessage:
'The revolve workflow is new and under tested. Please break it and report issues.',
},
axisOrEdge: {
inputType: 'options',
required: true,
defaultValue: 'Axis',
options: [
{ name: 'Axis', isCurrent: true, value: 'Axis' },
{ name: 'Edge', isCurrent: false, value: 'Edge' },
],
},
axis: {
required: true,
required: (commandContext) =>
['Axis'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'options',
options: [
{ name: 'X Axis', isCurrent: true, value: 'X' },
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
],
},
edge: {
required: (commandContext) =>
['Edge'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,

View File

@ -68,7 +68,7 @@ export const revolveAxisValidator = async ({
}
const sketchSelection = artifact.pathId
let edgeSelection = data.axis.graphSelections[0].artifact?.id
let edgeSelection = data.edge.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
@ -101,7 +101,7 @@ export const revolveAxisValidator = async ({
return true
} else {
// return error message for the toast
return 'Unable to revolve with selected axis'
return 'Unable to revolve with selected edge'
}
}

View File

@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch',
EXTRUDE: 'extrude',
LOFT: 'loft',
SWEEP: 'sweep',
SHELL: 'shell',
SEGMENT: 'seg',
REVOLVE: 'revolve',

View File

@ -15,6 +15,7 @@ import {
StateMachineCommandSetSchema,
} from './commandTypes'
import { DEV } from 'env'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
interface CreateMachineCommandProps<
T extends AnyStateMachine,
@ -84,7 +85,7 @@ export function createMachineCommand<
} else if ('status' in commandConfig) {
const { status } = commandConfig
if (status === 'inactive') return null
if (status === 'development' && !DEV) return null
if (status === 'development' && !(DEV || IS_NIGHTLY_OR_DEBUG)) return null
}
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined

51
src/lib/exceptions.ts Normal file
View File

@ -0,0 +1,51 @@
import { kclManager } from 'lib/singletons'
import { reloadModule, getModule } from 'lib/wasm_lib_wrapper'
import toast from 'react-hot-toast'
import { reportRejection } from './trap'
let initialized = false
/**
* WASM/Rust runtime can panic and the original try/catch/finally blocks will not trigger
* on the await promise. The interface will killed. This means we need to catch the error at
* the global/DOM level. This will have to interface with whatever controlflow that needs to be picked up
* within the error branch in the typescript to cover the application state.
*/
export const initializeWindowExceptionHandler = () => {
if (window && !initialized) {
window.addEventListener('error', (event) => {
void (async () => {
if (matchImportExportErrorCrash(event.message)) {
// do global singleton cleanup
kclManager.executeAstCleanUp()
toast.error(
'You have hit a KCL execution bug! Put your KCL code in a github issue to help us resolve this bug.'
)
try {
await reloadModule()
await getModule().default()
} catch (e) {
console.error('Failed to initialize wasm_lib')
console.error(e)
}
}
})().catch(reportRejection)
})
// Make sure we only initialize this event listener once
initialized = true
} else {
console.error(
`Failed to initialize, window: ${window}, initialized:${initialized}`
)
}
}
/**
* Specifically match a substring of the message error to detect an import export runtime issue
* when the WASM runtime panics
*/
const matchImportExportErrorCrash = (message: string): boolean => {
// called `Result::unwrap_throw()` on an `Err` value
const substringError = '`Result::unwrap_throw()` on an `Err` value'
return message.indexOf(substringError) !== -1 ? true : false
}

View File

@ -155,7 +155,7 @@ export interface components {
color?: string | null
/** @description The material that the filament is made of. */
material: components['schemas']['FilamentMaterial']
/** @description The name of the filament, this is likely specfic to the manufacturer. */
/** @description The name of the filament, this is likely specific to the manufacturer. */
name?: string | null
}
/** @description The material that the filament is made of. */

52
src/lib/markdown.ts Normal file
View File

@ -0,0 +1,52 @@
import { MarkedOptions, Renderer, unescape } from '@ts-stack/markdown'
import { openExternalBrowserIfDesktop } from './openWindow'
/**
* Main goal of this custom renderer is to prevent links from changing the current location
* this is specially important for the desktop app.
*/
export class SafeRenderer extends Renderer {
constructor(options: MarkedOptions) {
super(options)
// Attach a global function for non-react anchor elements that need safe navigation
window.openExternalLink = (e: React.MouseEvent<HTMLAnchorElement>) => {
openExternalBrowserIfDesktop()(e)
}
}
// Extended from https://github.com/ts-stack/markdown/blob/c5c1925c1153ca2fe9051c356ef0ddc60b3e1d6a/packages/markdown/src/renderer.ts#L116
link(href: string, title: string, text: string): string {
if (this.options.sanitize) {
let prot: string
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase()
} catch (e) {
return text
}
if (
// eslint-disable-next-line no-script-url
prot.indexOf('javascript:') === 0 ||
prot.indexOf('vbscript:') === 0 ||
prot.indexOf('data:') === 0
) {
return text
}
}
let out =
'<a onclick="openExternalLink(event)" target="_blank" href="' + href + '"'
if (title) {
out += ' title="' + title + '"'
}
out += '>' + text + '</a>'
return out
}
}

View File

@ -137,7 +137,7 @@ See later source ranges for more context. about the sweep`,
{ key: artifact.pathId, types: ['path'] },
artifactGraph
)
if (!err(path)) {
if (!err(path) && path.sweepId) {
const sweep = getArtifactOfTypes(
{ key: path.sweepId, types: ['sweep'] },
artifactGraph

View File

@ -670,6 +670,7 @@ export function codeToIdSelections(
}
}
if (type === 'extrude-wall' && entry.artifact.type === 'segment') {
if (!entry.artifact.surfaceId) return
const wall = engineCommandManager.artifactGraph.get(
entry.artifact.surfaceId
)
@ -714,6 +715,7 @@ export function codeToIdSelections(
(type === 'end-cap' || type === 'start-cap') &&
entry.artifact.type === 'path'
) {
if (!entry.artifact.sweepId) return
const extrusion = getArtifactOfTypes(
{
key: entry.artifact.sweepId,

View File

@ -1,20 +1,16 @@
import {
Program,
ProgramMemory,
_executor,
executor,
SourceRange,
ExecState,
} from '../lang/wasm'
import {
EngineCommandManager,
EngineCommandManagerEvents,
} from 'lang/std/engineConnection'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph'
import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { err, reportRejection } from 'lib/trap'
import { toSync } from './utils'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
type WebSocketResponse = Models['WebSocketResponse_type']
@ -94,36 +90,7 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession()
const execState = await _executor(ast, mockEngineCommandManager, pmo)
const execState = await executor(ast, mockEngineCommandManager, pmo)
await mockEngineCommandManager.waitForAllCommands()
return execState
}
export async function executor(
ast: Node<Program>,
pmo: ProgramMemory = ProgramMemory.empty()
): Promise<ExecState> {
const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({
setIsStreamReady: () => {},
setMediaStream: () => {},
width: 0,
height: 0,
makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes))
},
})
return new Promise((resolve) => {
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
toSync(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession()
const execState = await _executor(ast, engineCommandManager, pmo)
await engineCommandManager.waitForAllCommands()
resolve(execState)
}, reportRejection)
)
})
}

View File

@ -8,6 +8,7 @@ import {
modelingMachine,
pipeHasCircle,
} from 'machines/modelingMachine'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
import { EventFrom, StateFrom } from 'xstate'
export type ToolbarModeName = 'modeling' | 'sketching'
@ -103,7 +104,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
data: { name: 'Revolve', groupId: 'modeling' },
}),
icon: 'revolve',
status: DEV ? 'available' : 'kcl-only',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Revolve',
hotkey: 'R',
description:
@ -118,17 +119,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'sweep',
onClick: () => console.error('Sweep not yet implemented'),
onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Sweep', groupId: 'modeling' },
}),
icon: 'sweep',
status: 'unavailable',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Sweep',
hotkey: 'W',
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/498',
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/sweep',
},
],
},
@ -161,7 +166,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
data: { name: 'Fillet', groupId: 'modeling' },
}),
icon: 'fillet3d',
status: DEV ? 'available' : 'kcl-only',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Fillet',
hotkey: 'F',
description: 'Round the edges of a 3D solid.',
@ -442,18 +447,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' })),
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
!state.matches({ Sketch: 'Circle tool' }) &&
!state.matches({ Sketch: 'circle3PointToolSelect' })),
isActive: (state) =>
state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'circle3PointToolSelect' }),
hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
showTitle: false,
description: 'Start drawing a circle from its center',
links: [
{
label: 'GitHub issue',
url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
},
],
links: [],
},
{
id: 'circle-three-points',
@ -470,7 +473,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}),
icon: 'circle',
status: 'available',
title: 'Three-point circle',
title: '3-point circle',
showTitle: false,
description: 'Draw a circle defined by three points',
links: [],

View File

@ -153,7 +153,10 @@ export function toSync<F extends AsyncFn<F>>(
) => void | PromiseLike<void | null | undefined> | null | undefined
): (...args: Parameters<F>) => void {
return (...args: Parameters<F>) => {
fn(...args).catch(onReject)
void fn(...args).catch((...args) => {
console.error(...args)
return onReject(...args)
})
}
}
@ -342,7 +345,7 @@ export function onDragNumberCalculation(text: string, e: MouseEvent) {
)
const newVal = roundOff(addition, precision)
if (isNaN(newVal)) {
if (Number.isNaN(newVal)) {
return
}

108
src/lib/wasm_lib_wrapper.ts Normal file
View File

@ -0,0 +1,108 @@
/**
* This wrapper file is to enable reloading of the wasm_lib.js file.
* When the wasm instance bricks there is no API or interface to restart,
* restore, or re init the WebAssembly instance. The entire application would need
* to restart.
* A way to bypass this is by reloading the entire .js file so the global wasm variable
* gets reinitialized and we do not use that old reference
*/
import {
parse_wasm as ParseWasm,
recast_wasm as RecastWasm,
execute as Execute,
kcl_lint as KclLint,
modify_ast_for_sketch_wasm as ModifyAstForSketch,
is_points_ccw as IsPointsCcw,
get_tangential_arc_to_info as GetTangentialArcToInfo,
program_memory_init as ProgramMemoryInit,
make_default_planes as MakeDefaultPlanes,
coredump as CoreDump,
toml_stringify as TomlStringify,
default_app_settings as DefaultAppSettings,
parse_app_settings as ParseAppSettings,
parse_project_settings as ParseProjectSettings,
default_project_settings as DefaultProjectSettings,
base64_decode as Base64Decode,
clear_scene_and_bust_cache as ClearSceneAndBustCache,
} from '../wasm-lib/pkg/wasm_lib'
type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib')
// Stores the result of the import of the wasm_lib file
let data: ModuleType
// Imports the .js file again which will clear the old import
// This allows us to reinitialize the wasm instance
export async function reloadModule() {
data = await import(`../wasm-lib/pkg/wasm_lib`)
}
export function getModule(): ModuleType {
return data
}
export async function init(module_or_path: any) {
return await getModule().default(module_or_path)
}
export const parse_wasm: typeof ParseWasm = (...args) => {
return getModule().parse_wasm(...args)
}
export const recast_wasm: typeof RecastWasm = (...args) => {
return getModule().recast_wasm(...args)
}
export const execute: typeof Execute = (...args) => {
return getModule().execute(...args)
}
export const kcl_lint: typeof KclLint = (...args) => {
return getModule().kcl_lint(...args)
}
export const modify_ast_for_sketch_wasm: typeof ModifyAstForSketch = (
...args
) => {
return getModule().modify_ast_for_sketch_wasm(...args)
}
export const is_points_ccw: typeof IsPointsCcw = (...args) => {
return getModule().is_points_ccw(...args)
}
export const get_tangential_arc_to_info: typeof GetTangentialArcToInfo = (
...args
) => {
return getModule().get_tangential_arc_to_info(...args)
}
export const program_memory_init: typeof ProgramMemoryInit = (...args) => {
return getModule().program_memory_init(...args)
}
export const make_default_planes: typeof MakeDefaultPlanes = (...args) => {
return getModule().make_default_planes(...args)
}
export const coredump: typeof CoreDump = (...args) => {
return getModule().coredump(...args)
}
export const toml_stringify: typeof TomlStringify = (...args) => {
return getModule().toml_stringify(...args)
}
export const default_app_settings: typeof DefaultAppSettings = (...args) => {
return getModule().default_app_settings(...args)
}
export const parse_app_settings: typeof ParseAppSettings = (...args) => {
return getModule().parse_app_settings(...args)
}
export const parse_project_settings: typeof ParseProjectSettings = (
...args
) => {
return getModule().parse_project_settings(...args)
}
export const default_project_settings: typeof DefaultProjectSettings = (
...args
) => {
return getModule().default_project_settings(...args)
}
export const base64_decode: typeof Base64Decode = (...args) => {
return getModule().base64_decode(...args)
}
export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
...args
) => {
return getModule().clear_scene_and_bust_cache(...args)
}

View File

@ -45,6 +45,7 @@ import {
import { revolveSketch } from 'lang/modifyAst/addRevolve'
import {
addOffsetPlane,
addSweep,
deleteFromSelection,
extrudeSketch,
loftSketches,
@ -266,6 +267,7 @@ export type ModelingMachineEvent =
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Sweep'; data?: ModelingCommandSchema['Sweep'] }
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
@ -418,6 +420,8 @@ export const modelingMachine = setup({
},
'is editing existing sketch': ({ context: { sketchDetails } }) =>
isEditingExistingSketch({ sketchDetails }),
'is editing 3-point circle': ({ context: { sketchDetails } }) =>
isEditing3PointCircle({ sketchDetails }),
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'horizontal')
if (trap(info)) return false
@ -685,7 +689,7 @@ export const modelingMachine = setup({
if (event.type !== 'Revolve') return
;(async () => {
if (!event.data) return
const { selection, angle, axis } = event.data
const { selection, angle, axis, edge, axisOrEdge } = event.data
let ast = kclManager.ast
if (
'variableName' in angle &&
@ -710,7 +714,9 @@ export const modelingMachine = setup({
'variableName' in angle
? angle.variableIdentifierAst
: angle.valueAst,
axis
axisOrEdge,
axis,
edge
)
if (trap(revolveSketchRes)) return
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
@ -763,30 +769,6 @@ export const modelingMachine = setup({
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
})().catch(reportRejection)
},
'AST fillet': ({ event }) => {
if (event.type !== 'Fillet') return
if (!event.data) return
// Extract inputs
const ast = kclManager.ast
const { selection, radius } = event.data
const parameters: FilletParameters = {
type: EdgeTreatmentType.Fillet,
radius,
}
// Apply fillet to selection
const applyEdgeTreatmentToSelectionResult = applyEdgeTreatmentToSelection(
ast,
selection,
parameters
)
if (err(applyEdgeTreatmentToSelectionResult))
return applyEdgeTreatmentToSelectionResult
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
},
'set selection filter to curves only': () => {
;(async () => {
await engineCommandManager.sendSceneCommand({
@ -1566,6 +1548,66 @@ export const modelingMachine = setup({
}
}
),
sweepAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Sweep'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { profile, path } = input
// Find the profile declaration
const profileNodePath = getNodePathFromSourceRange(
ast,
profile.graphSelections[0].codeRef.range
)
const profileNode = getNodeFromPath<VariableDeclarator>(
ast,
profileNodePath,
'VariableDeclarator'
)
if (err(profileNode)) {
return new Error("Couldn't parse profile selection")
}
const profileDeclarator = profileNode.node
// Find the path declaration
const pathNodePath = getNodePathFromSourceRange(
ast,
path.graphSelections[0].codeRef.range
)
const pathNode = getNodeFromPath<VariableDeclarator>(
ast,
pathNodePath,
'VariableDeclarator'
)
if (err(pathNode)) {
return new Error("Couldn't parse path selection")
}
const pathDeclarator = pathNode.node
// Perform the sweep
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
const updateAstResult = await kclManager.updateAst(
sweepRes.modifiedAst,
true,
{
focusPath: [sweepRes.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
loftAstMod: fromPromise(
async ({
input,
@ -1670,6 +1712,33 @@ export const modelingMachine = setup({
}
}
),
filletAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Fillet'] | undefined
}) => {
if (!input) {
return new Error('No input provided')
}
// Extract inputs
const ast = kclManager.ast
const { selection, radius } = input
const parameters: FilletParameters = {
type: EdgeTreatmentType.Fillet,
radius,
}
// Apply fillet to selection
const filletResult = await applyEdgeTreatmentToSelection(
ast,
selection,
parameters
)
if (err(filletResult)) return filletResult
}
),
'submit-prompt-edit': fromPromise(
async ({ input }: { input: ModelingCommandSchema['Prompt-to-edit'] }) => {
console.log('doing thing', input)
@ -1734,6 +1803,11 @@ export const modelingMachine = setup({
reenter: false,
},
Sweep: {
target: 'Applying sweep',
reenter: true,
},
Loft: {
target: 'Applying loft',
reenter: true,
@ -1745,9 +1819,8 @@ export const modelingMachine = setup({
},
Fillet: {
target: 'idle',
actions: ['AST fillet'],
reenter: false,
target: 'Applying fillet',
reenter: true,
},
Export: {
@ -2082,6 +2155,10 @@ export const modelingMachine = setup({
target: 'SketchIdle',
guard: 'is editing existing sketch',
},
{
target: 'circle3PointToolSelect',
guard: 'is editing 3-point circle',
},
'Line tool',
],
},
@ -2413,13 +2490,8 @@ export const modelingMachine = setup({
circle3PointToolSelect: {
invoke: {
id: 'actor-circle-3-point',
input: function ({ context, event }) {
// These are not really necessary but I believe they are needed
// to satisfy TypeScript type narrowing or undefined check.
if (event.type !== 'change tool') return
if (event.data?.tool !== 'circle3Points') return
input: function ({ context }) {
if (!context.sketchDetails) return
return context.sketchDetails
},
src: 'actorCircle3Point',
@ -2527,6 +2599,19 @@ export const modelingMachine = setup({
},
},
'Applying sweep': {
invoke: {
src: 'sweepAstMod',
id: 'sweepAstMod',
input: ({ event }) => {
if (event.type !== 'Sweep') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying loft': {
invoke: {
src: 'loftAstMod',
@ -2553,6 +2638,19 @@ export const modelingMachine = setup({
},
},
'Applying fillet': {
invoke: {
src: 'filletAstMod',
id: 'filletAstMod',
input: ({ event }) => {
if (event.type !== 'Fillet') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying Prompt-to-edit': {
invoke: {
src: 'submit-prompt-edit',
@ -2638,6 +2736,34 @@ export function isEditingExistingSketch({
)
return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle
}
export function isEditing3PointCircle({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
if (!sketchDetails?.sketchPathToNode) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchPathToNode,
'VariableDeclarator'
)
if (err(variableDeclaration)) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasStartProfileAt = pipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
)
const hasCircle3Point = pipeExpression.body.some(
(item) =>
item.type === 'CallExpressionKw' &&
item.callee.name === 'circleThreePoint'
)
return (
(hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle3Point
)
}
export function pipeHasCircle({
sketchDetails,
}: {
@ -2658,6 +2784,27 @@ export function pipeHasCircle({
)
return hasCircle
}
export function pipeHasCircleThreePoint({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
if (!sketchDetails?.sketchPathToNode) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchPathToNode,
'VariableDeclarator'
)
if (err(variableDeclaration)) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasCircle = pipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'circleThreePoint'
)
return hasCircle
}
export function canRectangleOrCircleTool({
sketchDetails,

View File

@ -320,6 +320,11 @@ export function getAutoUpdater(): AppUpdater {
}
app.on('ready', () => {
// Disable auto updater on non-versioned builds
if (packageJSON.version === '0.0.0') {
return
}
const autoUpdater = getAutoUpdater()
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
// so at the moment this isn't worth enabling

View File

@ -41,13 +41,13 @@ export default function Export() {
export to almost any CAD software.
</p>
<p className="my-4">
Our teammate David is working on the file format, check out{' '}
Our teammate Katie is working on the file format, check out{' '}
<a
href="https://www.youtube.com/watch?v=8SuW0qkYCZo"
href="https://github.com/KhronosGroup/glTF/pull/2343"
target="_blank"
rel="noreferrer noopener"
>
his talk with the Metaverse Standards Forum
her standards proposal on GitHub
</a>
!
</p>

View File

@ -32,6 +32,8 @@ export const PACKAGE_NAME = isDesktop()
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const IS_NIGHTLY_OR_DEBUG = IS_NIGHTLY || APP_VERSION === '0.0.0'
export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''

151
src/wasm-lib/Cargo.lock generated
View File

@ -176,7 +176,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -187,7 +187,7 @@ checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -204,7 +204,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -474,7 +474,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -665,7 +665,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -676,7 +676,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -723,7 +723,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.33"
version = "0.1.34"
dependencies = [
"Inflector",
"anyhow",
@ -737,7 +737,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -748,7 +748,38 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.96",
]
[[package]]
@ -791,7 +822,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -829,7 +860,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -990,7 +1021,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -1086,7 +1117,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -1132,17 +1163,18 @@ dependencies = [
[[package]]
name = "handlebars"
version = "6.2.0"
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315"
checksum = "3d6b224b95c1e668ac0270325ad563b2eef1469fbbb8959bc7c692c844b813d9"
dependencies = [
"derive_builder",
"log",
"num-order",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror 1.0.68",
"thiserror 2.0.0",
]
[[package]]
@ -1494,7 +1526,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -1684,7 +1716,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.29"
version = "0.2.30"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1752,7 +1784,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.19"
version = "0.1.20"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -1819,9 +1851,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.79"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9cab4476455be70ea57643c31444068b056d091bd348cab6044c0d8ad7fcc"
checksum = "ce9e58b34645facea36bc9f4868877bbe6fcac01b92896825e8d4f2f7c71dbd6"
dependencies = [
"anyhow",
"chrono",
@ -1839,6 +1871,7 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"ts-rs",
"uuid",
]
@ -1851,18 +1884,18 @@ dependencies = [
"kittycad-modeling-cmds-macros-impl",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
name = "kittycad-modeling-cmds-macros-impl"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607507a8a0e4273b943179f0a3ef8e90712308d1d3095246040c29cfdbf985b"
checksum = "fdb4ee23cc996aa2dca7584d410e8826e08161e1ac4335bb646d5ede33f37cb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2012,7 +2045,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2311,7 +2344,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2325,7 +2358,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2365,7 +2398,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2423,7 +2456,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2553,7 +2586,7 @@ dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2612,7 +2645,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -2625,7 +2658,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3160,7 +3193,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3224,7 +3257,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3235,7 +3268,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3259,7 +3292,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3280,7 +3313,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3429,7 +3462,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3440,7 +3473,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3462,7 +3495,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3505,9 +3538,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.95"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@ -3531,7 +3564,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3639,7 +3672,7 @@ checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3650,7 +3683,7 @@ checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3762,7 +3795,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3904,7 +3937,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -3932,7 +3965,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -4015,7 +4048,7 @@ checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
"termcolor",
]
@ -4158,9 +4191,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
dependencies = [
"getrandom",
"serde",
@ -4194,7 +4227,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -4255,7 +4288,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
"wasm-bindgen-shared",
]
@ -4291,7 +4324,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -4672,7 +4705,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
"synstructure",
]
@ -4694,7 +4727,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]
@ -4714,7 +4747,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
"synstructure",
]
@ -4743,7 +4776,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.96",
]
[[package]]

View File

@ -76,7 +76,10 @@ members = [
[workspace.dependencies]
http = "1"
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.79", features = ["websocket"] }
kittycad-modeling-cmds = { version = "0.2.89", features = [
"ts-rs",
"websocket",
] }
[workspace.lints.clippy]
assertions_on_result_states = "warn"

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.33"
version = "0.1.34"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -20,7 +20,7 @@ quote = "1"
regex = "1.11"
serde = { version = "1.0.217", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.95", features = ["full"] }
syn = { version = "2.0.96", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.95"

View File

@ -832,7 +832,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
let result = match crate::test_server::execute_and_snapshot(code, crate::settings::types::UnitLength::Mm, None).await {
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", #fn_name, #index),
kcl_source: #code_block.to_string(),
}));

View File

@ -37,7 +37,7 @@ mod test_examples_someFn {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "someFn", 0usize),
kcl_source: "someFn()".to_string(),
}));

View File

@ -37,7 +37,7 @@ mod test_examples_someFn {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "someFn", 0usize),
kcl_source: "someFn()".to_string(),
}));

View File

@ -38,7 +38,7 @@ mod test_examples_show {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "show", 0usize),
kcl_source: "This is another code block.\nyes sirrr.\nshow".to_string(),
}));
@ -92,7 +92,7 @@ mod test_examples_show {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "show", 1usize),
kcl_source: "This is code.\nIt does other shit.\nshow".to_string(),
}));

View File

@ -38,7 +38,7 @@ mod test_examples_show {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "show", 0usize),
kcl_source: "This is code.\nIt does other shit.\nshow".to_string(),
}));

View File

@ -39,7 +39,7 @@ mod test_examples_my_func {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "my_func", 0usize),
kcl_source: "This is another code block.\nyes sirrr.\nmyFunc".to_string(),
}));
@ -93,7 +93,7 @@ mod test_examples_my_func {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "my_func", 1usize),
kcl_source: "This is code.\nIt does other shit.\nmyFunc".to_string(),
}));

View File

@ -39,7 +39,7 @@ mod test_examples_line_to {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "line_to", 0usize),
kcl_source: "This is another code block.\nyes sirrr.\nlineTo".to_string(),
}));
@ -93,7 +93,7 @@ mod test_examples_line_to {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "line_to", 1usize),
kcl_source: "This is code.\nIt does other shit.\nlineTo".to_string(),
}));

View File

@ -38,7 +38,7 @@ mod test_examples_min {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "min", 0usize),
kcl_source: "This is another code block.\nyes sirrr.\nmin".to_string(),
}));
@ -92,7 +92,7 @@ mod test_examples_min {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "min", 1usize),
kcl_source: "This is code.\nIt does other shit.\nmin".to_string(),
}));

View File

@ -38,7 +38,7 @@ mod test_examples_show {
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e,
error: e.error,
filename: format!("{}{}", "show", 0usize),
kcl_source: "This is code.\nIt does other shit.\nshow".to_string(),
}));

Some files were not shown because too many files have changed in this diff Show More