Compare commits

...

25 Commits

Author SHA1 Message Date
4107ab9b94 fix test 2024-09-05 14:29:45 +10:00
b473e23251 Merge remote-tracking branch 'origin' into kurt-modelstateindicator 2024-09-05 14:02:17 +10:00
81498fcafe fix test id 2024-09-05 14:02:06 +10:00
38d5fb8c41 more simple 2024-09-05 13:25:43 +10:00
a762d741a5 Generate download endpoint with linux links (#3778) 2024-09-04 20:03:55 -07:00
e62a5ccc47 ignore camera drags for model state indicator 2024-09-05 12:29:08 +10:00
4b8ca7f61f Double click multiple windows fixes (#3777)
* better way of doing on file open

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

* fixes

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

* updates

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

* cleanuer

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-05 02:18:33 +00:00
31b0a8af12 Cut release v0.25.0 (#3750)
* Cut release v0.25.0

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

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

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-09-04 21:33:22 -04:00
74b4cb9e08 quit macos all close (#3775)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 15:47:04 -07:00
e7c6dd3698 fix macos double click (#3774)
* fixmacos

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>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 15:32:34 -07:00
aa9abbe83f Loft (#3681)
* add loft

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

* add offsetPlane as well

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

* fix offset

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

* change to 2

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>

* fixes

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

* fixes

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

* fixes

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

* add-docs

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>
2024-09-04 14:34:33 -07:00
b19f3bbdb0 chore: added new command log for export done (#3770) 2024-09-04 14:03:21 -07:00
892e856471 fix export logic (#3769)
* fix export logic

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

* updates

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 13:12:16 -07:00
84fae12cdd Bump react-hotkeys-hook from 4.5.0 to 4.5.1 (#3759)
Bumps [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases)
- [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: react-hotkeys-hook
  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>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-09-04 12:37:51 -07:00
3d67781039 File associations (#3765)
* do windows

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

* info.plist conversion

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 12:03:44 -07:00
114c3a2580 Bump eslint-plugin-import from 2.29.1 to 2.30.0 (#3761)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.29.1 to 2.30.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...v2.30.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  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>
2024-09-04 11:29:52 -07:00
02b4aa0476 Bump google-github-actions/upload-cloud-storage from 2.1.1 to 2.2.0 (#3762)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.1...v2.2.0)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  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>
2024-09-04 11:29:21 -07:00
57f4e1b79c internal: Add ArtifactId type (#3752)
* Add ArtifactId type

* Use ArtifactId type in more places

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-09-04 11:49:13 -04:00
35f9b82a65 Set a default initial directory, handle an existing settings file with no project directory (#3734)
* Fix the project directory setting assignment from file

* Fix default project directory value initialization

* Add a couple tests for loading the app without project directory settings

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

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

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

* trigger CI

* Object merging logic was bad, blew away other app settings if they existed

* Update silly little export file size expectation numbers

* Make rename timeout in test way shorter

* Fix silly little test issues

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-09-04 11:30:09 -04:00
cbddb3553d Remove building scene, add code execution loading indicator to bottom-right controls (#3691)
* Remove isFirstRender because isExecuting means the same and remove building scene

* add small makefile util

* Remove waiting for building scene prompt

* Add a new model state indicator

* fmt lint tsc

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-09-04 08:35:40 -04:00
dd754c78ab Remove license from nsis (one less click) (#3763) 2024-09-04 07:02:49 -04:00
150f56b47a Bump electron-updater from 6.2.1 to 6.3.0 (#3748)
Bumps [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater) from 6.2.1 to 6.3.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.3.0/packages/electron-updater)

---
updated-dependencies:
- dependency-name: electron-updater
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-09-04 06:23:25 -04:00
0eef6ab7d3 Some colors and export file size changes occurred engine-side, update tests to match (#3753)
* Some colors and export file size changes occurred engine-side, update tests to match

* Fix up focusrite example after adjacent edge switch

* Yay now the export file is yet another new size

* Update bracket example code and some test colors that broke

* Use a shorter KCL sample so we don't experience weird locator behavior

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

* bump electron playwright timeout

* Relax color expectations a little bit, as they can be different on web and electron

* fix double export test

* unused var

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

* Re-run CI

* Bump getGreatestPixDiff comparisons from 10 to 15 in projects.spec.ts

* bump another pix diff to 15

* make retries work

* update expect numbers

* remove some logs

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-09-04 16:55:15 +10:00
91d3ba3fce bump all the rust things (#3755)
bump all the rust things;

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-03 16:26:35 -07:00
7165aa1b41 Bump google-github-actions/upload-cloud-storage from 2.1.3 to 2.2.0 (#3673)
* Bump google-github-actions/upload-cloud-storage from 2.1.3 to 2.2.0

Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.3 to 2.2.0.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.3...v2.2.0)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-03 16:17:22 -07:00
64 changed files with 6379 additions and 329 deletions

View File

@ -224,6 +224,8 @@ jobs:
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.msi" \
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.msi" \
--arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \
--arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \
'{
"version": $version,
"pub_date": $pub_date,
@ -240,6 +242,12 @@ jobs:
},
"msi-x64": {
"url": $windows_x64_url
},
"appimage-arm64": {
"url": $linux_arm64_url
},
"appimage-x64": {
"url": $linux_x64_url
}
}
}' > last_download.json
@ -297,7 +305,7 @@ jobs:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'Zoo*'
@ -305,7 +313,7 @@ jobs:
destination: ${{ env.BUCKET_DIR }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'latest*'
@ -313,13 +321,13 @@ jobs:
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: "out/tauri/${{ env.VERSION }}"
glob: '*/Zoo*'
@ -327,7 +335,7 @@ jobs:
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}

View File

@ -262,7 +262,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-14]
timeout-minutes: 30
timeout-minutes: 40
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
@ -381,7 +381,7 @@ jobs:
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
if [[ "$IS_UBUNTU" == "true" ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi

View File

@ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
yarn start
# I'm sorry this is so specific to my setup you may as well ignore this.
# This is so you don't have to deal with electron windows popping up constantly.
# It should work for you other Linux users.
lee-electron-test:
Xephyr -br -ac -noreset -screen 1200x500 :2 &
DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree"
killall Xephyr
$(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)'

View File

@ -56,6 +56,7 @@ layout: manual
* [`line`](kcl/line)
* [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log)
* [`log10`](kcl/log10)
* [`log2`](kcl/log2)
@ -63,6 +64,7 @@ layout: manual
* [`max`](kcl/max)
* [`min`](kcl/min)
* [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d)

477
docs/kcl/loft.md Normal file

File diff suppressed because one or more lines are too long

138
docs/kcl/offsetPlane.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,19 @@ test.describe('Code pane and errors', () => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, bracket)
await page.addInitScript(() => {
localStorage.setItem(
'persistCode',
`// Extruded Triangle
const sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([10, 0], %)
|> line([-5, 10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
@ -261,10 +271,7 @@ test(
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await u.waitForPageLoad()
})
// If they're open by default, we're not actually testing anything.
@ -292,16 +299,7 @@ test(
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
})
await test.step('All panes opened before should be visible', async () => {

View File

@ -43,12 +43,6 @@ test(
// open the project
await page.getByText(`bracket`).click()
// wait for the project to load
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -56,6 +50,17 @@ test(
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// Wait for the model to finish loading
const modelStateIndicator = page.getByTestId(
'model-state-indicator-execution-done'
)
const modelStateIndicatorLoading = page.getByTestId(
'model-state-indicator-loading'
)
await expect(modelStateIndicatorLoading).toBeVisible()
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
@ -104,7 +109,7 @@ test(
},
{ timeout: 15_000 }
)
.toBe(477327)
.toBe(477481)
// clean up output.gltf
await fsp.rm('output.gltf')

View File

@ -147,9 +147,6 @@ test.describe('Can export from electron app', () => {
const u = await getUtils(page)
page.on('console', console.log)
await electronApp.context().addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
})
const pointOnModel = { x: 630, y: 280 }
@ -173,10 +170,10 @@ test.describe('Can export from electron app', () => {
// 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, [75, 75, 75]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
})
const exportLocations: Array<Paths> = []
@ -207,7 +204,7 @@ test.describe('Can export from electron app', () => {
},
{ timeout: 15_000 }
)
.toBe(477327)
.toBe(477481)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -495,10 +492,6 @@ test(
await file.click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
)
@ -856,10 +849,10 @@ const extrude001 = extrude(200, sketch001)`)
// 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, [132, 132, 132]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 })
@ -867,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`)
await page.mouse.click(pointOnModel.x, pointOnModel.y)
// check user can interact with model by checking it turns yellow
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
.toBeLessThan(10)
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
.toBeLessThan(15)
}).toPass({ timeout: 40_000, intervals: [1_000] })
await page.getByTestId('app-logo').click()
@ -942,24 +935,15 @@ test(
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
// 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, [75, 75, 75]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -976,24 +960,15 @@ test(
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
// 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, [132, 132, 132]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
})
await test.step('Opening the router-template project should load the stream', async () => {
@ -1744,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
})
await test.step('Rename the folder', async () => {
await page.waitForTimeout(60000)
await page.waitForTimeout(2000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()

View File

@ -358,6 +358,7 @@ const sketch001 = startSketchAt([-0, -0])
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{
code: bracket,
@ -393,20 +394,22 @@ const sketch001 = startSketchAt([-0, -0])
await test.step('The second export is blocked', async () => {
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).toBeVisible()
await page.waitForTimeout(1000)
await Promise.all([
expect(exportingToastMessage.first()).toBeVisible(),
expect(alreadyExportingToastMessage).toBeVisible(),
])
})
await test.step('The first export still succeeds', async () => {
await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
await expect(successToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
await Promise.all([
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
expect(alreadyExportingToastMessage).not.toBeVisible({
timeout: 15_000,
}),
])
})
})
@ -419,10 +422,12 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
await Promise.all([
expect(exportingToastMessage).not.toBeVisible(),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toBeVisible()
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -852,10 +852,12 @@ export async function setupElectron({
testInfo,
folderSetupFn,
cleanProjectDir = true,
appSettings,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -889,15 +891,19 @@ export async function setupElectron({
if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify({
...TEST_SETTINGS,
settings: {
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
},
},
})
const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS,
settings: {
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}

View File

@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(1000)
let noHoverColor: [number, number, number] = [82, 82, 82]
let hoverColor: [number, number, number] = [116, 116, 116]
let selectColor: [number, number, number] = [144, 148, 97]
let noHoverColor: [number, number, number] = [92, 92, 92]
let hoverColor: [number, number, number] = [127, 127, 127]
let selectColor: [number, number, number] = [155, 155, 105]
const extrudeWall = { x: 670, y: 275 }
const extrudeText = `line([170.36, -121.61], %, $seg01)`
@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
await expect
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(5)
.toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100)
await page.mouse.move(extrudeWall.x, extrudeWall.y)
@ -798,43 +798,43 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(200)
await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(6)
).toBeLessThan(15)
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200)
await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(6)
).toBeLessThan(15)
await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously)
await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(6)
).toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face
noHoverColor = [104, 104, 104]
hoverColor = [134, 134, 134]
selectColor = [158, 162, 110]
noHoverColor = [115, 115, 115]
hoverColor = [145, 145, 145]
selectColor = [168, 168, 120]
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15)
await page.mouse.move(cap.x, cap.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText)
)
await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
await page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
})
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
page,

View File

@ -303,6 +303,61 @@ test.describe('Testing settings', () => {
}
)
test(
`Load desktop app with no settings file`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
// This is what makes no settings file get created
cleanProjectDir: false,
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
// If the app loads without exploding we're in the clear
await expect(errorHeading).not.toBeVisible()
await expect(projectDirLink).toBeVisible()
await electronApp.close()
}
)
test(
`Load desktop app with a settings file, but no project directory setting`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: {
app: {
themeColor: '259',
},
},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
// If the app loads without exploding we're in the clear
await expect(errorHeading).not.toBeVisible()
await expect(projectDirLink).toBeVisible()
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },

View File

@ -21,6 +21,13 @@ mac:
- arm64
notarize:
teamId: 92H8YB3B95
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
rank: Owner
win:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
@ -38,6 +45,12 @@ win:
sign: "./sign-win.js"
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
icon: "assets/icon.ico"
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
msi:
oneClick: false
@ -47,7 +60,6 @@ nsis:
oneClick: false
perMachine: true
allowElevation: true
license: "LICENSE"
installerIcon: "assets/icon.ico"
include: "./installer.nsh"
@ -58,6 +70,12 @@ linux:
arch:
- x64
- arm64
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
publish:
- provider: generic

1
interface.d.ts vendored
View File

@ -31,7 +31,6 @@ export interface IElectronAPI {
sep: typeof path.sep
rename: (prev: string, next: string) => typeof fs.rename
setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: {
name: string
}

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.24.12",
"version": "0.25.0",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -39,7 +39,7 @@
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.2.1",
"electron-updater": "^6.3.0",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0",
@ -51,7 +51,7 @@
"react": "^18.3.1",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.0",
"react-hotkeys-hook": "^4.5.1",
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
@ -169,7 +169,7 @@
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.25.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",

View File

@ -69,19 +69,6 @@ const router = createRouter([
path: PATHS.INDEX,
loader: async () => {
const onDesktop = isDesktop()
if (onDesktop) {
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}
}
return onDesktop
? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)

View File

@ -2,7 +2,7 @@ import { CommandLog } from 'lang/std/engineConnection'
import { engineCommandManager } from 'lib/singletons'
import { useState, useEffect } from 'react'
function useEngineCommands(): [CommandLog[], () => void] {
export function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
engineCommandManager.commandLogs
)

View File

@ -179,10 +179,7 @@ const FileTreeItem = ({
codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
kclManager.executeCode(true)
} else {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)

View File

@ -11,6 +11,8 @@ import {
import { engineCommandManager } from '../lib/singletons'
import { Spinner } from './Spinner'
const Loading = ({ children }: React.PropsWithChildren) => {
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
@ -65,17 +67,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
className="body-bg flex flex-col items-center justify-center h-screen"
data-testid="loading"
>
<svg viewBox="0 0 10 10" className="w-8 h-8">
<circle
cx="5"
cy="5"
r="4"
stroke="var(--primary)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
<Spinner />
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
<p
className={

View File

@ -11,6 +11,7 @@ import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator'
export function LowerRightControls({
children,
@ -65,6 +66,7 @@ export function LowerRightControls({
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`

View File

@ -0,0 +1,27 @@
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
import { useKclContext } from 'lang/KclProvider'
export const ModelStateIndicator = () => {
const { isExecuting } = useKclContext()
if (isExecuting)
return (
<div className="w-6 h-6" data-testid="model-state-indicator-loading">
<Spinner className="w-6 h-6" />
</div>
)
return (
<div
className="border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed"
data-testid="model-state-indicator"
>
<CustomIcon
data-testid="model-state-indicator-execution-done"
name="checkmark"
className="w-6 h-6"
/>
</div>
)
}

View File

@ -66,7 +66,6 @@ import {
hasExtrudableGeometry,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
@ -161,9 +160,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause()
kclManager.isFirstRender = true
kclManager.executeCode().then(() => {
kclManager.isFirstRender = false
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
@ -363,7 +360,7 @@ export const ModelingMachineProvider = ({
return {}
}),
Make: async (_, event) => {
if (event.type !== 'Make' || TEST) return
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
@ -407,7 +404,7 @@ export const ModelingMachineProvider = ({
)
},
'Engine export': async (_, event) => {
if (event.type !== 'Export' || TEST) return
if (event.type !== 'Export') return
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
return

View File

@ -193,10 +193,7 @@ export const SettingsAuthProviderBase = ({
resetSettingsIncludesUnitChange
) {
// Unit changes requires a re-exec of code
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
kclManager.executeCode(true)
} else {
// For any future logging we'd like to do
// console.log(

View File

@ -0,0 +1,17 @@
import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return (
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
<circle
cx="5"
cy="5"
r="4"
stroke="var(--primary)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
)
}

View File

@ -54,12 +54,10 @@ export const Stream = () => {
* central place, we can move this code there.
*/
async function executeCodeAndPlayStream() {
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
kclManager.isFirstRender = false
setStreamState(StreamState.Playing)
})
}
@ -219,7 +217,7 @@ export const Stream = () => {
* Play the vid
*/
useEffect(() => {
if (!kclManager.isFirstRender) {
if (!kclManager.isExecuting) {
setTimeout(() =>
// execute in the next event loop
videoRef.current?.play().catch((e) => {
@ -227,7 +225,7 @@ export const Stream = () => {
})
)
}
}, [kclManager.isFirstRender])
}, [kclManager.isExecuting])
useEffect(() => {
if (
@ -382,15 +380,15 @@ export const Stream = () => {
</div>
</div>
)}
{(!isNetworkOkay || isLoading || kclManager.isFirstRender) && (
{(!isNetworkOkay || isLoading) && (
<div className="text-center absolute inset-0">
<Loading>
{!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? (
{!isNetworkOkay && !isLoading ? (
<span data-testid="loading-stream">Stream disconnected...</span>
) : !isLoading && kclManager.isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span>
) : (
<span data-testid="loading-stream">Loading stream...</span>
!isLoading && (
<span data-testid="loading-stream">Loading stream...</span>
)
)}
</Loading>
</div>

View File

@ -60,8 +60,6 @@ export class KclManager {
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {}
isFirstRender = true
get ast() {
return this._ast
}

View File

@ -3,6 +3,8 @@ import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
export type ArtifactId = string
interface CommonCommandProperties {
range: SourceRange
pathToNode: PathToNode
@ -10,7 +12,7 @@ interface CommonCommandProperties {
export interface PlaneArtifact {
type: 'plane'
pathIds: Array<string>
pathIds: Array<ArtifactId>
codeRef: CommonCommandProperties
}
export interface PlaneArtifactRich {
@ -21,16 +23,16 @@ export interface PlaneArtifactRich {
export interface PathArtifact {
type: 'path'
planeId: string
segIds: Array<string>
extrusionId: string
solid2dId?: string
planeId: ArtifactId
segIds: Array<ArtifactId>
extrusionId: ArtifactId
solid2dId?: ArtifactId
codeRef: CommonCommandProperties
}
interface solid2D {
type: 'solid2D'
pathId: string
pathId: ArtifactId
}
export interface PathArtifactRich {
type: 'path'
@ -42,10 +44,10 @@ export interface PathArtifactRich {
interface SegmentArtifact {
type: 'segment'
pathId: string
surfaceId: string
edgeIds: Array<string>
edgeCutId?: string
pathId: ArtifactId
surfaceId: ArtifactId
edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId
codeRef: CommonCommandProperties
}
interface SegmentArtifactRich {
@ -59,9 +61,9 @@ interface SegmentArtifactRich {
interface ExtrusionArtifact {
type: 'extrusion'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
pathId: ArtifactId
surfaceIds: Array<ArtifactId>
edgeIds: Array<ArtifactId>
codeRef: CommonCommandProperties
}
interface ExtrusionArtifactRich {
@ -74,23 +76,23 @@ interface ExtrusionArtifactRich {
interface WallArtifact {
type: 'wall'
segId: string
edgeCutEdgeIds: Array<string>
extrusionId: string
pathIds: Array<string>
segId: ArtifactId
edgeCutEdgeIds: Array<ArtifactId>
extrusionId: ArtifactId
pathIds: Array<ArtifactId>
}
interface CapArtifact {
type: 'cap'
subType: 'start' | 'end'
edgeCutEdgeIds: Array<string>
extrusionId: string
pathIds: Array<string>
edgeCutEdgeIds: Array<ArtifactId>
extrusionId: ArtifactId
pathIds: Array<ArtifactId>
}
interface ExtrudeEdge {
type: 'extrudeEdge'
segId: string
extrusionId: string
segId: ArtifactId
extrusionId: ArtifactId
subType: 'opposite' | 'adjacent'
}
@ -98,16 +100,16 @@ interface ExtrudeEdge {
interface EdgeCut {
type: 'edgeCut'
subType: 'fillet' | 'chamfer'
consumedEdgeId: string
edgeIds: Array<string>
surfaceId: string
consumedEdgeId: ArtifactId
edgeIds: Array<ArtifactId>
surfaceId: ArtifactId
codeRef: CommonCommandProperties
}
interface EdgeCutEdge {
type: 'edgeCutEdge'
edgeCutId: string
surfaceId: string
edgeCutId: ArtifactId
surfaceId: ArtifactId
}
export type Artifact =
@ -122,7 +124,7 @@ export type Artifact =
| EdgeCutEdge
| solid2D
export type ArtifactGraph = Map<string, Artifact>
export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type']
@ -149,7 +151,7 @@ export function createArtifactGraph({
responseMap: ResponseMap
ast: Program
}) {
const myMap = new Map<string, Artifact>()
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
@ -166,7 +168,7 @@ export function createArtifactGraph({
const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand,
responseMap,
getArtifact: (id: string) => myMap.get(id),
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
})
@ -224,11 +226,11 @@ export function getArtifactsToUpdate({
orderedCommand: OrderedCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: string) => Artifact | undefined
currentPlaneId: string
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Program
}): Array<{
id: string
id: ArtifactId
artifact: Artifact
}> {
const pathToNode = getNodePathFromSourceRange(ast, range)
@ -514,7 +516,7 @@ export function filterArtifacts<T extends Artifact['type'][]>(
(!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>))
)
) as Map<string, Extract<Artifact, { type: T[number] }>>
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
}
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
@ -528,7 +530,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
},
map: ArtifactGraph
): Map<string, Extract<Artifact, { type: T[number] }>> {
): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
return new Map(
[...map].filter(
([key, value]) =>
@ -537,7 +539,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
(!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>))
)
) as Map<string, Extract<Artifact, { type: T[number] }>>
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
}
export function getArtifactOfTypes<T extends Artifact['type'][]>(
@ -545,7 +547,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
key,
types,
}: {
key: string
key: ArtifactId
types: T
},
map: ArtifactGraph
@ -718,7 +720,7 @@ export function getExtrudeEdgeCodeRef(
}
export function getExtrusionFromSuspectedExtrudeSurface(
id: string,
id: ArtifactId,
artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error {
const artifact = getArtifactOfTypes(
@ -733,7 +735,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
}
export function getExtrusionFromSuspectedPath(
id: string,
id: ArtifactId,
artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)

View File

@ -1252,6 +1252,10 @@ export type CommandLog =
type: 'execution-done'
data: null
}
| {
type: 'export-done'
data: null
}
export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run
@ -1918,7 +1922,13 @@ export class EngineCommandManager extends EventTarget {
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
this.pendingExport = {
resolve,
resolve: (passThrough) => {
this.addCommandLog({
type: 'export-done',
data: null,
})
resolve(passThrough)
},
reject: (reason: string) => {
this.exportIntent = null
reject(reason)

View File

@ -95,8 +95,6 @@ export const wasmUrl = () => {
document.location.pathname.split('/').slice(0, -1).join('/') +
'/wasm_lib_bg.wasm'
console.log(`Full URL for WASM: ${fullUrl}`)
return fullUrl
}

View File

@ -462,29 +462,60 @@ export const readProjectSettingsFile = async (
*/
export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath()
const initialProjectDirConfig: DeepPartial<
Configuration['settings']['project']
> = { directory: await getInitialDefaultDir() }
// The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
const parsedAppConfig = parseAppSettings(configToml)
if (err(parsedAppConfig)) {
return Promise.reject(parsedAppConfig)
}
return configObj
const hasProjectDirectorySetting =
parsedAppConfig.settings?.project?.directory ||
parsedAppConfig.settings?.app?.project_directory
if (hasProjectDirectorySetting) {
return parsedAppConfig
} else {
// inject the default project directory setting
const mergedConfig: DeepPartial<Configuration> = {
...parsedAppConfig,
settings: {
...parsedAppConfig.settings,
project: Object.assign(
{},
parsedAppConfig.settings?.project,
initialProjectDirConfig
),
},
}
return mergedConfig
}
}
// The file doesn't exist, create a new one.
// This defaultAppConfig is truly an empty object every time.
const defaultAppConfig = defaultAppSettings()
if (err(defaultAppConfig)) {
return Promise.reject(defaultAppConfig)
}
const initialDirConfig: DeepPartial<Configuration> = {
settings: { project: { directory: await getInitialDefaultDir() } },
// inject the default project directory setting
const mergedDefaultConfig: DeepPartial<Configuration> = {
...defaultAppConfig,
settings: {
...defaultAppConfig.settings,
project: Object.assign(
{},
defaultAppConfig.settings?.project,
initialProjectDirConfig
),
},
}
const config = Object.assign(defaultAppConfig, initialDirConfig)
return config
return mergedDefaultConfig
}
export const writeAppSettingsFile = async (tomlStr: string) => {

View File

@ -31,11 +31,11 @@ const bracket = startSketchOn('XY')
|> extrude(width, %)
|> fillet({
radius: filletR,
tags: [getPreviousAdjacentEdge(innerEdge)]
tags: [getNextAdjacentEdge(innerEdge)]
}, %)
|> fillet({
radius: filletR + thickness,
tags: [getPreviousAdjacentEdge(outerEdge)]
tags: [getNextAdjacentEdge(outerEdge)]
}, %)`
/**

View File

@ -14,7 +14,7 @@ const save_ = async (file: ModelingAppFile) => {
extensions.push(extension)
}
if (!(window as any).playwrightSkipFilePicker) {
if (window.electron.process.env.IS_PLAYWRIGHT) {
// skip file picker, save to default location
await window.electron.writeFile(
file.name,

View File

@ -81,7 +81,6 @@ export class MachineManager {
}
this._machines = await window.electron.listMachines()
console.log('Machines:', this._machines)
}
private async updateMachineApiIp(): Promise<void> {

View File

@ -14,6 +14,7 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
import { appThemeToTheme } from 'lib/theme'
import {
getInitialDefaultDir,
readAppSettingsFile,
readProjectSettingsFile,
writeAppSettingsFile,
@ -176,6 +177,11 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings()
// Because getting the default directory is async, we need to set it after
if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir()
}
setSettingsAtLevel(
settings,
'user',

View File

@ -16,7 +16,6 @@ window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager)
kclManager.isFirstRender = true
engineCommandManager.kclManager = kclManager
engineCommandManager.getAstCb = () => kclManager.ast

View File

@ -60,7 +60,7 @@ if (process.defaultApp) {
// Must be done before ready event.
registerStartupListeners()
const createWindow = (): BrowserWindow => {
const createWindow = (filePath?: string): BrowserWindow => {
const newWindow = new BrowserWindow({
autoHideMenuBar: true,
show: false,
@ -81,9 +81,26 @@ const createWindow = (): BrowserWindow => {
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else {
newWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
)
getProjectPathAtStartup(filePath).then((projectPath) => {
const startIndex = path.join(
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
if (projectPath === null) {
newWindow.loadFile(startIndex)
return
}
console.log('Loading file', projectPath)
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl)
newWindow.loadFile(startIndex, {
hash: fullUrl,
})
})
}
// Open the DevTools.
@ -94,13 +111,11 @@ const createWindow = (): BrowserWindow => {
return newWindow
}
// Quit when all windows are closed, except on macOS. There, it's common
// Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
// explicitly with Cmd + Q, but it is a really weird behavior with our app.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
app.quit()
})
// This method will be called when Electron has finished
@ -235,7 +250,9 @@ app.on('ready', async () => {
})
})
ipcMain.handle('loadProjectAtStartup', async () => {
const getProjectPathAtStartup = async (
filePath?: string
): Promise<string | null> => {
// If we are in development mode, we don't want to load a project at
// startup.
// Since the args passed are always '.'
@ -243,52 +260,54 @@ ipcMain.handle('loadProjectAtStartup', async () => {
return null
}
let projectPath: string | null = null
// macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
projectPath = macOpenFiles[0] // We only do one project at a time
}
// Reset this so we don't accidentally use it again.
const macOpenFilesEmpty: string[] = []
// @ts-ignore
global['macOpenFiles'] = macOpenFilesEmpty
let projectPath: string | null = filePath || null
if (projectPath === null) {
// macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
projectPath = macOpenFiles[0] // We only do one project at a time
}
// Reset this so we don't accidentally use it again.
const macOpenFilesEmpty: string[] = []
// @ts-ignore
global['macOpenFiles'] = macOpenFilesEmpty
// macOS: open-url events that were received before the app is ready
const getOpenUrls: string[] = (global as any).getOpenUrls
if (getOpenUrls && getOpenUrls.length > 0) {
projectPath = getOpenUrls[0] // We only do one project at a
}
// Reset this so we don't accidentally use it again.
// @ts-ignore
global['getOpenUrls'] = []
// macOS: open-url events that were received before the app is ready
const getOpenUrls: string[] = (global as any).getOpenUrls
if (getOpenUrls && getOpenUrls.length > 0) {
projectPath = getOpenUrls[0] // We only do one project at a
}
// Reset this so we don't accidentally use it again.
// @ts-ignore
global['getOpenUrls'] = []
// Check if we have a project path in the command line arguments
// If we do, we will load the project at that path
if (args._.length > 1) {
if (args._[1].length > 0) {
projectPath = args._[1]
// Reset all this value so we don't accidentally use it again.
args._[1] = ''
// Check if we have a project path in the command line arguments
// If we do, we will load the project at that path
if (args._.length > 1) {
if (args._[1].length > 0) {
projectPath = args._[1]
// Reset all this value so we don't accidentally use it again.
args._[1] = ''
}
}
}
if (projectPath) {
// We have a project path, load the project information.
console.log(`Loading project at startup: ${projectPath}`)
try {
const currentFile = await getCurrentProjectFile(projectPath)
console.log(`Project loaded: ${currentFile}`)
return currentFile
} catch (e) {
console.error(e)
const currentFile = await getCurrentProjectFile(projectPath)
if (currentFile instanceof Error) {
console.error(currentFile)
return null
}
return null
console.log(`Project loaded: ${currentFile}`)
return currentFile
}
return null
})
}
function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {})
@ -305,10 +324,11 @@ function registerStartupListeners() {
app.on('open-file', function (event, path) {
event.preventDefault()
macOpenFiles.push(path)
// If we have a mainWindow, lets open another window.
if (mainWindow) {
createWindow()
createWindow(path)
} else {
macOpenFiles.push(path)
}
})
@ -324,10 +344,11 @@ function registerStartupListeners() {
) {
event.preventDefault()
openUrls.push(url)
// If we have a mainWindow, lets open another window.
if (mainWindow) {
createWindow()
createWindow(url)
} else {
openUrls.push(url)
}
}

View File

@ -60,9 +60,6 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api')
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')
contextBridge.exposeInMainWorld('electron', {
login,
// Passing fs directly is not recommended since it gives a lot of power
@ -96,7 +93,6 @@ contextBridge.exposeInMainWorld('electron', {
isWindows,
isLinux,
},
loadProjectAtStartup,
// IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is
// no way to set it across the bridge boundary. We need to make it a command.
setBaseUrl: (value: string) => (process.env.BASE_URL = value),

View File

@ -107,10 +107,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
kclManager.isFirstRender = true
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}}
nextText="Overwrite code and continue"

View File

@ -13,10 +13,7 @@ export default function Sketching() {
async function clearEditor() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor('')
kclManager.isFirstRender = true
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await kclManager.executeCode(true)
}
clearEditor()

View File

@ -82,10 +82,7 @@ export function useDemoCode() {
if (!editorManager.editorView || codeManager.code === bracket) return
setTimeout(async () => {
codeManager.updateCodeStateEditor(bracket)
kclManager.isFirstRender = true
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await kclManager.executeCode(true)
await codeManager.writeToFile()
})
}, [editorManager.editorView])

View File

@ -672,7 +672,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.25"
version = "0.1.26"
dependencies = [
"Inflector",
"anyhow",
@ -1345,7 +1345,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.11"
version = "0.2.13"
dependencies = [
"anyhow",
"approx",
@ -1417,7 +1417,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.9"
version = "0.1.10"
dependencies = [
"anyhow",
"hyper",

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.25"
version = "0.1.26"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.9"
version = "0.1.10"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.11"
version = "0.2.13"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -20,7 +20,7 @@ clap = { version = "4.5.16", default-features = false, optional = true, features
convert_case = "0.6.0"
dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.24", path = "../derive-docs" }
derive-docs = { version = "0.1.26", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"

View File

@ -294,6 +294,13 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_sketch_groups_and_data<'a, T>(&'a self) -> Result<(Vec<SketchGroup>, Option<T>), KclError>
where
T: FromArgs<'a> + serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
@ -360,6 +367,13 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_float<'a, T>(&'a self) -> Result<(T, f64), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> {
FromArgs::from_args(self, 0)
}
@ -620,6 +634,8 @@ impl_from_arg_via_json!(super::revolve::RevolveData);
impl_from_arg_via_json!(super::sketch::SketchData);
impl_from_arg_via_json!(crate::std::import::ImportFormat);
impl_from_arg_via_json!(crate::std::polar::PolarCoordsData);
impl_from_arg_via_json!(crate::std::loft::LoftData);
impl_from_arg_via_json!(crate::std::planes::StandardPlane);
impl_from_arg_via_json!(SketchGroup);
impl_from_arg_via_json!(FaceTag);
impl_from_arg_via_json!(String);
@ -690,3 +706,13 @@ impl<'a> FromKclValue<'a> for SketchSurface {
}
}
}
impl<'a> FromKclValue<'a> for Vec<SketchGroup> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<SketchGroup>>().map(|x| x.0)
}
}

View File

@ -0,0 +1,141 @@
//! Standard library lofts.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, KclValue, SketchGroup},
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
};
const DEFAULT_V_DEGREE: u32 = 1;
/// Data for a loft.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LoftData {
/// Degree of the interpolation. Must be greater than zero.
/// For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction.
/// This defaults to 2, if not specified.
pub v_degree: Option<std::num::NonZeroU32>,
/// Attempt to approximate rational curves (such as arcs) using a bezier.
/// This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
/// Over time, this field won't be necessary.
#[serde(default)]
pub bez_approximate_rational: Option<bool>,
/// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
#[serde(default)]
pub base_curve_index: Option<u32>,
/// Tolerance for the loft operation.
#[serde(default)]
pub tolerance: Option<f64>,
}
impl Default for LoftData {
fn default() -> Self {
Self {
// This unwrap is safe because the default value is always greater than zero.
v_degree: Some(std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
bez_approximate_rational: None,
base_curve_index: None,
tolerance: None,
}
}
}
/// Create a 3D surface or solid by interpolating between two or more sketches.
pub async fn loft(args: Args) -> Result<KclValue, KclError> {
let (sketch_groups, data): (Vec<SketchGroup>, Option<LoftData>) = args.get_sketch_groups_and_data()?;
let extrude_group = inner_loft(sketch_groups, data, args).await?;
Ok(KclValue::ExtrudeGroup(extrude_group))
}
/// Create a 3D surface or solid by interpolating between two or more sketches.
///
/// The sketches need to closed and on the same plane.
///
/// ```no_run
/// // Loft a square and a triangle.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const triangleSketch = startSketchOn(offsetPlane('XY', 75))
/// |> startProfileAt([0, 125], %)
/// |> line([-15, -30], %)
/// |> line([30, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// loft([squareSketch, triangleSketch])
/// ```
///
/// ```no_run
/// // Loft a square, a circle, and another circle.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// |> circle([0, 100], 50, %)
///
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// |> circle([0, 100], 20, %)
///
/// loft([squareSketch, circleSketch0, circleSketch1])
/// ```
#[stdlib {
name = "loft",
}]
async fn inner_loft(
sketch_groups: Vec<SketchGroup>,
data: Option<LoftData>,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
// Make sure we have at least two sketches.
if sketch_groups.len() < 2 {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Loft requires at least two sketches, but only {} were provided.",
sketch_groups.len()
),
source_ranges: vec![args.source_range],
}));
}
// Get the loft data.
let data = data.unwrap_or_default();
let id = uuid::Uuid::new_v4();
args.batch_modeling_cmd(
id,
ModelingCmd::Loft {
section_ids: sketch_groups.iter().map(|group| group.id).collect(),
base_curve_index: data.base_curve_index,
bez_approximate_rational: data.bez_approximate_rational.unwrap_or(false),
tolerance: data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units)),
v_degree: data
.v_degree
.unwrap_or_else(|| std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap())
.into(),
},
)
.await?;
// Using the first sketch as the base curve, idk we might want to change this later.
do_post_extrude(sketch_groups[0].clone(), 0.0, id, args).await
}

View File

@ -9,8 +9,10 @@ pub mod fillet;
pub mod helix;
pub mod import;
pub mod kcl_stdlib;
pub mod loft;
pub mod math;
pub mod patterns;
pub mod planes;
pub mod polar;
pub mod revolve;
pub mod segment;
@ -98,6 +100,8 @@ lazy_static! {
Box::new(crate::std::shell::Shell),
Box::new(crate::std::shell::Hollow),
Box::new(crate::std::revolve::Revolve),
Box::new(crate::std::loft::Loft),
Box::new(crate::std::planes::OffsetPlane),
Box::new(crate::std::import::Import),
Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin),

View File

@ -0,0 +1,168 @@
//! Standard library plane helpers.
use derive_docs::stdlib;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
executor::{KclValue, Metadata, Plane, UserVal},
std::{sketch::PlaneData, Args},
};
/// One of the standard planes.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum StandardPlane {
/// The XY plane.
#[serde(rename = "XY", alias = "xy")]
XY,
/// The opposite side of the XY plane.
#[serde(rename = "-XY", alias = "-xy")]
NegXY,
/// The XZ plane.
#[serde(rename = "XZ", alias = "xz")]
XZ,
/// The opposite side of the XZ plane.
#[serde(rename = "-XZ", alias = "-xz")]
NegXZ,
/// The YZ plane.
#[serde(rename = "YZ", alias = "yz")]
YZ,
/// The opposite side of the YZ plane.
#[serde(rename = "-YZ", alias = "-yz")]
NegYZ,
}
impl From<StandardPlane> for PlaneData {
fn from(value: StandardPlane) -> Self {
match value {
StandardPlane::XY => PlaneData::XY,
StandardPlane::NegXY => PlaneData::NegXY,
StandardPlane::XZ => PlaneData::XZ,
StandardPlane::NegXZ => PlaneData::NegXZ,
StandardPlane::YZ => PlaneData::YZ,
StandardPlane::NegYZ => PlaneData::NegYZ,
}
}
}
/// Offset a plane by a distance along its normal.
pub async fn offset_plane(args: Args) -> Result<KclValue, KclError> {
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
let plane = inner_offset_plane(std_plane, offset).await?;
Ok(KclValue::UserVal(UserVal::set(
vec![Metadata {
source_range: args.source_range,
}],
plane,
)))
}
/// Offset a plane by a distance along its normal.
///
/// For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ'
/// plane and 10 units away from it.
///
/// ```no_run
/// // Loft a square and a circle on the `XY` plane using offset.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('XY', 150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `XZ` plane using offset.
/// const squareSketch = startSketchOn('XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('XZ', 150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `YZ` plane using offset.
/// const squareSketch = startSketchOn('YZ')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('YZ', 150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `-XZ` plane using offset.
/// const squareSketch = startSketchOn('-XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('-XZ', -150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
#[stdlib {
name = "offsetPlane",
}]
async fn inner_offset_plane(std_plane: StandardPlane, offset: f64) -> Result<PlaneData, KclError> {
// Convert to the plane type.
let plane_data: PlaneData = std_plane.into();
// Convert to a plane.
let mut plane = Plane::from(plane_data);
match std_plane {
StandardPlane::XY => {
plane.origin.z += offset;
}
StandardPlane::XZ => {
plane.origin.y -= offset;
}
StandardPlane::YZ => {
plane.origin.x += offset;
}
StandardPlane::NegXY => {
plane.origin.z -= offset;
}
StandardPlane::NegXZ => {
plane.origin.y += offset;
}
StandardPlane::NegYZ => {
plane.origin.x -= offset;
}
}
Ok(PlaneData::Plane {
origin: Box::new(plane.origin),
x_axis: Box::new(plane.x_axis),
y_axis: Box::new(plane.y_axis),
z_axis: Box::new(plane.z_axis),
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -55,10 +55,10 @@ const bracketBody = bs
|> fillet({
radius: radius,
tags: [
getNextAdjacentEdge(bs.tags.edge7),
getNextAdjacentEdge(bs.tags.edge2),
getNextAdjacentEdge(bs.tags.edge3),
getNextAdjacentEdge(bs.tags.edge6)
getPreviousAdjacentEdge(bs.tags.edge7),
getPreviousAdjacentEdge(bs.tags.edge2),
getPreviousAdjacentEdge(bs.tags.edge3),
getPreviousAdjacentEdge(bs.tags.edge6)
]
}, %)

View File

@ -2314,6 +2314,11 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz#20c09cf44dcb082140cc7f439dd679fe4bba3375"
integrity sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@rushstack/eslint-patch@^1.1.0":
version "1.10.4"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1"
@ -3242,7 +3247,7 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8:
array-includes@^3.1.6, array-includes@^3.1.8:
version "3.1.8"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d"
integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==
@ -3271,7 +3276,7 @@ array.prototype.findlast@^1.2.5:
es-object-atoms "^1.0.0"
es-shim-unscopables "^1.0.2"
array.prototype.findlastindex@^1.2.3:
array.prototype.findlastindex@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d"
integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==
@ -3668,6 +3673,14 @@ builder-util-runtime@9.2.4:
debug "^4.3.4"
sax "^1.2.4"
builder-util-runtime@9.2.5:
version "9.2.5"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz#0afdffa0adb5c84c14926c7dd2cf3c6e96e9be83"
integrity sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==
dependencies:
debug "^4.3.4"
sax "^1.2.4"
builder-util@24.13.1:
version "24.13.1"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816"
@ -4613,12 +4626,12 @@ electron-to-chromium@^1.5.4:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6"
integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==
electron-updater@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
electron-updater@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.0.tgz#13a5c3c3f0b2b114fe33181e24a8270096734b3e"
integrity sha512-3Xlezhk+dKaSQrOnkQNqCGiuGSSUPO9BV9TQZ4Iig6AyTJ4FzJONE5gFFc382sY53Sh9dwJfzKsA3DxRHt2btw==
dependencies:
builder-util-runtime "9.2.4"
builder-util-runtime "9.2.5"
fs-extra "^10.1.0"
js-yaml "^4.1.0"
lazy-val "^1.0.5"
@ -4935,10 +4948,10 @@ eslint-import-resolver-node@^0.3.9:
is-core-module "^2.13.0"
resolve "^1.22.4"
eslint-module-utils@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34"
integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==
eslint-module-utils@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342"
integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==
dependencies:
debug "^3.2.7"
@ -4958,26 +4971,27 @@ eslint-plugin-flowtype@^8.0.3:
lodash "^4.17.21"
string-natural-compare "^3.0.1"
eslint-plugin-import@^2.25.0, eslint-plugin-import@^2.25.3:
version "2.29.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643"
integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==
eslint-plugin-import@^2.25.3, eslint-plugin-import@^2.30.0:
version "2.30.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449"
integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==
dependencies:
array-includes "^3.1.7"
array.prototype.findlastindex "^1.2.3"
"@rtsao/scc" "^1.1.0"
array-includes "^3.1.8"
array.prototype.findlastindex "^1.2.5"
array.prototype.flat "^1.3.2"
array.prototype.flatmap "^1.3.2"
debug "^3.2.7"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.8.0"
hasown "^2.0.0"
is-core-module "^2.13.1"
eslint-module-utils "^2.9.0"
hasown "^2.0.2"
is-core-module "^2.15.1"
is-glob "^4.0.3"
minimatch "^3.1.2"
object.fromentries "^2.0.7"
object.groupby "^1.0.1"
object.values "^1.1.7"
object.fromentries "^2.0.8"
object.groupby "^1.0.3"
object.values "^1.2.0"
semver "^6.3.1"
tsconfig-paths "^3.15.0"
@ -6258,10 +6272,10 @@ is-ci@^3.0.0:
dependencies:
ci-info "^3.2.0"
is-core-module@^2.13.0, is-core-module@^2.13.1:
version "2.15.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea"
integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==
is-core-module@^2.13.0, is-core-module@^2.15.1:
version "2.15.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
dependencies:
hasown "^2.0.2"
@ -7384,7 +7398,7 @@ object.entries@^1.1.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
object.fromentries@^2.0.7, object.fromentries@^2.0.8:
object.fromentries@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65"
integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==
@ -7394,7 +7408,7 @@ object.fromentries@^2.0.7, object.fromentries@^2.0.8:
es-abstract "^1.23.2"
es-object-atoms "^1.0.0"
object.groupby@^1.0.1:
object.groupby@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e"
integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==
@ -7403,7 +7417,7 @@ object.groupby@^1.0.1:
define-properties "^1.2.1"
es-abstract "^1.23.2"
object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0:
object.values@^1.1.6, object.values@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==
@ -8045,10 +8059,10 @@ react-hot-toast@^2.4.1:
dependencies:
goober "^2.1.10"
react-hotkeys-hook@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz#807b389b15256daf6a813a1ec09e6698064fe97f"
integrity sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==
react-hotkeys-hook@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.1.tgz#990260ecc7e5a431414148a93b02a2f1a9707897"
integrity sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==
react-is@^16.13.1:
version "16.13.1"