Compare commits

..

1 Commits

Author SHA1 Message Date
49c8fc4c97 Update bracket example code and some test colors that broke 2024-09-03 18:35:09 -04:00
91 changed files with 775 additions and 7004 deletions

View File

@ -2,9 +2,7 @@ NODE_ENV=development
DEV=true DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence! VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"

View File

@ -5,7 +5,6 @@ on:
push: push:
branches: branches:
- main - main
- cut-release-v0.25.1-updater-test-build-2
release: release:
types: [published] types: [published]
schedule: schedule:
@ -14,8 +13,8 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04) # Will checkout the last commit from the default branch (main as of 2023-10-04)
env: env:
CUT_RELEASE_PR: true CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: true BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -45,7 +44,7 @@ jobs:
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib # TODO: see if we can fetch from main instead if no diff at src/wasm-lib
- name: Run build:wasm - name: Run build:wasm
run: "yarn build:wasm" run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Set nightly version - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
@ -82,6 +81,8 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true CSC_FOR_PULL_REQUEST: true
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
@ -141,12 +142,37 @@ jobs:
- name: List artifacts in out/ - name: List artifacts in out/
run: ls -R out run: ls -R out
- name: Prepare the tauri update bundles (macOS)
if: ${{ env.BUILD_RELEASE && matrix.os == 'macos-14' }}
run: |
for ARCH in arm64 x64; do
TAURI_DIR=out/tauri/$VERSION/macos
TEMP_DIR=temp/$ARCH
mkdir -p $TAURI_DIR
mkdir -p $TEMP_DIR
unzip out/*-$ARCH-mac.zip -d $TEMP_DIR
tar -czvf "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz" -C $TEMP_DIR "Zoo Modeling App.app"
yarn tauri signer sign "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz"
done
ls -R out
- name: Prepare the tauri update bundles (Windows)
if: ${{ env.BUILD_RELEASE && matrix.os == 'windows-2022' }}
run: |
$env:TAURI_DIR="out/tauri/${env:VERSION}/nsis"
mkdir -p ${env:TAURI_DIR}
$env:OUT_FILE="${env:TAURI_DIR}/Zoo Modeling App_${env:VERSION_NO_V}_x64-setup.nsis.zip"
7z a -mm=Copy "${env:OUT_FILE}" ./out/*-x64-win.exe
yarn tauri signer sign "${env:OUT_FILE}"
ls -R out
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: out-${{ matrix.os }} name: out-${{ matrix.os }}
path: | path: |
out/Zoo*.* out/Zoo*.*
out/latest*.yml out/latest*.yml
out/tauri
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
@ -157,15 +183,17 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
contents: write contents: write
# if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [prepare-files, build-apps] needs: [prepare-files, build-apps]
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }} NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app/test/cut-release-v0.25.1-updater-test' }} BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app/test/cut-release-v0.25.1-updater-test' }} WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
BUCKET_DIR_TAURI: 'dl.kittycad.io/releases/modeling-app/tauri-compat'
WEBSITE_DIR_TAURI: 'dl.zoo.dev/releases/modeling-app/tauri-compat'
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -196,8 +224,6 @@ jobs:
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \ --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_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 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, "version": $version,
"pub_date": $pub_date, "pub_date": $pub_date,
@ -214,17 +240,49 @@ jobs:
}, },
"msi-x64": { "msi-x64": {
"url": $windows_x64_url "url": $windows_x64_url
},
"appimage-arm64": {
"url": $linux_arm64_url
},
"appimage-x64": {
"url": $linux_x64_url
} }
} }
}' > last_download.json }' > last_download.json
cat last_download.json cat last_download.json
- name: Generate the update static endpoint for tauri
run: |
TAURI_DIR=out/tauri/$VERSION
MAC_ARM64_SIG=`cat $TAURI_DIR/macos/*-arm64.app.tar.gz.sig`
MAC_X64_SIG=`cat $TAURI_DIR/macos/*-x64.app.tar.gz.sig`
WINDOWS_SIG=`cat $TAURI_DIR/nsis/*.nsis.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR_TAURI}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg mac_arm64_sig "$MAC_ARM64_SIG" \
--arg mac_arm64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-arm64.app.tar.gz" \
--arg mac_x64_sig "$MAC_X64_SIG" \
--arg mac_x64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-x64.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/nsis/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64-setup.nsis.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $mac_x64_sig,
"url": $mac_x64_url
},
"darwin-aarch64": {
"signature": $mac_arm64_sig,
"url": $mac_arm64_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
}
}
}' > last_update.json
cat last_update.json
- name: List artifacts - name: List artifacts
run: "ls -R out" run: "ls -R out"
@ -239,45 +297,41 @@ jobs:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }} project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket - name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: out path: out
glob: 'Zoo*' glob: 'Zoo*'
parent: false parent: false
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
- name: Upload release files to public bucket (test/electron-builder workaround)
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'Zoo*'
parent: false
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload update endpoint to public bucket - name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: out path: out
glob: 'latest*' glob: 'latest*'
parent: false parent: false
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
- name: Upload update endpoint to public bucket (test/electron-builder workaround)
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'latest*'
parent: false
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload download endpoint to public bucket - name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: last_download.json path: last_download.json
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: "out/tauri/${{ env.VERSION }}"
glob: '*/Zoo*'
parent: false
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
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github - name: Upload release files to Github
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@ -28,7 +28,6 @@ jobs:
dir: ['src/wasm-lib'] dir: ['src/wasm-lib']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: taiki-e/install-action@just
- name: Install latest rust - name: Install latest rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -42,7 +41,7 @@ jobs:
- name: Run clippy - name: Run clippy
run: | run: |
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
just lint cargo clippy --all --tests --benches -- -D warnings
# If this fails, run "cargo check" to update Cargo.lock, # If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR. # then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating - name: Check Cargo.lock doesn't need updating

View File

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

View File

@ -7,14 +7,6 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS) dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
yarn start 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) $(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)' yarn xstate typegen 'src/**/*.ts?(x)'

View File

@ -351,6 +351,25 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
</details> </details>
### Tauri e2e tests
#### Windows (local only until the CI edge version mismatch is fixed)
```
yarn install
yarn build:wasm-dev
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
yarn vite build --mode development
yarn tauri build --debug -b
$env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
$env:E2E_TAURI_ENABLED="true"
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
Stop-Process -Name msedgedriver
yarn wdio run wdio.conf.ts
```
## KCL ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -27,19 +27,9 @@ test.describe('Code pane and errors', () => {
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript(() => { await page.addInitScript((code) => {
localStorage.setItem( localStorage.setItem('persistCode', code)
'persistCode', }, bracket)
`// 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 page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -271,7 +261,10 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await u.waitForPageLoad() await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
}) })
// If they're open by default, we're not actually testing anything. // If they're open by default, we're not actually testing anything.
@ -299,7 +292,16 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await u.waitForPageLoad() 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 test.step('All panes opened before should be visible', async () => { await test.step('All panes opened before should be visible', async () => {

View File

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

View File

@ -112,8 +112,7 @@ test.describe('when using the file tree to', () => {
}) })
const { const {
openKclCodePanel, panesOpen,
openFilePanel,
createAndSelectProject, createAndSelectProject,
pasteCodeInEditor, pasteCodeInEditor,
createNewFileAndSelect, createNewFileAndSelect,
@ -125,9 +124,9 @@ test.describe('when using the file tree to', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents // File the main.kcl with contents
const kclCube = await fsp.readFile( const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl', 'src/wasm-lib/tests/executor/inputs/cube.kcl',
@ -202,78 +201,4 @@ test.describe('when using the file tree to', () => {
await electronApp.close() await electronApp.close()
} }
) )
test(
'loading small file, then large, then back to small',
{
tag: '@electron',
},
async ({ browser: _ }, testInfo) => {
const { page } = await setupElectron({
testInfo,
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
createNewFile,
openDebugPanel,
closeDebugPanel,
expectCmdLog,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// Create a small file
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
// pasted into main.kcl
await pasteCodeInEditor(kclCube)
// Create a large lego file
await createNewFile('lego')
const legoFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'lego.kcl' }),
})
await expect(legoFile).toBeVisible({ timeout: 60_000 })
await legoFile.click()
const kclLego = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/lego.kcl',
'utf-8'
)
await pasteCodeInEditor(kclLego)
const mainFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'main.kcl' }),
})
// Open settings and enable the debug panel
await page
.getByRole('link', {
name: 'settings Settings',
})
.click()
await page.locator('#showDebugPanel').getByText('OffOn').click()
await page.getByTestId('settings-close-button').click()
await test.step('swap between small and large files', async () => {
await openDebugPanel()
// Previously created a file so we need to start back at main.kcl
await mainFile.click()
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
// Click the large file
await legoFile.click()
// Once it is building, click back to the smaller file
await mainFile.click()
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
await closeDebugPanel()
})
}
)
}) })

View File

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

View File

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

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -548,16 +548,13 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFileAndSelect: async (name: string) => { createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => { return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
const newFile = page await page
.locator('[data-testid="file-pane-scroll-container"] button') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name }) .filter({ hasText: name })
.click()
await expect(newFile).toBeVisible()
await newFile.click()
}) })
}, },
@ -588,15 +585,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
}) })
}, },
/**
* @deprecated Sorry I don't have time to fix this right now, but runs like
* the one linked below show me that setting the open panes in this manner is not reliable.
* You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup,
* or you can imperatively open the panes with functions like {openKclCodePanel}
* (or we can make a general openPane function that takes a paneId).,
* but having a separate initScript does not seem to work reliably.
* @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553
*/
panesOpen: async (paneIds: PaneId[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
@ -864,12 +852,10 @@ export async function setupElectron({
testInfo, testInfo,
folderSetupFn, folderSetupFn,
cleanProjectDir = true, cleanProjectDir = true,
appSettings,
}: { }: {
testInfo: TestInfo testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void> folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) { }) {
// create or otherwise clear the folder // create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir') const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -903,19 +889,15 @@ export async function setupElectron({
if (cleanProjectDir) { if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME) const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify( const settingsOverrides = TOML.stringify({
appSettings ...TEST_SETTINGS,
? { settings: appSettings } settings: {
: { app: {
...TEST_SETTINGS, ...TEST_SETTINGS.app,
settings: { projectDirectory: projectDirName,
app: { },
...TEST_SETTINGS.app, },
projectDirectory: projectDirName, })
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides) await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
} }

View File

@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
await expect await expect
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor)) .poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(15) .toBeLessThan(5)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.move(extrudeWall.x, extrudeWall.y) await page.mouse.move(extrudeWall.x, extrudeWall.y)
@ -798,18 +798,18 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor) await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(15) ).toBeLessThan(6)
await page.mouse.click(extrudeWall.x, extrudeWall.y) await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15) ).toBeLessThan(6)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // check color stays there, i.e. not overridden (this was a bug previously)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15) ).toBeLessThan(6)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300) await page.waitForTimeout(300)
@ -820,21 +820,21 @@ const extrude001 = extrude(50, sketch001)
hoverColor = [145, 145, 145] hoverColor = [145, 145, 145]
selectColor = [168, 168, 120] selectColor = [168, 168, 120]
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
await page.mouse.move(cap.x, cap.y) await page.mouse.move(cap.x, cap.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText( await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText) removeAfterFirstParenthesis(capText)
) )
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
await page.mouse.click(cap.x, cap.y) await page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // check color stays there, i.e. not overridden (this was a bug previously)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
}) })
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({ test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
page, page,

View File

@ -288,7 +288,7 @@ test.describe('Testing settings', () => {
}) })
await test.step('Refresh the application and see project setting applied', async () => { await test.step('Refresh the application and see project setting applied', async () => {
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload()
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click() await settingsCloseButton.click()
@ -303,109 +303,53 @@ 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( test(
`Closing settings modal should go back to the original file being viewed`, `Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async () => {},
const bracketDir = join(dir, 'project-000')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(bracketDir, '2.kcl')
)
},
}) })
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
const kclCylinder = await fsp.readFile(
executorInputPath('cylinder.kcl'),
'utf8'
)
const { const {
openKclCodePanel, panesOpen,
openFilePanel, createAndSelectProject,
waitForPageLoad, pasteCodeInEditor,
selectFile, clickPane,
createNewFileAndSelect,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await test.step('Precondition: Open to second project file', async () => { await panesOpen([])
await expect(page.getByTestId('home-section')).toBeVisible()
await page.getByText('project-000').click()
await waitForPageLoad()
await openKclCodePanel()
await openFilePanel()
await editorTextMatches(kclCube)
await selectFile('2.kcl') await test.step('Precondition: No projects exist', async () => {
await editorTextMatches(kclCylinder) await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link')
await expect(projectLinksPre).toHaveCount(0)
}) })
await createAndSelectProject('project-000')
await clickPane('code')
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await clickPane('files')
await createNewFileAndSelect('2.kcl')
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
const settingsOpenButton = page.getByRole('link', { const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
@ -413,9 +357,6 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => { await test.step('Open and close settings', async () => {
await settingsOpenButton.click() await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click() await settingsCloseButton.click()
}) })

View File

@ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
// Ensure the final toast remains. // Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible() await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible() await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible() await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model. // Ensure you can copy the code for the final model.
@ -690,53 +690,40 @@ test(
'Text-to-CAD functionality', 'Text-to-CAD functionality',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo }) const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () => const fileExists = () =>
fs.existsSync(join(dir, projectName, textToCadFileName)) fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl'))
const { const { createAndSelectProject, panesOpen } = await getUtils(page, test)
createAndSelectProject,
openFilePanel,
openKclCodePanel,
waitForPageLoad,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
// Locators await panesOpen(['code', 'files'])
const projectMenuButton = page.getByRole('button', { name: projectName })
const textToCadFileButton = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: textToCadFileName }),
})
const textToCadComment = page.getByText(
`// Generated by Text-to-CAD: ${prompt}`
)
// Create and navigate to the project // Create and navigate to the project
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command // Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await waitForPageLoad() await expect(
await openFilePanel() page.getByRole('button', { name: 'Start Sketch' })
await openKclCodePanel() ).toBeEnabled({
timeout: 20_000,
})
await test.step(`Test file creation`, async () => { await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, prompt) await sendPromptFromCommandBar(page, 'lego 2x4')
// File is considered created if it shows up in the Project Files pane // File is considered created if it shows up in the Project Files pane
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 }) const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await expect(file).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy() expect(fileExists()).toBeTruthy()
}) })
await test.step(`Test file navigation`, async () => { await test.step(`Test file navigation`, async () => {
await expect(projectMenuButton).toContainText('main.kcl') const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await textToCadFileButton.click() await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane // File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
await expect(textToCadComment).toBeVisible({ timeout: 20_000 }) await expect(kclComment).toBeVisible({ timeout: 20_000 })
await expect(projectMenuButton).toContainText(textToCadFileName)
}) })
await test.step(`Test file deletion on rejection`, async () => { await test.step(`Test file deletion on rejection`, async () => {
@ -750,8 +737,6 @@ test(
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy() expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()

View File

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

2
interface.d.ts vendored
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.25.2", "version": "0.24.12",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -39,7 +39,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.3.0", "electron-updater": "^6.2.1",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@ -51,7 +51,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.1", "react-hotkeys-hook": "^4.5.0",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
@ -137,6 +137,7 @@
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@tauri-apps/cli": "^2.0.0-rc.9",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10", "@types/d3-force": "^3.0.10",
@ -168,7 +169,7 @@
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.25.0",
"eslint-plugin-suggest-no-throw": "^1.0.0", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.10",
"http-server": "^14.1.1", "http-server": "^14.1.1",

View File

@ -122,11 +122,11 @@ export function App() {
// Override the electron window draggable region behavior as well // Override the electron window draggable region behavior as well
// when the button is down in the stream // when the button is down in the stream
style={ style={
isDesktop() && context.store?.buttonDownInStream {
? ({ '-webkit-app-region': context.store?.buttonDownInStream
'-webkit-app-region': 'no-drag', ? 'no-drag'
} as React.CSSProperties) : '',
: {} } as React.CSSProperties
} }
project={{ project, file }} project={{ project, file }}
enableMenu={true} enableMenu={true}

View File

@ -69,6 +69,19 @@ const router = createRouter([
path: PATHS.INDEX, path: PATHS.INDEX,
loader: async () => { loader: async () => {
const onDesktop = isDesktop() 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 return onDesktop
? redirect(PATHS.HOME) ? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME) : redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)

View File

@ -20,8 +20,6 @@ import {
ToolbarItemResolved, ToolbarItemResolved,
ToolbarModeName, ToolbarModeName,
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -290,11 +288,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
return ( return (
<Tooltip <Tooltip
inert={false} inert={false}
wrapperStyle={
isDesktop()
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
position="bottom" position="bottom"
wrapperClassName="!p-4 !pointer-events-auto" wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
@ -344,7 +337,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
<li key={link.label} className="contents"> <li key={link.label} className="contents">
<a <a
href={link.url} href={link.url}
onClick={openExternalBrowserIfDesktop(link.url)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit" className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"

View File

@ -6,9 +6,6 @@
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
}
.header.desktopApp {
/* Make the header act as a handle to drag the electron app window, /* Make the header act as a handle to drag the electron app window,
* per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region * per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region
* all interactive elements opt-out of this behavior by default in src/index.css * all interactive elements opt-out of this behavior by default in src/index.css

View File

@ -6,7 +6,6 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton' import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton' import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
@ -33,9 +32,7 @@ export const AppHeader = ({
className={ className={
'w-full grid ' + 'w-full grid ' +
styles.header + styles.header +
` ${ ' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
isDesktop() ? styles.desktopApp + ' ' : ''
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
className className
} }
style={style} style={style}

View File

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

View File

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

View File

@ -11,8 +11,6 @@ import {
import { engineCommandManager } from '../lib/singletons' import { engineCommandManager } from '../lib/singletons'
import { Spinner } from './Spinner'
const Loading = ({ children }: React.PropsWithChildren) => { const Loading = ({ children }: React.PropsWithChildren) => {
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset) const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
@ -67,7 +65,17 @@ const Loading = ({ children }: React.PropsWithChildren) => {
className="body-bg flex flex-col items-center justify-center h-screen" className="body-bg flex flex-col items-center justify-center h-screen"
data-testid="loading" data-testid="loading"
> >
<Spinner /> <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>
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p> <p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
<p <p
className={ className={

View File

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

View File

@ -1,45 +0,0 @@
import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => {
const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 '
let icon = <Spinner className={className} />
let dataTestId = 'model-state-indicator'
if (lastCommandType === 'receive-reliable') {
className +=
'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'
icon = (
<CustomIcon
data-testid={dataTestId + '-receive-reliable'}
name="checkmark"
/>
)
} else if (lastCommandType === 'execution-done') {
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'
icon = (
<CustomIcon
data-testid={dataTestId + '-execution-done'}
name="checkmark"
/>
)
} else if (lastCommandType === 'export-done') {
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'
icon = (
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
)
}
return (
<div className={className} data-testid="model-state-indicator">
{icon}
</div>
)
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ interface TooltipProps extends React.PropsWithChildren {
position?: TooltipPosition position?: TooltipPosition
wrapperClassName?: string wrapperClassName?: string
contentClassName?: string contentClassName?: string
wrapperStyle?: React.CSSProperties
delay?: number delay?: number
hoverOnly?: boolean hoverOnly?: boolean
inert?: boolean inert?: boolean
@ -23,7 +22,6 @@ export default function Tooltip({
position = 'top', position = 'top',
wrapperClassName: className, wrapperClassName: className,
contentClassName, contentClassName,
wrapperStyle = {},
delay = 200, delay = 200,
hoverOnly = false, hoverOnly = false,
inert = true, inert = true,
@ -38,10 +36,7 @@ export default function Tooltip({
} ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${ } ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${
styles[position] styles[position]
} ${className}`} } ${className}`}
style={Object.assign( style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
{ '--_delay': delay + 'ms' } as React.CSSProperties,
wrapperStyle
)}
> >
<div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}> <div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}>
{children} {children}

View File

@ -8,7 +8,7 @@ import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange } from 'lang/wasm' import { PathToNode, SourceRange, parse, recast } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal) export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -23,7 +23,8 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
const parsed = ast const parsed = parse(recast(ast))
if (trap(parsed)) return
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(
parsed, parsed,

View File

@ -50,14 +50,6 @@ body.dark {
@apply text-chalkboard-10; @apply text-chalkboard-10;
} }
@media (prefers-color-scheme: dark) {
body,
.body-bg,
.dark .body-bg {
@apply bg-chalkboard-100;
}
}
select { select {
@apply bg-chalkboard-20; @apply bg-chalkboard-20;
} }
@ -295,11 +287,32 @@ code {
} }
@layer utilities { @layer utilities {
/* /* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */
This is where your own custom Tailwind utility classes can go, @keyframes circle-in-hesitate {
which lets you use them with @apply in your CSS, and get 0% {
autocomplete in classNames in your JSX. clip-path: circle(
*/ var(--circle-size-start, 0%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
40% {
clip-path: circle(
var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%)
);
}
100% {
clip-path: circle(
var(--circle-size-end, 125%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
}
.in-circle-hesitate {
animation: var(--circle-duration, 2.5s)
var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate
both;
}
} }
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import {
parseProjectSettings, parseProjectSettings,
} from 'lang/wasm' } from 'lang/wasm'
import { import {
DEFAULT_HOST,
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
PROJECT_FOLDER, PROJECT_FOLDER,
PROJECT_SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME,
@ -461,60 +462,29 @@ export const readProjectSettingsFile = async (
*/ */
export const readAppSettingsFile = async () => { export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath() let settingsPath = await getAppSettingsFilePath()
const initialProjectDirConfig: DeepPartial<
Configuration['settings']['project']
> = { directory: await getInitialDefaultDir() }
// The file exists, read it and parse it. // The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) { if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath) const configToml = await window.electron.readFile(settingsPath)
const parsedAppConfig = parseAppSettings(configToml) const configObj = parseAppSettings(configToml)
if (err(parsedAppConfig)) { if (err(configObj)) {
return Promise.reject(parsedAppConfig) return Promise.reject(configObj)
} }
const hasProjectDirectorySetting = return configObj
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. // The file doesn't exist, create a new one.
// This defaultAppConfig is truly an empty object every time.
const defaultAppConfig = defaultAppSettings() const defaultAppConfig = defaultAppSettings()
if (err(defaultAppConfig)) { if (err(defaultAppConfig)) {
return Promise.reject(defaultAppConfig) return Promise.reject(defaultAppConfig)
} }
const initialDirConfig: DeepPartial<Configuration> = {
// inject the default project directory setting settings: { project: { directory: await getInitialDefaultDir() } },
const mergedDefaultConfig: DeepPartial<Configuration> = {
...defaultAppConfig,
settings: {
...defaultAppConfig.settings,
project: Object.assign(
{},
defaultAppConfig.settings?.project,
initialProjectDirConfig
),
},
} }
return mergedDefaultConfig const config = Object.assign(defaultAppConfig, initialDirConfig)
return config
} }
export const writeAppSettingsFile = async (tomlStr: string) => { export const writeAppSettingsFile = async (tomlStr: string) => {
@ -555,6 +525,28 @@ export const getUser = async (
token: string, token: string,
hostname: string hostname: string
): Promise<Models['User_type']> => { ): Promise<Models['User_type']> => {
// Use the host passed in if it's set.
// Otherwise, use the default host.
const host = !hostname ? DEFAULT_HOST : hostname
// Change the baseURL to the one we want.
let baseurl = host
if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) {
baseurl = `https://${host}`
if (host.indexOf('localhost') === 0) {
baseurl = `http://${host}`
}
}
// Use kittycad library to fetch the user info from /user/me
if (baseurl !== DEFAULT_HOST) {
// The TypeScript generated library uses environment variables for this
// because it was intended for NodeJS.
// Needs to stay like this because window.electron.kittycad needs it
// internally.
window.electron.setBaseUrl(baseurl)
}
try { try {
const user = await window.electron.kittycad('users.get_user_self', { const user = await window.electron.kittycad('users.get_user_self', {
client: { token }, client: { token },

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import {
kclManager, kclManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { CallExpression, SourceRange, Expr, parse } from 'lang/wasm' import { CallExpression, SourceRange, Expr, parse, recast } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state' import { EditorSelection, SelectionRange } from '@codemirror/state'
@ -300,7 +300,8 @@ export function processCodeMirrorRanges({
} }
function updateSceneObjectColors(codeBasedSelections: Selection[]) { function updateSceneObjectColors(codeBasedSelections: Selection[]) {
const updated = kclManager.ast const updated = parse(recast(kclManager.ast))
if (err(updated)) return
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if ( if (

View File

@ -14,7 +14,6 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { mouseControlsToCameraSystem } from 'lib/cameraControls' import { mouseControlsToCameraSystem } from 'lib/cameraControls'
import { appThemeToTheme } from 'lib/theme' import { appThemeToTheme } from 'lib/theme'
import { import {
getInitialDefaultDir,
readAppSettingsFile, readAppSettingsFile,
readProjectSettingsFile, readProjectSettingsFile,
writeAppSettingsFile, writeAppSettingsFile,
@ -177,11 +176,6 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload) if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings() 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( setSettingsAtLevel(
settings, settings,
'user', 'user',

View File

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

View File

@ -129,16 +129,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'loft', id: 'loft',
onClick: () => console.error('Loft not yet implemented'), onClick: () => console.error('Loft not yet implemented'),
icon: 'loft', icon: 'loft',
status: 'kcl-only', status: 'unavailable',
title: 'Loft', title: 'Loft',
hotkey: 'L', hotkey: 'L',
description: description:
'Create a 3D body by blending between two or more sketches.', 'Create a 3D body by blending between two or more sketches.',
links: [ links: [
{
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/loft',
},
{ {
label: 'GitHub discussion', label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/613', url: 'https://github.com/KittyCAD/modeling-app/discussions/613',

View File

@ -2,14 +2,7 @@
// template that ElectronJS provides. // template that ElectronJS provides.
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'
app,
BrowserWindow,
ipcMain,
dialog,
shell,
nativeTheme,
} from 'electron'
import path from 'path' import path from 'path'
import { Issuer } from 'openid-client' import { Issuer } from 'openid-client'
import { Bonjour, Service } from 'bonjour-service' import { Bonjour, Service } from 'bonjour-service'
@ -67,7 +60,7 @@ if (process.defaultApp) {
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() registerStartupListeners()
const createWindow = (filePath?: string): BrowserWindow => { const createWindow = (): BrowserWindow => {
const newWindow = new BrowserWindow({ const newWindow = new BrowserWindow({
autoHideMenuBar: true, autoHideMenuBar: true,
show: false, show: false,
@ -82,33 +75,15 @@ const createWindow = (filePath?: string): BrowserWindow => {
icon: path.resolve(process.cwd(), 'assets', 'icon.png'), icon: path.resolve(process.cwd(), 'assets', 'icon.png'),
frame: os.platform() !== 'darwin', frame: os.platform() !== 'darwin',
titleBarStyle: 'hiddenInset', titleBarStyle: 'hiddenInset',
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1C1C1C' : '#FCFCFC',
}) })
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else { } else {
getProjectPathAtStartup(filePath).then((projectPath) => { newWindow.loadFile(
const startIndex = path.join( path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
__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. // Open the DevTools.
@ -119,11 +94,13 @@ const createWindow = (filePath?: string): BrowserWindow => {
return newWindow return newWindow
} }
// Quit when all windows are closed, even on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q, but it is a really weird behavior with our app. // explicitly with Cmd + Q.
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
app.quit() if (process.platform !== 'darwin') {
app.quit()
}
}) })
// This method will be called when Electron has finished // This method will be called when Electron has finished
@ -258,9 +235,7 @@ app.on('ready', async () => {
}) })
}) })
const getProjectPathAtStartup = async ( ipcMain.handle('loadProjectAtStartup', async () => {
filePath?: string
): Promise<string | null> => {
// If we are in development mode, we don't want to load a project at // If we are in development mode, we don't want to load a project at
// startup. // startup.
// Since the args passed are always '.' // Since the args passed are always '.'
@ -268,54 +243,52 @@ const getProjectPathAtStartup = async (
return null return null
} }
let projectPath: string | null = filePath || null let projectPath: string | null = null
if (projectPath === null) { // macOS: open-file events that were received before the app is ready
// macOS: open-file events that were received before the app is ready const macOpenFiles: string[] = (global as any).macOpenFiles
const macOpenFiles: string[] = (global as any).macOpenFiles if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) { projectPath = macOpenFiles[0] // We only do one project at a time
projectPath = macOpenFiles[0] // We only do one project at a time }
} // Reset this so we don't accidentally use it again.
// Reset this so we don't accidentally use it again. const macOpenFilesEmpty: string[] = []
const macOpenFilesEmpty: string[] = [] // @ts-ignore
// @ts-ignore global['macOpenFiles'] = macOpenFilesEmpty
global['macOpenFiles'] = macOpenFilesEmpty
// macOS: open-url events that were received before the app is ready // macOS: open-url events that were received before the app is ready
const getOpenUrls: string[] = (global as any).getOpenUrls const getOpenUrls: string[] = (global as any).getOpenUrls
if (getOpenUrls && getOpenUrls.length > 0) { if (getOpenUrls && getOpenUrls.length > 0) {
projectPath = getOpenUrls[0] // We only do one project at a projectPath = getOpenUrls[0] // We only do one project at a
} }
// Reset this so we don't accidentally use it again. // Reset this so we don't accidentally use it again.
// @ts-ignore // @ts-ignore
global['getOpenUrls'] = [] global['getOpenUrls'] = []
// Check if we have a project path in the command line arguments // Check if we have a project path in the command line arguments
// If we do, we will load the project at that path // If we do, we will load the project at that path
if (args._.length > 1) { if (args._.length > 1) {
if (args._[1].length > 0) { if (args._[1].length > 0) {
projectPath = args._[1] projectPath = args._[1]
// Reset all this value so we don't accidentally use it again. // Reset all this value so we don't accidentally use it again.
args._[1] = '' args._[1] = ''
}
} }
} }
if (projectPath) { if (projectPath) {
// We have a project path, load the project information. // We have a project path, load the project information.
console.log(`Loading project at startup: ${projectPath}`) console.log(`Loading project at startup: ${projectPath}`)
const currentFile = await getCurrentProjectFile(projectPath) try {
const currentFile = await getCurrentProjectFile(projectPath)
if (currentFile instanceof Error) { console.log(`Project loaded: ${currentFile}`)
console.error(currentFile) return currentFile
return null } catch (e) {
console.error(e)
} }
console.log(`Project loaded: ${currentFile}`) return null
return currentFile
} }
return null return null
} })
function parseCLIArgs(): minimist.ParsedArgs { function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {}) return minimist(process.argv, {})
@ -332,11 +305,10 @@ function registerStartupListeners() {
app.on('open-file', function (event, path) { app.on('open-file', function (event, path) {
event.preventDefault() event.preventDefault()
macOpenFiles.push(path)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow(path) createWindow()
} else {
macOpenFiles.push(path)
} }
}) })
@ -352,11 +324,10 @@ function registerStartupListeners() {
) { ) {
event.preventDefault() event.preventDefault()
openUrls.push(url)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow(url) createWindow()
} else {
openUrls.push(url)
} }
} }

View File

@ -60,6 +60,9 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> => const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api') ipcRenderer.invoke('find_machine_api')
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
login, login,
// Passing fs directly is not recommended since it gives a lot of power // Passing fs directly is not recommended since it gives a lot of power
@ -93,6 +96,10 @@ contextBridge.exposeInMainWorld('electron', {
isWindows, isWindows,
isLinux, 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),
process: { process: {
// Setter/getter has to be created because // Setter/getter has to be created because
// these are read-only over the boundary. // these are read-only over the boundary.

View File

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

View File

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

View File

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

View File

@ -58,23 +58,19 @@ const SignIn = () => {
} }
return ( return (
<main <main className="bg-primary h-screen grid place-items-stretch m-0 p-2">
className="bg-primary h-screen grid place-items-stretch m-0 p-2"
style={
isDesktop()
? ({
'-webkit-app-region': 'drag',
} as CSSProperties)
: {}
}
>
<div <div
style={ style={
isDesktop() {
? ({ '-webkit-app-region': 'no-drag' } as CSSProperties) height: 'calc(100vh - 16px)',
: {} '--circle-x': '14%',
'--circle-y': '12%',
'--circle-size-mid': '15%',
'--circle-size-end': '200%',
'--circle-timing': 'cubic-bezier(0.25, 1, 0.4, 0.9)',
} as CSSProperties
} }
className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto" className="in-circle-hesitate body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
> >
<div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5"> <div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5">
<div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8"> <div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8">
@ -198,7 +194,7 @@ const SignIn = () => {
<div className="flex gap-4 flex-wrap items-center"> <div className="flex gap-4 flex-wrap items-center">
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/docs/kcl-samples/a-parametric-bearing-pillow-block" to="https://zoo.dev/docs/kcl-samples/ball-bearing"
iconStart={{ icon: 'settings' }} iconStart={{ icon: 'settings' }}
className="border-chalkboard-30 dark:border-chalkboard-80" className="border-chalkboard-30 dark:border-chalkboard-80"
> >

View File

@ -370,9 +370,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.17" version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -380,9 +380,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.17" version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@ -620,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.1.0" version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@ -672,7 +672,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.26" version = "0.1.25"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -1345,7 +1345,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.14" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -1357,7 +1357,7 @@ dependencies = [
"clap", "clap",
"convert_case", "convert_case",
"criterion", "criterion",
"dashmap 6.1.0", "dashmap 6.0.1",
"databake", "databake",
"derive-docs", "derive-docs",
"expectorate", "expectorate",
@ -1399,7 +1399,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winnow", "winnow 0.5.40",
"zip", "zip",
] ]
@ -1417,7 +1417,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.10" version = "0.1.9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper", "hyper",
@ -2581,9 +2581,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.128" version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.2.5",
"itoa", "itoa",
@ -3117,7 +3117,7 @@ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow", "winnow 0.6.18",
] ]
[[package]] [[package]]
@ -3800,6 +3800,15 @@ version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.18"

View File

@ -15,7 +15,7 @@ data-encoding = "2.6.0"
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad.workspace = true kittycad.workspace = true
serde_json = "1.0.128" serde_json = "1.0.127"
tokio = { version = "1.40.0", features = ["sync"] } tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19" toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }

View File

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

View File

@ -2,6 +2,3 @@
new-test name: new-test name:
echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs
TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})' TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})'
lint:
cargo clippy --all --tests --benches -- -D warnings

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-test-server" name = "kcl-test-server"
description = "A test server for KCL" description = "A test server for KCL"
version = "0.1.10" version = "0.1.9"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -11,5 +11,5 @@ hyper = { version = "0.14.29", features = ["server"] }
kcl-lib = { version = "0.2", path = "../kcl" } kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0" pico-args = "0.5.0"
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.128" serde_json = "1.0.127"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.14" version = "0.2.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -16,11 +16,11 @@ async-recursion = "1.1.1"
async-trait = "0.1.82" async-trait = "0.1.82"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.17", default-features = false, optional = true, features = ["std", "derive"] } clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0" convert_case = "0.6.0"
dashmap = "6.1.0" dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.26", path = "../derive-docs" } derive-docs = { version = "0.1.24", path = "../derive-docs" }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
git_rev = "0.1.0" git_rev = "0.1.0"
@ -37,7 +37,7 @@ reqwest = { version = "0.11.26", default-features = false, features = ["stream",
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.128" serde_json = "1.0.127"
sha2 = "0.10.8" sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true } tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.63" thiserror = "1.0.63"
@ -47,7 +47,7 @@ url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.6.18" winnow = "0.5.40"
zip = { version = "2.0.0", default-features = false } zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::{settings::types::UnitLength::Mm, test_server}; use kcl_lib::test_server;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) { pub fn bench_execute(c: &mut Criterion) {
@ -13,42 +13,26 @@ pub fn bench_execute(c: &mut Criterion) {
// Configure Criterion.rs to detect smaller differences and increase sample size to improve // Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise. // precision and counteract the resulting noise.
group.sample_size(10); group.sample_size(10);
group.bench_with_input(BenchmarkId::new("execute", name), &code, |b, &s| { group.bench_with_input(BenchmarkId::new("execute_", name), &code, |b, &s| {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime // Spawn a future onto the runtime
b.iter(|| { b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap(); rt.block_on(test_server::execute_and_snapshot(
s,
kcl_lib::settings::types::UnitLength::Mm,
))
.unwrap();
}); });
}); });
group.finish(); group.finish();
} }
} }
pub fn bench_lego(c: &mut Criterion) { criterion_group!(benches, bench_execute);
let mut group = c.benchmark_group("executor_lego_pattern");
// Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise.
group.sample_size(10);
// Create lego bricks with N x 10 bumps, where N is each element of `sizes`.
let sizes = vec![1, 2, 4];
for size in sizes {
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let rt = Runtime::new().unwrap();
let code = LEGO_PROGRAM.replace("{{N}}", &size.to_string());
// Spawn a future onto the runtime
b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(&code, Mm)).unwrap();
});
});
}
group.finish();
}
criterion_group!(benches, bench_lego, bench_execute);
criterion_main!(benches); criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl"); const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl"); const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl"); const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl"); const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");
const LEGO_PROGRAM: &str = include_str!("../../tests/executor/inputs/slow_lego.kcl.tmpl");

View File

@ -995,20 +995,20 @@ impl SketchSurface {
} }
pub(crate) fn x_axis(&self) -> Point3d { pub(crate) fn x_axis(&self) -> Point3d {
match self { match self {
SketchSurface::Plane(plane) => plane.x_axis, SketchSurface::Plane(plane) => plane.x_axis.clone(),
SketchSurface::Face(face) => face.x_axis, SketchSurface::Face(face) => face.x_axis.clone(),
} }
} }
pub(crate) fn y_axis(&self) -> Point3d { pub(crate) fn y_axis(&self) -> Point3d {
match self { match self {
SketchSurface::Plane(plane) => plane.y_axis, SketchSurface::Plane(plane) => plane.y_axis.clone(),
SketchSurface::Face(face) => face.y_axis, SketchSurface::Face(face) => face.y_axis.clone(),
} }
} }
pub(crate) fn z_axis(&self) -> Point3d { pub(crate) fn z_axis(&self) -> Point3d {
match self { match self {
SketchSurface::Plane(plane) => plane.z_axis, SketchSurface::Plane(plane) => plane.z_axis.clone(),
SketchSurface::Face(face) => face.z_axis, SketchSurface::Face(face) => face.z_axis.clone(),
} }
} }
} }
@ -1304,7 +1304,7 @@ impl Point2d {
} }
} }
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, Default)]
#[ts(export)] #[ts(export)]
pub struct Point3d { pub struct Point3d {
pub x: f64, pub x: f64,
@ -1313,7 +1313,6 @@ pub struct Point3d {
} }
impl Point3d { impl Point3d {
pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 };
pub fn new(x: f64, y: f64, z: f64) -> Self { pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z } Self { x, y, z }
} }

View File

@ -927,7 +927,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
match body_items_within_function.parse_next(i) { match body_items_within_function.parse_next(i) {
Err(ErrMode::Backtrack(_)) => { Err(ErrMode::Backtrack(_)) => {
i.reset(&start); i.reset(start);
break; break;
} }
Err(e) => return Err(e), Err(e) => return Err(e),
@ -937,7 +937,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
} }
} }
(Err(ErrMode::Backtrack(_)), _) => { (Err(ErrMode::Backtrack(_)), _) => {
i.reset(&start); i.reset(start);
break; break;
} }
(Err(e), _) => return Err(e), (Err(e), _) => return Err(e),
@ -1276,7 +1276,7 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
/// Consume tokens that make up a binary expression, but don't actually return them. /// Consume tokens that make up a binary expression, but don't actually return them.
/// Why not? /// Why not?
/// Because this is designed to be used with .take() within the `binary_expression` parser. /// Because this is designed to be used with .recognize() within the `binary_expression` parser.
fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> { fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> {
let first = operand.parse_next(i).map(BinaryExpressionToken::from)?; let first = operand.parse_next(i).map(BinaryExpressionToken::from)?;
let remaining: Vec<_> = repeat( let remaining: Vec<_> = repeat(
@ -1308,7 +1308,7 @@ fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> {
} }
fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> { fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> {
let span_with_brackets = bracketed_section.take().parse_next(i)?; let span_with_brackets = bracketed_section.recognize().parse_next(i)?;
let n = span_with_brackets.len(); let n = span_with_brackets.len();
let mut span_no_brackets = &span_with_brackets[1..n - 1]; let mut span_no_brackets = &span_with_brackets[1..n - 1];
let expr = binary_expression.parse_next(&mut span_no_brackets)?; let expr = binary_expression.parse_next(&mut span_no_brackets)?;

View File

@ -1,6 +1,5 @@
use winnow::{ use winnow::{
error::{ErrorKind, ParseError, StrContext}, error::{ErrorKind, ParseError, StrContext},
stream::Stream,
Located, Located,
}; };
@ -103,17 +102,14 @@ impl<C> std::default::Default for ContextError<C> {
} }
} }
impl<I, C> winnow::error::ParserError<I> for ContextError<C> impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
where
I: Stream,
{
#[inline] #[inline]
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self { fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
Self::default() Self::default()
} }
#[inline] #[inline]
fn append(self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, _kind: ErrorKind) -> Self { fn append(self, _input: &I, _kind: ErrorKind) -> Self {
self self
} }
@ -123,12 +119,9 @@ where
} }
} }
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> {
where
I: Stream,
{
#[inline] #[inline]
fn add_context(mut self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, ctx: C) -> Self { fn add_context(mut self, _input: &I, ctx: C) -> Self {
self.context.push(ctx); self.context.push(ctx);
self self
} }

View File

@ -294,13 +294,6 @@ impl Args {
FromArgs::from_args(self, 0) 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> pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
where where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized, T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
@ -367,13 +360,6 @@ impl Args {
FromArgs::from_args(self, 0) 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> { pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
@ -634,8 +620,6 @@ impl_from_arg_via_json!(super::revolve::RevolveData);
impl_from_arg_via_json!(super::sketch::SketchData); impl_from_arg_via_json!(super::sketch::SketchData);
impl_from_arg_via_json!(crate::std::import::ImportFormat); 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::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!(SketchGroup);
impl_from_arg_via_json!(FaceTag); impl_from_arg_via_json!(FaceTag);
impl_from_arg_via_json!(String); impl_from_arg_via_json!(String);
@ -706,13 +690,3 @@ 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

@ -1,10 +1,7 @@
//! Functions related to extruding. //! Functions related to extruding.
use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kittycad::types::{ExtrusionFaceCapType, ExtrusionFaceInfo};
use schemars::JsonSchema; use schemars::JsonSchema;
use uuid::Uuid; use uuid::Uuid;
@ -93,7 +90,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
adjust_camera: false, adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_group.on { planar_normal: if let SketchSurface::Plane(plane) = &sketch_group.on {
// We pass in the normal for the plane here. // We pass in the normal for the plane here.
Some(plane.z_axis.into()) Some(plane.z_axis.clone().into())
} else { } else {
None None
}, },
@ -101,7 +98,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
) )
.await?; .await?;
args.batch_modeling_cmd( args.send_modeling_cmd(
id, id,
kittycad::types::ModelingCmd::Extrude { kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id, target: sketch_group.id,
@ -114,7 +111,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
// Disable the sketch mode. // Disable the sketch mode.
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {}) args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
.await?; .await?;
extrude_groups.push(do_post_extrude(sketch_group.clone(), length, args.clone()).await?); extrude_groups.push(do_post_extrude(sketch_group.clone(), length, id, args.clone()).await?);
} }
Ok(extrude_groups.into()) Ok(extrude_groups.into())
@ -123,6 +120,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
pub(crate) async fn do_post_extrude( pub(crate) async fn do_post_extrude(
sketch_group: SketchGroup, sketch_group: SketchGroup,
length: f64, length: f64,
id: Uuid,
args: Args, args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> { ) -> Result<Box<ExtrudeGroup>, KclError> {
// Bring the object to the front of the scene. // Bring the object to the front of the scene.
@ -166,7 +164,7 @@ pub(crate) async fn do_post_extrude(
let solid3d_info = args let solid3d_info = args
.send_modeling_cmd( .send_modeling_cmd(
uuid::Uuid::new_v4(), id,
kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo {
edge_id, edge_id,
object_id: sketch_group.id, object_id: sketch_group.id,
@ -183,95 +181,91 @@ pub(crate) async fn do_post_extrude(
vec![] vec![]
}; };
for (curve_id, face_id) in face_infos for face_info in face_infos.iter() {
.iter() if face_info.cap == kittycad::types::ExtrusionFaceCapType::None {
.filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
.filter_map(|face_info| {
if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) { if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
Some((curve_id, face_id)) args.batch_modeling_cmd(
} else { uuid::Uuid::new_v4(),
None kittycad::types::ModelingCmd::Solid3DGetOppositeEdge {
} edge_id: curve_id,
}) object_id: sketch_group.id,
{ face_id,
// Batch these commands, because the Rust code doesn't actually care about the outcome. },
// So, there's no need to await them. )
// Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm) .await?;
// uses this to build the artifact graph, which the UI needs.
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetOppositeEdge {
edge_id: curve_id,
object_id: sketch_group.id,
face_id,
},
)
.await?;
args.batch_modeling_cmd( args.batch_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge {
edge_id: curve_id, edge_id: curve_id,
object_id: sketch_group.id, object_id: sketch_group.id,
face_id, face_id,
}, },
) )
.await?; .await?;
}
}
} }
let Faces { // Create a hashmap for quick id lookup
sides: face_id_map, let mut face_id_map = std::collections::HashMap::new();
start_cap_id, // creating fake ids for start and end caps is to make extrudes mock-execute safe
end_cap_id, let mut start_cap_id = if args.ctx.is_mock { Some(Uuid::new_v4()) } else { None };
} = analyze_faces(&args, face_infos); let mut end_cap_id = if args.ctx.is_mock { Some(Uuid::new_v4()) } else { None };
// Iterate over the sketch_group.value array and add face_id to GeoMeta
let new_value = sketch_group
.value
.iter()
.flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path {
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
Some(extrude_surface)
}
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
Some(extrude_surface)
}
}
} else if args.ctx.is_mock {
// Only pre-populate the extrude surface if we are in mock mode.
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { for face_info in face_infos {
// pushing this values with a fake face_id to make extrudes mock-execute safe match face_info.cap {
face_id: Uuid::new_v4(), kittycad::types::ExtrusionFaceCapType::Bottom => start_cap_id = face_info.face_id,
tag: path.get_base().tag.clone(), kittycad::types::ExtrusionFaceCapType::Top => end_cap_id = face_info.face_id,
geo_meta: GeoMeta { _ => {
id: path.get_base().geo_meta.id, if let Some(curve_id) = face_info.curve_id {
metadata: path.get_base().geo_meta.metadata.clone(), face_id_map.insert(curve_id, face_info.face_id);
}, }
});
Some(extrude_surface)
} else {
None
} }
}) }
.collect(); }
// Iterate over the sketch_group.value array and add face_id to GeoMeta
let mut new_value: Vec<ExtrudeSurface> = Vec::new();
for path in sketch_group.value.iter() {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path {
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
new_value.push(extrude_surface);
}
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
});
new_value.push(extrude_surface);
}
}
} else if args.ctx.is_mock {
// Only pre-populate the extrude surface if we are in mock mode.
new_value.push(ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
// pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: Uuid::new_v4(),
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
}));
}
}
Ok(Box::new(ExtrudeGroup { Ok(Box::new(ExtrudeGroup {
// Ok so you would think that the id would be the id of the extrude group, // Ok so you would think that the id would be the id of the extrude group,
@ -279,45 +273,11 @@ pub(crate) async fn do_post_extrude(
// sketch group. // sketch group.
id: sketch_group.id, id: sketch_group.id,
value: new_value, value: new_value,
meta: sketch_group.meta.clone(), sketch_group: sketch_group.clone(),
sketch_group,
height: length, height: length,
start_cap_id, start_cap_id,
end_cap_id, end_cap_id,
edge_cuts: vec![], edge_cuts: vec![],
meta: sketch_group.meta,
})) }))
} }
#[derive(Default)]
struct Faces {
/// Maps curve ID to face ID for each side.
sides: HashMap<Uuid, Option<Uuid>>,
/// Top face ID.
end_cap_id: Option<Uuid>,
/// Bottom face ID.
start_cap_id: Option<Uuid>,
}
fn analyze_faces(args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
let mut faces = Faces {
sides: HashMap::with_capacity(face_infos.len()),
..Default::default()
};
if args.ctx.is_mock {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(Uuid::new_v4());
faces.end_cap_id = Some(Uuid::new_v4());
}
for face_info in face_infos {
match face_info.cap {
ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
faces.sides.insert(curve_id, face_info.face_id);
}
}
}
}
faces
}

View File

@ -1,174 +0,0 @@
//! 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 = 2;
/// 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])
/// ```
///
/// ```no_run
/// // Loft a square, a circle, and another circle with options.
/// 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], {
/// // This can be set to override the automatically determined
/// // topological base curve, which is usually the first section encountered.
/// baseCurveIndex: 0,
/// // 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.
/// bezApproximateRational: false,
/// // Tolerance for the loft operation.
/// tolerance: 0.000001,
/// // 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.
/// vDegree: 2,
/// })
/// ```
#[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, args).await
}

View File

@ -9,10 +9,8 @@ pub mod fillet;
pub mod helix; pub mod helix;
pub mod import; pub mod import;
pub mod kcl_stdlib; pub mod kcl_stdlib;
pub mod loft;
pub mod math; pub mod math;
pub mod patterns; pub mod patterns;
pub mod planes;
pub mod polar; pub mod polar;
pub mod revolve; pub mod revolve;
pub mod segment; pub mod segment;
@ -100,8 +98,6 @@ lazy_static! {
Box::new(crate::std::shell::Shell), Box::new(crate::std::shell::Shell),
Box::new(crate::std::shell::Hollow), Box::new(crate::std::shell::Hollow),
Box::new(crate::std::revolve::Revolve), 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::import::Import),
Box::new(crate::std::math::Cos), Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin), Box::new(crate::std::math::Sin),
@ -488,7 +484,7 @@ layout: manual
buf.push_str(&fn_docs); buf.push_str(&fn_docs);
// Write the file. // Write the file.
expectorate::assert_contents(format!("../../../docs/kcl/{}.md", internal_fn.name()), &buf); expectorate::assert_contents(&format!("../../../docs/kcl/{}.md", internal_fn.name()), &buf);
} }
} }

View File

@ -1,168 +0,0 @@
//! 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),
})
}

View File

@ -299,7 +299,7 @@ async fn inner_revolve(
} }
} }
do_post_extrude(sketch_group, 0.0, args).await do_post_extrude(sketch_group, 0.0, id, args).await
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1269,7 +1269,7 @@ pub(crate) async fn inner_start_profile_at(
adjust_camera: false, adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface { planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
// We pass in the normal for the plane here. // We pass in the normal for the plane here.
Some(plane.z_axis.into()) Some(plane.z_axis.clone().into())
} else { } else {
None None
}, },

View File

@ -50,13 +50,13 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
} }
fn block_comment(i: &mut Located<&str>) -> PResult<Token> { fn block_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = ("/*", take_until(0.., "*/"), "*/").take(); let inner = ("/*", take_until(0.., "*/"), "*/").recognize();
let (value, range) = inner.with_span().parse_next(i)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::BlockComment, value.to_string())) Ok(Token::from_range(range, TokenType::BlockComment, value.to_string()))
} }
fn line_comment(i: &mut Located<&str>) -> PResult<Token> { fn line_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).take(); let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).recognize();
let (value, range) = inner.with_span().parse_next(i)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::LineComment, value.to_string())) Ok(Token::from_range(range, TokenType::LineComment, value.to_string()))
} }
@ -68,7 +68,7 @@ fn number(i: &mut Located<&str>) -> PResult<Token> {
// No digits before the decimal point. // No digits before the decimal point.
('.', digit1).map(|_| ()), ('.', digit1).map(|_| ()),
)); ));
let (value, range) = number_parser.take().with_span().parse_next(i)?; let (value, range) = number_parser.recognize().with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::Number, value.to_string())) Ok(Token::from_range(range, TokenType::Number, value.to_string()))
} }
@ -79,12 +79,12 @@ fn whitespace(i: &mut Located<&str>) -> PResult<Token> {
fn inner_word(i: &mut Located<&str>) -> PResult<()> { fn inner_word(i: &mut Located<&str>) -> PResult<()> {
one_of(('a'..='z', 'A'..='Z', '_')).parse_next(i)?; one_of(('a'..='z', 'A'..='Z', '_')).parse_next(i)?;
repeat::<_, _, (), _, _>(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?; repeat(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?;
Ok(()) Ok(())
} }
fn word(i: &mut Located<&str>) -> PResult<Token> { fn word(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = inner_word.take().with_span().parse_next(i)?; let (value, range) = inner_word.recognize().with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::Word, value.to_string())) Ok(Token::from_range(range, TokenType::Word, value.to_string()))
} }
@ -162,9 +162,9 @@ fn inner_single_quote(i: &mut Located<&str>) -> PResult<()> {
} }
fn string(i: &mut Located<&str>) -> PResult<Token> { fn string(i: &mut Located<&str>) -> PResult<Token> {
let single_quoted_string = ('\'', inner_single_quote.take(), '\''); let single_quoted_string = ('\'', inner_single_quote.recognize(), '\'');
let double_quoted_string = ('"', inner_double_quote.take(), '"'); let double_quoted_string = ('"', inner_double_quote.recognize(), '"');
let either_quoted_string = alt((single_quoted_string.take(), double_quoted_string.take())); let either_quoted_string = alt((single_quoted_string.recognize(), double_quoted_string.recognize()));
let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?; let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::String, value.to_string())) Ok(Token::from_range(range, TokenType::String, value.to_string()))
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,3 +1,3 @@
[toolchain] [toolchain]
channel = "1.81.0" channel = "1.80.1"
components = ["clippy", "rustfmt"] components = ["clippy", "rustfmt"]

View File

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

View File

@ -1,81 +0,0 @@
// 2x8 Lego Brick
// A standard Lego brick with 2 bumps wide and 8 bumps long.
// Define constants
const lbumps = 10 // number of bumps long
const wbumps = {{N}} // number of bumps wide
const pitch = 8.0
const clearance = 0.1
const bumpDiam = 4.8
const bumpHeight = 1.8
const height = 9.6
const t = (pitch - (2 * clearance) - bumpDiam) / 2.0
const totalLength = lbumps * pitch - (2.0 * clearance)
const totalWidth = wbumps * pitch - (2.0 * clearance)
// Create the plane for the pegs. This is a hack so that the pegs can be patterned along the face of the lego base.
const pegFace = {
plane: {
origin: { x: 0, y: 0, z: height },
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }
}
}
// Create the plane for the tubes underneath the lego. This is a hack so that the tubes can be patterned underneath the lego.
const tubeFace = {
plane: {
origin: { x: 0, y: 0, z: height - t },
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }
}
}
// Make the base
const s = startSketchOn('XY')
|> startProfileAt([-totalWidth / 2, -totalLength / 2], %)
|> line([totalWidth, 0], %)
|> line([0, totalLength], %)
|> line([-totalWidth, 0], %)
|> close(%)
|> extrude(height, %)
// Sketch and extrude a rectangular shape to create the shell underneath the lego. This is a hack until we have a shell function.
const shellExtrude = startSketchOn(s, "start")
|> startProfileAt([
-(totalWidth / 2 - t),
-(totalLength / 2 - t)
], %)
|> line([totalWidth - (2 * t), 0], %)
|> line([0, totalLength - (2 * t)], %)
|> line([-(totalWidth - (2 * t)), 0], %)
|> close(%)
|> extrude(-(height - t), %)
fn tr = (i) => {
let j = i + 1
let x = (j/wbumps) * pitch
let y = (j % wbumps) * pitch
return {
translate: [x, y, 0],
}
}
// Create the pegs on the top of the base
const totalBumps = (wbumps * lbumps)-1
const peg = startSketchOn(s, 'end')
|> circle([
-(pitch*(wbumps-1)/2),
-(pitch*(lbumps-1)/2)
], bumpDiam / 2, %)
|> patternLinear2d({
axis: [1, 0],
repetitions: wbumps-1,
distance: pitch
}, %)
|> patternLinear2d({
axis: [0, 1],
repetitions: lbumps-1,
distance: pitch
}, %)
|> extrude(bumpHeight, %)
// |> patternTransform(int(totalBumps-1), tr, %)

148
yarn.lock
View File

@ -2314,11 +2314,6 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz#20c09cf44dcb082140cc7f439dd679fe4bba3375" 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== 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": "@rushstack/eslint-patch@^1.1.0":
version "1.10.4" version "1.10.4"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1"
@ -2353,6 +2348,72 @@
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.8.4.tgz#0ff84b6a0e4b394335cf7ccf759c36b58cbd02eb" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.8.4.tgz#0ff84b6a0e4b394335cf7ccf759c36b58cbd02eb"
integrity sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg== integrity sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==
"@tauri-apps/cli-darwin-arm64@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.9.tgz#d6d9522b549a73ffb2c10ee273e6ac766dfa5914"
integrity sha512-RaCx1KpMX27iS1Cn7MYbVA0Gc5NsjU0Z1Qo42ibzF4OHInOkDcx3qjAaE+xD572Lb9ksBO725cIcYCdgqGu4Vw==
"@tauri-apps/cli-darwin-x64@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.9.tgz#7ae9abfbeff998f13608d9248bdadba73b1560c0"
integrity sha512-KKUs8kbHYZrcmY/AjKjxEEm7aHGWQsn3+BGsgamKl97k2K5R5Z0KLJUy6QVhUSISEIievjDPmBDIwgA6mlrCLQ==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.9.tgz#8330576565f9ac411011d491a26e94d9116eb5ad"
integrity sha512-OgVCt72g0AnIB3zuKJLEIOCNeviiNeLoQQsVs7ESaqxZ/gMXY35yGVhrFm83eAQ0G4BervHDog15bsY3Dxbc/g==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.9.tgz#9b4b79dd256c39fed495fd8b7ffdb798078c61ab"
integrity sha512-7kQcXXXpCYB0AWbTRaKAim3JVMKdrxVOiqnOW+7elkqDQxDqmLQho2ah1qHv7LzZ6Z83u5QejrRLeHrrdo3PEg==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.9.tgz#5afd06c1601ff823b7d82785236f63af379fd6d4"
integrity sha512-2hqANZrydqZpptUsfAHSL5DIaEfHN73UGEu+5keFCV1Irh+QPydr1CYrqhgFF982ev6Ars7nxALwpPhEODjYlg==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.9.tgz#39185adc857e3e8474008600b7f0a6e0e42abdbf"
integrity sha512-Zjna6eoVSlmZtzAXgH27sgJRnczNzMKRiGsMpY00PFxN9sbQwlsS3yMfB8GHsBeBoq+qJQsteRwhrn1mj6e3Rg==
"@tauri-apps/cli-linux-x64-musl@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.9.tgz#a8d703010892622cf38e87950f5d2920833fac88"
integrity sha512-8ODcbvwZw29sAWns36BeBYJ3iu3Mtv4J3WkcoVbanVCP8nu7ja3401VnWBjckRiI1iDJIm59m6ojVkGYQhAe9Q==
"@tauri-apps/cli-win32-arm64-msvc@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.9.tgz#8ddea7d990b701357fe3dfd8e8e1783898206d85"
integrity sha512-j6jJId8hlid/W4ezDRNK49DSjxb82W6d1qVqO7zksKdZLy8tVzFkZXwEeKhabzRQsO87KL34I+ciRlmInGis0Q==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.9.tgz#ffa340d2dbf0e87355fa92650fbd707adc12d84e"
integrity sha512-w9utY58kfzJS+iLCjyQyQbJS8YaCM8YCWkgK2ZkySmHAdnqdGeyJEWig1qrLH1TWd+O6K3TlCNv55ujeAtOE4w==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.9.tgz#93f0cdc8c6999227aeee86741b553c16cb7ac20f"
integrity sha512-+l2RcpTthzYkw3VsmcZkb099Jfl0d21a9VIFxdk+duKeYieRpb0MsIBP6fS7WlNAeqrinC0zi/zt+Nia6mPuyw==
"@tauri-apps/cli@^2.0.0-rc.9":
version "2.0.0-rc.9"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-rc.9.tgz#b641ad224dd055aae4f101c14d0696d2e06862c0"
integrity sha512-cjj5HVKHUlxL87TN7ZZpnlMgcBS+ToIyfLB6jpaNDZ9Op0/qzccWGZpPbW2P/BnfF/qwHzVJNUPGANFyvBSUeg==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "2.0.0-rc.9"
"@tauri-apps/cli-darwin-x64" "2.0.0-rc.9"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-rc.9"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-rc.9"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-rc.9"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-rc.9"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-rc.9"
"@tauri-apps/cli-win32-arm64-msvc" "2.0.0-rc.9"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-rc.9"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-rc.9"
"@testing-library/dom@^10.0.0": "@testing-library/dom@^10.0.0":
version "10.4.0" version "10.4.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8"
@ -3181,7 +3242,7 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
array-includes@^3.1.6, array-includes@^3.1.8: array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8:
version "3.1.8" version "3.1.8"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d"
integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==
@ -3210,7 +3271,7 @@ array.prototype.findlast@^1.2.5:
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
es-shim-unscopables "^1.0.2" es-shim-unscopables "^1.0.2"
array.prototype.findlastindex@^1.2.5: array.prototype.findlastindex@^1.2.3:
version "1.2.5" version "1.2.5"
resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d"
integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==
@ -3607,14 +3668,6 @@ builder-util-runtime@9.2.4:
debug "^4.3.4" debug "^4.3.4"
sax "^1.2.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: builder-util@24.13.1:
version "24.13.1" version "24.13.1"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816" resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816"
@ -4560,12 +4613,12 @@ electron-to-chromium@^1.5.4:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6"
integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==
electron-updater@^6.3.0: electron-updater@^6.2.1:
version "6.3.0" version "6.2.1"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.0.tgz#13a5c3c3f0b2b114fe33181e24a8270096734b3e" resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
integrity sha512-3Xlezhk+dKaSQrOnkQNqCGiuGSSUPO9BV9TQZ4Iig6AyTJ4FzJONE5gFFc382sY53Sh9dwJfzKsA3DxRHt2btw== integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
dependencies: dependencies:
builder-util-runtime "9.2.5" builder-util-runtime "9.2.4"
fs-extra "^10.1.0" fs-extra "^10.1.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"
lazy-val "^1.0.5" lazy-val "^1.0.5"
@ -4882,10 +4935,10 @@ eslint-import-resolver-node@^0.3.9:
is-core-module "^2.13.0" is-core-module "^2.13.0"
resolve "^1.22.4" resolve "^1.22.4"
eslint-module-utils@^2.9.0: eslint-module-utils@^2.8.0:
version "2.9.0" version "2.8.1"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34"
integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ== integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==
dependencies: dependencies:
debug "^3.2.7" debug "^3.2.7"
@ -4905,27 +4958,26 @@ eslint-plugin-flowtype@^8.0.3:
lodash "^4.17.21" lodash "^4.17.21"
string-natural-compare "^3.0.1" string-natural-compare "^3.0.1"
eslint-plugin-import@^2.25.3, eslint-plugin-import@^2.30.0: eslint-plugin-import@^2.25.0, eslint-plugin-import@^2.25.3:
version "2.30.0" version "2.29.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643"
integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==
dependencies: dependencies:
"@rtsao/scc" "^1.1.0" array-includes "^3.1.7"
array-includes "^3.1.8" array.prototype.findlastindex "^1.2.3"
array.prototype.findlastindex "^1.2.5"
array.prototype.flat "^1.3.2" array.prototype.flat "^1.3.2"
array.prototype.flatmap "^1.3.2" array.prototype.flatmap "^1.3.2"
debug "^3.2.7" debug "^3.2.7"
doctrine "^2.1.0" doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.9" eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.9.0" eslint-module-utils "^2.8.0"
hasown "^2.0.2" hasown "^2.0.0"
is-core-module "^2.15.1" is-core-module "^2.13.1"
is-glob "^4.0.3" is-glob "^4.0.3"
minimatch "^3.1.2" minimatch "^3.1.2"
object.fromentries "^2.0.8" object.fromentries "^2.0.7"
object.groupby "^1.0.3" object.groupby "^1.0.1"
object.values "^1.2.0" object.values "^1.1.7"
semver "^6.3.1" semver "^6.3.1"
tsconfig-paths "^3.15.0" tsconfig-paths "^3.15.0"
@ -6206,10 +6258,10 @@ is-ci@^3.0.0:
dependencies: dependencies:
ci-info "^3.2.0" ci-info "^3.2.0"
is-core-module@^2.13.0, is-core-module@^2.15.1: is-core-module@^2.13.0, is-core-module@^2.13.1:
version "2.15.1" version "2.15.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea"
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==
dependencies: dependencies:
hasown "^2.0.2" hasown "^2.0.2"
@ -7332,7 +7384,7 @@ object.entries@^1.1.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
object.fromentries@^2.0.8: object.fromentries@^2.0.7, object.fromentries@^2.0.8:
version "2.0.8" version "2.0.8"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65"
integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==
@ -7342,7 +7394,7 @@ object.fromentries@^2.0.8:
es-abstract "^1.23.2" es-abstract "^1.23.2"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
object.groupby@^1.0.3: object.groupby@^1.0.1:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e"
integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==
@ -7351,7 +7403,7 @@ object.groupby@^1.0.3:
define-properties "^1.2.1" define-properties "^1.2.1"
es-abstract "^1.23.2" es-abstract "^1.23.2"
object.values@^1.1.6, object.values@^1.2.0: object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==
@ -7993,10 +8045,10 @@ react-hot-toast@^2.4.1:
dependencies: dependencies:
goober "^2.1.10" goober "^2.1.10"
react-hotkeys-hook@^4.5.1: react-hotkeys-hook@^4.5.0:
version "4.5.1" version "4.5.0"
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.1.tgz#990260ecc7e5a431414148a93b02a2f1a9707897" resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz#807b389b15256daf6a813a1ec09e6698064fe97f"
integrity sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg== integrity sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==
react-is@^16.13.1: react-is@^16.13.1:
version "16.13.1" version "16.13.1"