Compare commits
25 Commits
franknoiro
...
kurt-model
Author | SHA1 | Date | |
---|---|---|---|
4107ab9b94 | |||
b473e23251 | |||
81498fcafe | |||
38d5fb8c41 | |||
a762d741a5 | |||
e62a5ccc47 | |||
4b8ca7f61f | |||
31b0a8af12 | |||
74b4cb9e08 | |||
e7c6dd3698 | |||
aa9abbe83f | |||
b19f3bbdb0 | |||
892e856471 | |||
84fae12cdd | |||
3d67781039 | |||
114c3a2580 | |||
02b4aa0476 | |||
57f4e1b79c | |||
35f9b82a65 | |||
cbddb3553d | |||
dd754c78ab | |||
150f56b47a | |||
0eef6ab7d3 | |||
91d3ba3fce | |||
7165aa1b41 |
18
.github/workflows/build-test-publish-apps.yml
vendored
@ -224,6 +224,8 @@ 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,
|
||||||
@ -240,6 +242,12 @@ 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
|
||||||
@ -297,7 +305,7 @@ 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.1.3
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
with:
|
with:
|
||||||
path: out
|
path: out
|
||||||
glob: 'Zoo*'
|
glob: 'Zoo*'
|
||||||
@ -305,7 +313,7 @@ jobs:
|
|||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket
|
- name: Upload update endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
with:
|
with:
|
||||||
path: out
|
path: out
|
||||||
glob: 'latest*'
|
glob: 'latest*'
|
||||||
@ -313,13 +321,13 @@ jobs:
|
|||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload download endpoint to public bucket
|
- name: Upload download endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
with:
|
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
|
- name: Upload release files to public bucket for tauri
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
with:
|
with:
|
||||||
path: "out/tauri/${{ env.VERSION }}"
|
path: "out/tauri/${{ env.VERSION }}"
|
||||||
glob: '*/Zoo*'
|
glob: '*/Zoo*'
|
||||||
@ -327,7 +335,7 @@ jobs:
|
|||||||
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
|
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket for tauri
|
- name: Upload update endpoint to public bucket for tauri
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
with:
|
with:
|
||||||
path: last_update.json
|
path: last_update.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
4
.github/workflows/playwright.yml
vendored
@ -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: 30
|
timeout-minutes: 40
|
||||||
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 --grep=@electron || true
|
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
|
||||||
else
|
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
|
||||||
|
8
Makefile
@ -7,6 +7,14 @@ 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)'
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ 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)
|
||||||
@ -63,6 +64,7 @@ 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)
|
||||||
|
477
docs/kcl/loft.md
Normal file
138
docs/kcl/offsetPlane.md
Normal file
4938
docs/kcl/std.json
@ -27,9 +27,19 @@ 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((code) => {
|
await page.addInitScript(() => {
|
||||||
localStorage.setItem('persistCode', code)
|
localStorage.setItem(
|
||||||
}, bracket)
|
'persistCode',
|
||||||
|
`// Extruded Triangle
|
||||||
|
const sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([10, 0], %)
|
||||||
|
|> line([-5, 10], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
const extrude001 = extrude(5, sketch001)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -261,10 +271,7 @@ test(
|
|||||||
|
|
||||||
await page.getByText('bracket').click()
|
await page.getByText('bracket').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
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.
|
||||||
@ -292,16 +299,7 @@ test(
|
|||||||
|
|
||||||
await page.getByText('router-template-slate').click()
|
await page.getByText('router-template-slate').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
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 () => {
|
||||||
|
@ -43,12 +43,6 @@ 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()
|
||||||
|
|
||||||
@ -56,6 +50,17 @@ 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'
|
||||||
|
)
|
||||||
|
const modelStateIndicatorLoading = page.getByTestId(
|
||||||
|
'model-state-indicator-loading'
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(modelStateIndicatorLoading).toBeVisible()
|
||||||
|
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 +109,7 @@ test(
|
|||||||
},
|
},
|
||||||
{ timeout: 15_000 }
|
{ timeout: 15_000 }
|
||||||
)
|
)
|
||||||
.toBe(477327)
|
.toBe(477481)
|
||||||
|
|
||||||
// clean up output.gltf
|
// clean up output.gltf
|
||||||
await fsp.rm('output.gltf')
|
await fsp.rm('output.gltf')
|
||||||
|
@ -147,9 +147,6 @@ 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 }
|
||||||
|
|
||||||
@ -173,10 +170,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, [75, 75, 75]), {
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
})
|
})
|
||||||
.toBeLessThan(10)
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
const exportLocations: Array<Paths> = []
|
const exportLocations: Array<Paths> = []
|
||||||
@ -207,7 +204,7 @@ test.describe('Can export from electron app', () => {
|
|||||||
},
|
},
|
||||||
{ timeout: 15_000 }
|
{ timeout: 15_000 }
|
||||||
)
|
)
|
||||||
.toBe(477327)
|
.toBe(477481)
|
||||||
|
|
||||||
// clean up output.gltf
|
// clean up output.gltf
|
||||||
await fsp.rm('output.gltf')
|
await fsp.rm('output.gltf')
|
||||||
@ -495,10 +492,6 @@ 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'
|
||||||
)
|
)
|
||||||
@ -856,10 +849,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, [132, 132, 132]), {
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
})
|
})
|
||||||
.toBeLessThan(10)
|
.toBeLessThan(15)
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await page.mouse.move(0, 0, { steps: 5 })
|
await page.mouse.move(0, 0, { steps: 5 })
|
||||||
@ -867,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`)
|
|||||||
await page.mouse.click(pointOnModel.x, pointOnModel.y)
|
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, [176, 180, 132]))
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
|
||||||
.toBeLessThan(10)
|
.toBeLessThan(15)
|
||||||
}).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()
|
||||||
@ -942,24 +935,15 @@ test(
|
|||||||
|
|
||||||
await page.getByText('bracket').click()
|
await page.getByText('bracket').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
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, [75, 75, 75]), {
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
})
|
})
|
||||||
.toBeLessThan(10)
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
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 () => {
|
||||||
@ -976,24 +960,15 @@ test(
|
|||||||
|
|
||||||
await page.getByText('router-template-slate').click()
|
await page.getByText('router-template-slate').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
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, [132, 132, 132]), {
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
})
|
})
|
||||||
.toBeLessThan(10)
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
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 () => {
|
||||||
@ -1744,7 +1719,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(60000)
|
await page.waitForTimeout(2000)
|
||||||
await folderToRename.click({ button: 'right' })
|
await folderToRename.click({ button: 'right' })
|
||||||
await expect(renameMenuItem).toBeVisible()
|
await expect(renameMenuItem).toBeVisible()
|
||||||
await renameMenuItem.click()
|
await renameMenuItem.click()
|
||||||
|
@ -358,6 +358,7 @@ 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,
|
||||||
@ -393,20 +394,22 @@ 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 expect(exportingToastMessage).toBeVisible()
|
await Promise.all([
|
||||||
await expect(alreadyExportingToastMessage).toBeVisible()
|
expect(exportingToastMessage.first()).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 expect(exportingToastMessage).not.toBeVisible()
|
await Promise.all([
|
||||||
await expect(errorToastMessage).not.toBeVisible()
|
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
|
||||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
expect(errorToastMessage).not.toBeVisible(),
|
||||||
|
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||||
await expect(successToastMessage).toBeVisible()
|
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
|
||||||
|
expect(alreadyExportingToastMessage).not.toBeVisible({
|
||||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
timeout: 15_000,
|
||||||
|
}),
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -419,10 +422,12 @@ const sketch001 = startSketchAt([-0, -0])
|
|||||||
await expect(exportingToastMessage).toBeVisible()
|
await expect(exportingToastMessage).toBeVisible()
|
||||||
|
|
||||||
// Expect it to succeed.
|
// Expect it to succeed.
|
||||||
await expect(exportingToastMessage).not.toBeVisible()
|
await Promise.all([
|
||||||
await expect(errorToastMessage).not.toBeVisible()
|
expect(exportingToastMessage).not.toBeVisible(),
|
||||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
expect(errorToastMessage).not.toBeVisible(),
|
||||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||||
|
expect(alreadyExportingToastMessage).not.toBeVisible(),
|
||||||
|
])
|
||||||
|
|
||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible()
|
||||||
})
|
})
|
||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@ -852,10 +852,12 @@ 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')
|
||||||
@ -889,15 +891,19 @@ 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(
|
||||||
...TEST_SETTINGS,
|
appSettings
|
||||||
settings: {
|
? { settings: appSettings }
|
||||||
app: {
|
: {
|
||||||
...TEST_SETTINGS.app,
|
...TEST_SETTINGS,
|
||||||
projectDirectory: projectDirName,
|
settings: {
|
||||||
},
|
app: {
|
||||||
},
|
...TEST_SETTINGS.app,
|
||||||
})
|
projectDirectory: projectDirName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
|
|||||||
|
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
let noHoverColor: [number, number, number] = [82, 82, 82]
|
let noHoverColor: [number, number, number] = [92, 92, 92]
|
||||||
let hoverColor: [number, number, number] = [116, 116, 116]
|
let hoverColor: [number, number, number] = [127, 127, 127]
|
||||||
let selectColor: [number, number, number] = [144, 148, 97]
|
let selectColor: [number, number, number] = [155, 155, 105]
|
||||||
|
|
||||||
const extrudeWall = { x: 670, y: 275 }
|
const extrudeWall = { x: 670, y: 275 }
|
||||||
const extrudeText = `line([170.36, -121.61], %, $seg01)`
|
const extrudeText = `line([170.36, -121.61], %, $seg01)`
|
||||||
@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
|
|||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
||||||
.toBeLessThan(5)
|
.toBeLessThan(15)
|
||||||
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,43 +798,43 @@ 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(6)
|
).toBeLessThan(15)
|
||||||
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(6)
|
).toBeLessThan(15)
|
||||||
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(6)
|
).toBeLessThan(15)
|
||||||
|
|
||||||
await page.mouse.move(nothing.x, nothing.y)
|
await page.mouse.move(nothing.x, nothing.y)
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(300)
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
// because of shading, color is not exact everywhere on the face
|
// because of shading, color is not exact everywhere on the face
|
||||||
noHoverColor = [104, 104, 104]
|
noHoverColor = [115, 115, 115]
|
||||||
hoverColor = [134, 134, 134]
|
hoverColor = [145, 145, 145]
|
||||||
selectColor = [158, 162, 110]
|
selectColor = [168, 168, 120]
|
||||||
|
|
||||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
|
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15)
|
||||||
await page.mouse.move(cap.x, cap.y)
|
await 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(6)
|
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
|
||||||
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(6)
|
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||||
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(6)
|
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||||
})
|
})
|
||||||
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,
|
||||||
|
@ -303,6 +303,61 @@ test.describe('Testing settings', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Load desktop app with no settings file`,
|
||||||
|
{ tag: '@electron' },
|
||||||
|
async ({ browser: _ }, testInfo) => {
|
||||||
|
const { electronApp, page } = await setupElectron({
|
||||||
|
// This is what makes no settings file get created
|
||||||
|
cleanProjectDir: false,
|
||||||
|
testInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
|
const errorHeading = page.getByRole('heading', {
|
||||||
|
name: 'An unextected error occurred',
|
||||||
|
})
|
||||||
|
const projectDirLink = page.getByText('Loaded from')
|
||||||
|
|
||||||
|
// If the app loads without exploding we're in the clear
|
||||||
|
await expect(errorHeading).not.toBeVisible()
|
||||||
|
await expect(projectDirLink).toBeVisible()
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Load desktop app with a settings file, but no project directory setting`,
|
||||||
|
{ tag: '@electron' },
|
||||||
|
async ({ browser: _ }, testInfo) => {
|
||||||
|
const { electronApp, page } = await setupElectron({
|
||||||
|
testInfo,
|
||||||
|
appSettings: {
|
||||||
|
app: {
|
||||||
|
themeColor: '259',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
|
const errorHeading = page.getByRole('heading', {
|
||||||
|
name: 'An unextected error occurred',
|
||||||
|
})
|
||||||
|
const projectDirLink = page.getByText('Loaded from')
|
||||||
|
|
||||||
|
// If the app loads without exploding we're in the clear
|
||||||
|
await expect(errorHeading).not.toBeVisible()
|
||||||
|
await expect(projectDirLink).toBeVisible()
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test(
|
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' },
|
||||||
|
@ -21,6 +21,13 @@ 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}"
|
||||||
@ -38,6 +45,12 @@ 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
|
||||||
@ -47,7 +60,6 @@ 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"
|
||||||
|
|
||||||
@ -58,6 +70,12 @@ 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
|
||||||
|
1
interface.d.ts
vendored
@ -31,7 +31,6 @@ export interface IElectronAPI {
|
|||||||
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
|
setBaseUrl: (value: string) => void
|
||||||
loadProjectAtStartup: () => Promise<ProjectState | null>
|
|
||||||
packageJson: {
|
packageJson: {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.24.12",
|
"version": "0.25.0",
|
||||||
"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.2.1",
|
"electron-updater": "^6.3.0",
|
||||||
"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.0",
|
"react-hotkeys-hook": "^4.5.1",
|
||||||
"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",
|
||||||
@ -169,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.25.0",
|
"eslint-plugin-import": "^2.30.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",
|
||||||
|
@ -69,19 +69,6 @@ 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)
|
||||||
|
@ -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'
|
||||||
|
|
||||||
function useEngineCommands(): [CommandLog[], () => void] {
|
export function useEngineCommands(): [CommandLog[], () => void] {
|
||||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
|
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
|
||||||
engineCommandManager.commandLogs
|
engineCommandManager.commandLogs
|
||||||
)
|
)
|
||||||
|
@ -179,10 +179,7 @@ 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.isFirstRender = true
|
kclManager.executeCode(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)
|
||||||
|
@ -11,6 +11,8 @@ 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)
|
||||||
|
|
||||||
@ -65,17 +67,7 @@ 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"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
<Spinner />
|
||||||
<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={
|
||||||
|
@ -11,6 +11,7 @@ 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,
|
||||||
@ -65,6 +66,7 @@ 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}`
|
||||||
|
27
src/components/ModelStateIndicator.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Spinner } from './Spinner'
|
||||||
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
|
|
||||||
|
export const ModelStateIndicator = () => {
|
||||||
|
const { isExecuting } = useKclContext()
|
||||||
|
|
||||||
|
if (isExecuting)
|
||||||
|
return (
|
||||||
|
<div className="w-6 h-6" data-testid="model-state-indicator-loading">
|
||||||
|
<Spinner className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed"
|
||||||
|
data-testid="model-state-indicator"
|
||||||
|
>
|
||||||
|
<CustomIcon
|
||||||
|
data-testid="model-state-indicator-execution-done"
|
||||||
|
name="checkmark"
|
||||||
|
className="w-6 h-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -66,7 +66,6 @@ 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'
|
||||||
@ -161,9 +160,7 @@ 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) => {
|
||||||
@ -363,7 +360,7 @@ export const ModelingMachineProvider = ({
|
|||||||
return {}
|
return {}
|
||||||
}),
|
}),
|
||||||
Make: async (_, event) => {
|
Make: async (_, event) => {
|
||||||
if (event.type !== 'Make' || TEST) return
|
if (event.type !== 'Make') 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')
|
||||||
@ -407,7 +404,7 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
'Engine export': async (_, event) => {
|
'Engine export': async (_, event) => {
|
||||||
if (event.type !== 'Export' || TEST) return
|
if (event.type !== 'Export') return
|
||||||
if (engineCommandManager.exportIntent) {
|
if (engineCommandManager.exportIntent) {
|
||||||
toast.error('Already exporting')
|
toast.error('Already exporting')
|
||||||
return
|
return
|
||||||
|
@ -193,10 +193,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
resetSettingsIncludesUnitChange
|
resetSettingsIncludesUnitChange
|
||||||
) {
|
) {
|
||||||
// Unit changes requires a re-exec of code
|
// Unit changes requires a re-exec of code
|
||||||
kclManager.isFirstRender = true
|
kclManager.executeCode(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(
|
||||||
|
17
src/components/Spinner.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { SVGProps } from 'react'
|
||||||
|
|
||||||
|
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
|
||||||
|
<circle
|
||||||
|
cx="5"
|
||||||
|
cy="5"
|
||||||
|
r="4"
|
||||||
|
stroke="var(--primary)"
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray="4, 4"
|
||||||
|
className="animate-spin origin-center"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -54,12 +54,10 @@ 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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -219,7 +217,7 @@ export const Stream = () => {
|
|||||||
* Play the vid
|
* Play the vid
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kclManager.isFirstRender) {
|
if (!kclManager.isExecuting) {
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
// execute in the next event loop
|
// execute in the next event loop
|
||||||
videoRef.current?.play().catch((e) => {
|
videoRef.current?.play().catch((e) => {
|
||||||
@ -227,7 +225,7 @@ export const Stream = () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [kclManager.isFirstRender])
|
}, [kclManager.isExecuting])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -382,15 +380,15 @@ export const Stream = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(!isNetworkOkay || isLoading || kclManager.isFirstRender) && (
|
{(!isNetworkOkay || isLoading) && (
|
||||||
<div className="text-center absolute inset-0">
|
<div className="text-center absolute inset-0">
|
||||||
<Loading>
|
<Loading>
|
||||||
{!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? (
|
{!isNetworkOkay && !isLoading ? (
|
||||||
<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>
|
|
||||||
) : (
|
) : (
|
||||||
<span data-testid="loading-stream">Loading stream...</span>
|
!isLoading && (
|
||||||
|
<span data-testid="loading-stream">Loading stream...</span>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,8 +60,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ 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
|
||||||
@ -10,7 +12,7 @@ interface CommonCommandProperties {
|
|||||||
|
|
||||||
export interface PlaneArtifact {
|
export interface PlaneArtifact {
|
||||||
type: 'plane'
|
type: 'plane'
|
||||||
pathIds: Array<string>
|
pathIds: Array<ArtifactId>
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
export interface PlaneArtifactRich {
|
export interface PlaneArtifactRich {
|
||||||
@ -21,16 +23,16 @@ export interface PlaneArtifactRich {
|
|||||||
|
|
||||||
export interface PathArtifact {
|
export interface PathArtifact {
|
||||||
type: 'path'
|
type: 'path'
|
||||||
planeId: string
|
planeId: ArtifactId
|
||||||
segIds: Array<string>
|
segIds: Array<ArtifactId>
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
solid2dId?: string
|
solid2dId?: ArtifactId
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
interface solid2D {
|
interface solid2D {
|
||||||
type: 'solid2D'
|
type: 'solid2D'
|
||||||
pathId: string
|
pathId: ArtifactId
|
||||||
}
|
}
|
||||||
export interface PathArtifactRich {
|
export interface PathArtifactRich {
|
||||||
type: 'path'
|
type: 'path'
|
||||||
@ -42,10 +44,10 @@ export interface PathArtifactRich {
|
|||||||
|
|
||||||
interface SegmentArtifact {
|
interface SegmentArtifact {
|
||||||
type: 'segment'
|
type: 'segment'
|
||||||
pathId: string
|
pathId: ArtifactId
|
||||||
surfaceId: string
|
surfaceId: ArtifactId
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<ArtifactId>
|
||||||
edgeCutId?: string
|
edgeCutId?: ArtifactId
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
interface SegmentArtifactRich {
|
interface SegmentArtifactRich {
|
||||||
@ -59,9 +61,9 @@ interface SegmentArtifactRich {
|
|||||||
|
|
||||||
interface ExtrusionArtifact {
|
interface ExtrusionArtifact {
|
||||||
type: 'extrusion'
|
type: 'extrusion'
|
||||||
pathId: string
|
pathId: ArtifactId
|
||||||
surfaceIds: Array<string>
|
surfaceIds: Array<ArtifactId>
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<ArtifactId>
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
interface ExtrusionArtifactRich {
|
interface ExtrusionArtifactRich {
|
||||||
@ -74,23 +76,23 @@ interface ExtrusionArtifactRich {
|
|||||||
|
|
||||||
interface WallArtifact {
|
interface WallArtifact {
|
||||||
type: 'wall'
|
type: 'wall'
|
||||||
segId: string
|
segId: ArtifactId
|
||||||
edgeCutEdgeIds: Array<string>
|
edgeCutEdgeIds: Array<ArtifactId>
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
pathIds: Array<string>
|
pathIds: Array<ArtifactId>
|
||||||
}
|
}
|
||||||
interface CapArtifact {
|
interface CapArtifact {
|
||||||
type: 'cap'
|
type: 'cap'
|
||||||
subType: 'start' | 'end'
|
subType: 'start' | 'end'
|
||||||
edgeCutEdgeIds: Array<string>
|
edgeCutEdgeIds: Array<ArtifactId>
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
pathIds: Array<string>
|
pathIds: Array<ArtifactId>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtrudeEdge {
|
interface ExtrudeEdge {
|
||||||
type: 'extrudeEdge'
|
type: 'extrudeEdge'
|
||||||
segId: string
|
segId: ArtifactId
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
subType: 'opposite' | 'adjacent'
|
subType: 'opposite' | 'adjacent'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,16 +100,16 @@ interface ExtrudeEdge {
|
|||||||
interface EdgeCut {
|
interface EdgeCut {
|
||||||
type: 'edgeCut'
|
type: 'edgeCut'
|
||||||
subType: 'fillet' | 'chamfer'
|
subType: 'fillet' | 'chamfer'
|
||||||
consumedEdgeId: string
|
consumedEdgeId: ArtifactId
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<ArtifactId>
|
||||||
surfaceId: string
|
surfaceId: ArtifactId
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EdgeCutEdge {
|
interface EdgeCutEdge {
|
||||||
type: 'edgeCutEdge'
|
type: 'edgeCutEdge'
|
||||||
edgeCutId: string
|
edgeCutId: ArtifactId
|
||||||
surfaceId: string
|
surfaceId: ArtifactId
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Artifact =
|
export type Artifact =
|
||||||
@ -122,7 +124,7 @@ export type Artifact =
|
|||||||
| EdgeCutEdge
|
| EdgeCutEdge
|
||||||
| solid2D
|
| solid2D
|
||||||
|
|
||||||
export type ArtifactGraph = Map<string, Artifact>
|
export type ArtifactGraph = Map<ArtifactId, Artifact>
|
||||||
|
|
||||||
export type EngineCommand = Models['WebSocketRequest_type']
|
export type EngineCommand = Models['WebSocketRequest_type']
|
||||||
|
|
||||||
@ -149,7 +151,7 @@ export function createArtifactGraph({
|
|||||||
responseMap: ResponseMap
|
responseMap: ResponseMap
|
||||||
ast: Program
|
ast: Program
|
||||||
}) {
|
}) {
|
||||||
const myMap = new Map<string, Artifact>()
|
const myMap = new Map<ArtifactId, Artifact>()
|
||||||
|
|
||||||
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
||||||
let currentPlaneId = ''
|
let currentPlaneId = ''
|
||||||
@ -166,7 +168,7 @@ export function createArtifactGraph({
|
|||||||
const artifactsToUpdate = getArtifactsToUpdate({
|
const artifactsToUpdate = getArtifactsToUpdate({
|
||||||
orderedCommand,
|
orderedCommand,
|
||||||
responseMap,
|
responseMap,
|
||||||
getArtifact: (id: string) => myMap.get(id),
|
getArtifact: (id: ArtifactId) => myMap.get(id),
|
||||||
currentPlaneId,
|
currentPlaneId,
|
||||||
ast,
|
ast,
|
||||||
})
|
})
|
||||||
@ -224,11 +226,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: string) => Artifact | undefined
|
getArtifact: (id: ArtifactId) => Artifact | undefined
|
||||||
currentPlaneId: string
|
currentPlaneId: ArtifactId
|
||||||
ast: Program
|
ast: Program
|
||||||
}): Array<{
|
}): Array<{
|
||||||
id: string
|
id: ArtifactId
|
||||||
artifact: Artifact
|
artifact: Artifact
|
||||||
}> {
|
}> {
|
||||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||||
@ -514,7 +516,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<string, Extract<Artifact, { type: T[number] }>>
|
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
||||||
@ -528,7 +530,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
|||||||
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
||||||
},
|
},
|
||||||
map: ArtifactGraph
|
map: ArtifactGraph
|
||||||
): Map<string, Extract<Artifact, { type: T[number] }>> {
|
): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
|
||||||
return new Map(
|
return new Map(
|
||||||
[...map].filter(
|
[...map].filter(
|
||||||
([key, value]) =>
|
([key, value]) =>
|
||||||
@ -537,7 +539,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<string, Extract<Artifact, { type: T[number] }>>
|
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
||||||
@ -545,7 +547,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
|||||||
key,
|
key,
|
||||||
types,
|
types,
|
||||||
}: {
|
}: {
|
||||||
key: string
|
key: ArtifactId
|
||||||
types: T
|
types: T
|
||||||
},
|
},
|
||||||
map: ArtifactGraph
|
map: ArtifactGraph
|
||||||
@ -718,7 +720,7 @@ export function getExtrudeEdgeCodeRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getExtrusionFromSuspectedExtrudeSurface(
|
export function getExtrusionFromSuspectedExtrudeSurface(
|
||||||
id: string,
|
id: ArtifactId,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): ExtrusionArtifact | Error {
|
): ExtrusionArtifact | Error {
|
||||||
const artifact = getArtifactOfTypes(
|
const artifact = getArtifactOfTypes(
|
||||||
@ -733,7 +735,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getExtrusionFromSuspectedPath(
|
export function getExtrusionFromSuspectedPath(
|
||||||
id: string,
|
id: ArtifactId,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): ExtrusionArtifact | Error {
|
): ExtrusionArtifact | Error {
|
||||||
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
|
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
|
||||||
|
@ -1252,6 +1252,10 @@ 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
|
||||||
@ -1918,7 +1922,13 @@ 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,
|
resolve: (passThrough) => {
|
||||||
|
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)
|
||||||
|
@ -95,8 +95,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,29 +462,60 @@ 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 configObj = parseAppSettings(configToml)
|
const parsedAppConfig = parseAppSettings(configToml)
|
||||||
if (err(configObj)) {
|
if (err(parsedAppConfig)) {
|
||||||
return Promise.reject(configObj)
|
return Promise.reject(parsedAppConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return configObj
|
const hasProjectDirectorySetting =
|
||||||
|
parsedAppConfig.settings?.project?.directory ||
|
||||||
|
parsedAppConfig.settings?.app?.project_directory
|
||||||
|
|
||||||
|
if (hasProjectDirectorySetting) {
|
||||||
|
return parsedAppConfig
|
||||||
|
} else {
|
||||||
|
// inject the default project directory setting
|
||||||
|
const mergedConfig: DeepPartial<Configuration> = {
|
||||||
|
...parsedAppConfig,
|
||||||
|
settings: {
|
||||||
|
...parsedAppConfig.settings,
|
||||||
|
project: Object.assign(
|
||||||
|
{},
|
||||||
|
parsedAppConfig.settings?.project,
|
||||||
|
initialProjectDirConfig
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return mergedConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The file doesn't exist, create a new one.
|
// 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> = {
|
|
||||||
settings: { project: { directory: await getInitialDefaultDir() } },
|
// inject the default project directory setting
|
||||||
|
const mergedDefaultConfig: DeepPartial<Configuration> = {
|
||||||
|
...defaultAppConfig,
|
||||||
|
settings: {
|
||||||
|
...defaultAppConfig.settings,
|
||||||
|
project: Object.assign(
|
||||||
|
{},
|
||||||
|
defaultAppConfig.settings?.project,
|
||||||
|
initialProjectDirConfig
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
const config = Object.assign(defaultAppConfig, initialDirConfig)
|
return mergedDefaultConfig
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const writeAppSettingsFile = async (tomlStr: string) => {
|
export const writeAppSettingsFile = async (tomlStr: string) => {
|
||||||
|
@ -31,11 +31,11 @@ const bracket = startSketchOn('XY')
|
|||||||
|> extrude(width, %)
|
|> extrude(width, %)
|
||||||
|> fillet({
|
|> fillet({
|
||||||
radius: filletR,
|
radius: filletR,
|
||||||
tags: [getPreviousAdjacentEdge(innerEdge)]
|
tags: [getNextAdjacentEdge(innerEdge)]
|
||||||
}, %)
|
}, %)
|
||||||
|> fillet({
|
|> fillet({
|
||||||
radius: filletR + thickness,
|
radius: filletR + thickness,
|
||||||
tags: [getPreviousAdjacentEdge(outerEdge)]
|
tags: [getNextAdjacentEdge(outerEdge)]
|
||||||
}, %)`
|
}, %)`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,7 +14,7 @@ const save_ = async (file: ModelingAppFile) => {
|
|||||||
extensions.push(extension)
|
extensions.push(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(window as any).playwrightSkipFilePicker) {
|
if (window.electron.process.env.IS_PLAYWRIGHT) {
|
||||||
// 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,
|
||||||
|
@ -81,7 +81,6 @@ 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> {
|
||||||
|
@ -14,6 +14,7 @@ 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,
|
||||||
@ -176,6 +177,11 @@ 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',
|
||||||
|
@ -16,7 +16,6 @@ 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
|
||||||
|
115
src/main.ts
@ -60,7 +60,7 @@ if (process.defaultApp) {
|
|||||||
// Must be done before ready event.
|
// Must be done before ready event.
|
||||||
registerStartupListeners()
|
registerStartupListeners()
|
||||||
|
|
||||||
const createWindow = (): BrowserWindow => {
|
const createWindow = (filePath?: string): BrowserWindow => {
|
||||||
const newWindow = new BrowserWindow({
|
const newWindow = new BrowserWindow({
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
show: false,
|
show: false,
|
||||||
@ -81,9 +81,26 @@ const createWindow = (): BrowserWindow => {
|
|||||||
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 {
|
||||||
newWindow.loadFile(
|
getProjectPathAtStartup(filePath).then((projectPath) => {
|
||||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
|
const startIndex = path.join(
|
||||||
)
|
__dirname,
|
||||||
|
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (projectPath === null) {
|
||||||
|
newWindow.loadFile(startIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Loading file', projectPath)
|
||||||
|
|
||||||
|
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
||||||
|
console.log('Full URL', fullUrl)
|
||||||
|
|
||||||
|
newWindow.loadFile(startIndex, {
|
||||||
|
hash: fullUrl,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
@ -94,13 +111,11 @@ const createWindow = (): BrowserWindow => {
|
|||||||
return newWindow
|
return newWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, even on macOS. There, it's common
|
||||||
// for applications and their menu bar to stay active until the user quits
|
// for applications and their menu bar to stay active until the user quits
|
||||||
// explicitly with Cmd + Q.
|
// explicitly with Cmd + Q, but it is a really weird behavior with our app.
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
app.quit()
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
@ -235,7 +250,9 @@ app.on('ready', async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('loadProjectAtStartup', async () => {
|
const getProjectPathAtStartup = async (
|
||||||
|
filePath?: string
|
||||||
|
): Promise<string | null> => {
|
||||||
// If we are in development mode, we don't want to load a project at
|
// 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 '.'
|
||||||
@ -243,52 +260,54 @@ ipcMain.handle('loadProjectAtStartup', async () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let projectPath: string | null = null
|
let projectPath: string | null = filePath || null
|
||||||
// macOS: open-file events that were received before the app is ready
|
if (projectPath === null) {
|
||||||
const macOpenFiles: string[] = (global as any).macOpenFiles
|
// macOS: open-file events that were received before the app is ready
|
||||||
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
|
const macOpenFiles: string[] = (global as any).macOpenFiles
|
||||||
projectPath = macOpenFiles[0] // We only do one project at a time
|
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
|
||||||
}
|
projectPath = macOpenFiles[0] // We only do one project at a time
|
||||||
// Reset this so we don't accidentally use it again.
|
}
|
||||||
const macOpenFilesEmpty: string[] = []
|
// Reset this so we don't accidentally use it again.
|
||||||
// @ts-ignore
|
const macOpenFilesEmpty: string[] = []
|
||||||
global['macOpenFiles'] = macOpenFilesEmpty
|
// @ts-ignore
|
||||||
|
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}`)
|
||||||
try {
|
const currentFile = await getCurrentProjectFile(projectPath)
|
||||||
const currentFile = await getCurrentProjectFile(projectPath)
|
|
||||||
console.log(`Project loaded: ${currentFile}`)
|
if (currentFile instanceof Error) {
|
||||||
return currentFile
|
console.error(currentFile)
|
||||||
} catch (e) {
|
return null
|
||||||
console.error(e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
console.log(`Project loaded: ${currentFile}`)
|
||||||
|
return currentFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})
|
}
|
||||||
|
|
||||||
function parseCLIArgs(): minimist.ParsedArgs {
|
function parseCLIArgs(): minimist.ParsedArgs {
|
||||||
return minimist(process.argv, {})
|
return minimist(process.argv, {})
|
||||||
@ -305,10 +324,11 @@ 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()
|
createWindow(path)
|
||||||
|
} else {
|
||||||
|
macOpenFiles.push(path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -324,10 +344,11 @@ 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()
|
createWindow(url)
|
||||||
|
} else {
|
||||||
|
openUrls.push(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,9 +60,6 @@ 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
|
||||||
@ -96,7 +93,6 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
isWindows,
|
isWindows,
|
||||||
isLinux,
|
isLinux,
|
||||||
},
|
},
|
||||||
loadProjectAtStartup,
|
|
||||||
// IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is
|
// 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.
|
// no way to set it across the bridge boundary. We need to make it a command.
|
||||||
setBaseUrl: (value: string) => (process.env.BASE_URL = value),
|
setBaseUrl: (value: string) => (process.env.BASE_URL = value),
|
||||||
|
@ -107,10 +107,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
|
|||||||
codeManager.updateCodeStateEditor(bracket)
|
codeManager.updateCodeStateEditor(bracket)
|
||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
|
|
||||||
kclManager.isFirstRender = true
|
await kclManager.executeCode(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"
|
||||||
|
@ -13,10 +13,7 @@ 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('')
|
||||||
kclManager.isFirstRender = true
|
await kclManager.executeCode(true)
|
||||||
await kclManager.executeCode(true).then(() => {
|
|
||||||
kclManager.isFirstRender = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEditor()
|
clearEditor()
|
||||||
|
@ -82,10 +82,7 @@ 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)
|
||||||
kclManager.isFirstRender = true
|
await kclManager.executeCode(true)
|
||||||
await kclManager.executeCode(true).then(() => {
|
|
||||||
kclManager.isFirstRender = false
|
|
||||||
})
|
|
||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
})
|
})
|
||||||
}, [editorManager.editorView])
|
}, [editorManager.editorView])
|
||||||
|
6
src/wasm-lib/Cargo.lock
generated
@ -672,7 +672,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
version = "0.1.25"
|
version = "0.1.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1345,7 +1345,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.11"
|
version = "0.2.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx",
|
"approx",
|
||||||
@ -1417,7 +1417,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
version = "0.1.9"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
@ -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.25"
|
version = "0.1.26"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -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.9"
|
version = "0.1.10"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
@ -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.11"
|
version = "0.2.13"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
@ -20,7 +20,7 @@ clap = { version = "4.5.16", default-features = false, optional = true, features
|
|||||||
convert_case = "0.6.0"
|
convert_case = "0.6.0"
|
||||||
dashmap = "6.0.1"
|
dashmap = "6.0.1"
|
||||||
databake = { version = "0.1.8", features = ["derive"] }
|
databake = { version = "0.1.8", features = ["derive"] }
|
||||||
derive-docs = { version = "0.1.24", path = "../derive-docs" }
|
derive-docs = { version = "0.1.26", path = "../derive-docs" }
|
||||||
form_urlencoded = "1.2.1"
|
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"
|
||||||
|
@ -294,6 +294,13 @@ 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,
|
||||||
@ -360,6 +367,13 @@ 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)
|
||||||
}
|
}
|
||||||
@ -620,6 +634,8 @@ impl_from_arg_via_json!(super::revolve::RevolveData);
|
|||||||
impl_from_arg_via_json!(super::sketch::SketchData);
|
impl_from_arg_via_json!(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);
|
||||||
@ -690,3 +706,13 @@ impl<'a> FromKclValue<'a> for SketchSurface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> FromKclValue<'a> for Vec<SketchGroup> {
|
||||||
|
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
|
||||||
|
let KclValue::UserVal(uv) = arg else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
uv.get::<Vec<SketchGroup>>().map(|x| x.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
141
src/wasm-lib/kcl/src/std/loft.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
//! Standard library lofts.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use derive_docs::stdlib;
|
||||||
|
use kittycad::types::ModelingCmd;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{KclError, KclErrorDetails},
|
||||||
|
executor::{ExtrudeGroup, KclValue, SketchGroup},
|
||||||
|
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_V_DEGREE: u32 = 1;
|
||||||
|
|
||||||
|
/// Data for a loft.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LoftData {
|
||||||
|
/// Degree of the interpolation. Must be greater than zero.
|
||||||
|
/// For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction.
|
||||||
|
/// This defaults to 2, if not specified.
|
||||||
|
pub v_degree: Option<std::num::NonZeroU32>,
|
||||||
|
/// Attempt to approximate rational curves (such as arcs) using a bezier.
|
||||||
|
/// This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
|
||||||
|
/// Over time, this field won't be necessary.
|
||||||
|
#[serde(default)]
|
||||||
|
pub bez_approximate_rational: Option<bool>,
|
||||||
|
/// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_curve_index: Option<u32>,
|
||||||
|
/// Tolerance for the loft operation.
|
||||||
|
#[serde(default)]
|
||||||
|
pub tolerance: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoftData {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
// This unwrap is safe because the default value is always greater than zero.
|
||||||
|
v_degree: Some(std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
|
||||||
|
bez_approximate_rational: None,
|
||||||
|
base_curve_index: None,
|
||||||
|
tolerance: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a 3D surface or solid by interpolating between two or more sketches.
|
||||||
|
pub async fn loft(args: Args) -> Result<KclValue, KclError> {
|
||||||
|
let (sketch_groups, data): (Vec<SketchGroup>, Option<LoftData>) = args.get_sketch_groups_and_data()?;
|
||||||
|
|
||||||
|
let extrude_group = inner_loft(sketch_groups, data, args).await?;
|
||||||
|
Ok(KclValue::ExtrudeGroup(extrude_group))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a 3D surface or solid by interpolating between two or more sketches.
|
||||||
|
///
|
||||||
|
/// The sketches need to closed and on the same plane.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // Loft a square and a triangle.
|
||||||
|
/// const squareSketch = startSketchOn('XY')
|
||||||
|
/// |> startProfileAt([-100, 200], %)
|
||||||
|
/// |> line([200, 0], %)
|
||||||
|
/// |> line([0, -200], %)
|
||||||
|
/// |> line([-200, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// const triangleSketch = startSketchOn(offsetPlane('XY', 75))
|
||||||
|
/// |> startProfileAt([0, 125], %)
|
||||||
|
/// |> line([-15, -30], %)
|
||||||
|
/// |> line([30, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// loft([squareSketch, triangleSketch])
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // Loft a square, a circle, and another circle.
|
||||||
|
/// const squareSketch = startSketchOn('XY')
|
||||||
|
/// |> startProfileAt([-100, 200], %)
|
||||||
|
/// |> line([200, 0], %)
|
||||||
|
/// |> line([0, -200], %)
|
||||||
|
/// |> line([-200, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
|
||||||
|
/// |> circle([0, 100], 50, %)
|
||||||
|
///
|
||||||
|
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
|
||||||
|
/// |> circle([0, 100], 20, %)
|
||||||
|
///
|
||||||
|
/// loft([squareSketch, circleSketch0, circleSketch1])
|
||||||
|
/// ```
|
||||||
|
#[stdlib {
|
||||||
|
name = "loft",
|
||||||
|
}]
|
||||||
|
async fn inner_loft(
|
||||||
|
sketch_groups: Vec<SketchGroup>,
|
||||||
|
data: Option<LoftData>,
|
||||||
|
args: Args,
|
||||||
|
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||||
|
// Make sure we have at least two sketches.
|
||||||
|
if sketch_groups.len() < 2 {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"Loft requires at least two sketches, but only {} were provided.",
|
||||||
|
sketch_groups.len()
|
||||||
|
),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the loft data.
|
||||||
|
let data = data.unwrap_or_default();
|
||||||
|
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
args.batch_modeling_cmd(
|
||||||
|
id,
|
||||||
|
ModelingCmd::Loft {
|
||||||
|
section_ids: sketch_groups.iter().map(|group| group.id).collect(),
|
||||||
|
base_curve_index: data.base_curve_index,
|
||||||
|
bez_approximate_rational: data.bez_approximate_rational.unwrap_or(false),
|
||||||
|
tolerance: data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units)),
|
||||||
|
v_degree: data
|
||||||
|
.v_degree
|
||||||
|
.unwrap_or_else(|| std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap())
|
||||||
|
.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Using the first sketch as the base curve, idk we might want to change this later.
|
||||||
|
do_post_extrude(sketch_groups[0].clone(), 0.0, id, args).await
|
||||||
|
}
|
@ -9,8 +9,10 @@ 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;
|
||||||
@ -98,6 +100,8 @@ 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),
|
||||||
|
168
src/wasm-lib/kcl/src/std/planes.rs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
//! Standard library plane helpers.
|
||||||
|
|
||||||
|
use derive_docs::stdlib;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::KclError,
|
||||||
|
executor::{KclValue, Metadata, Plane, UserVal},
|
||||||
|
std::{sketch::PlaneData, Args},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One of the standard planes.
|
||||||
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum StandardPlane {
|
||||||
|
/// The XY plane.
|
||||||
|
#[serde(rename = "XY", alias = "xy")]
|
||||||
|
XY,
|
||||||
|
/// The opposite side of the XY plane.
|
||||||
|
#[serde(rename = "-XY", alias = "-xy")]
|
||||||
|
NegXY,
|
||||||
|
/// The XZ plane.
|
||||||
|
#[serde(rename = "XZ", alias = "xz")]
|
||||||
|
XZ,
|
||||||
|
/// The opposite side of the XZ plane.
|
||||||
|
#[serde(rename = "-XZ", alias = "-xz")]
|
||||||
|
NegXZ,
|
||||||
|
/// The YZ plane.
|
||||||
|
#[serde(rename = "YZ", alias = "yz")]
|
||||||
|
YZ,
|
||||||
|
/// The opposite side of the YZ plane.
|
||||||
|
#[serde(rename = "-YZ", alias = "-yz")]
|
||||||
|
NegYZ,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StandardPlane> for PlaneData {
|
||||||
|
fn from(value: StandardPlane) -> Self {
|
||||||
|
match value {
|
||||||
|
StandardPlane::XY => PlaneData::XY,
|
||||||
|
StandardPlane::NegXY => PlaneData::NegXY,
|
||||||
|
StandardPlane::XZ => PlaneData::XZ,
|
||||||
|
StandardPlane::NegXZ => PlaneData::NegXZ,
|
||||||
|
StandardPlane::YZ => PlaneData::YZ,
|
||||||
|
StandardPlane::NegYZ => PlaneData::NegYZ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset a plane by a distance along its normal.
|
||||||
|
pub async fn offset_plane(args: Args) -> Result<KclValue, KclError> {
|
||||||
|
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
|
||||||
|
|
||||||
|
let plane = inner_offset_plane(std_plane, offset).await?;
|
||||||
|
|
||||||
|
Ok(KclValue::UserVal(UserVal::set(
|
||||||
|
vec![Metadata {
|
||||||
|
source_range: args.source_range,
|
||||||
|
}],
|
||||||
|
plane,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset a plane by a distance along its normal.
|
||||||
|
///
|
||||||
|
/// For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ'
|
||||||
|
/// plane and 10 units away from it.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // Loft a square and a circle on the `XY` plane using offset.
|
||||||
|
/// const squareSketch = startSketchOn('XY')
|
||||||
|
/// |> startProfileAt([-100, 200], %)
|
||||||
|
/// |> line([200, 0], %)
|
||||||
|
/// |> line([0, -200], %)
|
||||||
|
/// |> line([-200, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// const circleSketch = startSketchOn(offsetPlane('XY', 150))
|
||||||
|
/// |> circle([0, 100], 50, %)
|
||||||
|
///
|
||||||
|
/// loft([squareSketch, circleSketch])
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // Loft a square and a circle on the `XZ` plane using offset.
|
||||||
|
/// const squareSketch = startSketchOn('XZ')
|
||||||
|
/// |> startProfileAt([-100, 200], %)
|
||||||
|
/// |> line([200, 0], %)
|
||||||
|
/// |> line([0, -200], %)
|
||||||
|
/// |> line([-200, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// const circleSketch = startSketchOn(offsetPlane('XZ', 150))
|
||||||
|
/// |> circle([0, 100], 50, %)
|
||||||
|
///
|
||||||
|
/// loft([squareSketch, circleSketch])
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // Loft a square and a circle on the `YZ` plane using offset.
|
||||||
|
/// const squareSketch = startSketchOn('YZ')
|
||||||
|
/// |> startProfileAt([-100, 200], %)
|
||||||
|
/// |> line([200, 0], %)
|
||||||
|
/// |> line([0, -200], %)
|
||||||
|
/// |> line([-200, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// const circleSketch = startSketchOn(offsetPlane('YZ', 150))
|
||||||
|
/// |> circle([0, 100], 50, %)
|
||||||
|
///
|
||||||
|
/// loft([squareSketch, circleSketch])
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// // Loft a square and a circle on the `-XZ` plane using offset.
|
||||||
|
/// const squareSketch = startSketchOn('-XZ')
|
||||||
|
/// |> startProfileAt([-100, 200], %)
|
||||||
|
/// |> line([200, 0], %)
|
||||||
|
/// |> line([0, -200], %)
|
||||||
|
/// |> line([-200, 0], %)
|
||||||
|
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// const circleSketch = startSketchOn(offsetPlane('-XZ', -150))
|
||||||
|
/// |> circle([0, 100], 50, %)
|
||||||
|
///
|
||||||
|
/// loft([squareSketch, circleSketch])
|
||||||
|
/// ```
|
||||||
|
#[stdlib {
|
||||||
|
name = "offsetPlane",
|
||||||
|
}]
|
||||||
|
async fn inner_offset_plane(std_plane: StandardPlane, offset: f64) -> Result<PlaneData, KclError> {
|
||||||
|
// Convert to the plane type.
|
||||||
|
let plane_data: PlaneData = std_plane.into();
|
||||||
|
// Convert to a plane.
|
||||||
|
let mut plane = Plane::from(plane_data);
|
||||||
|
|
||||||
|
match std_plane {
|
||||||
|
StandardPlane::XY => {
|
||||||
|
plane.origin.z += offset;
|
||||||
|
}
|
||||||
|
StandardPlane::XZ => {
|
||||||
|
plane.origin.y -= offset;
|
||||||
|
}
|
||||||
|
StandardPlane::YZ => {
|
||||||
|
plane.origin.x += offset;
|
||||||
|
}
|
||||||
|
StandardPlane::NegXY => {
|
||||||
|
plane.origin.z -= offset;
|
||||||
|
}
|
||||||
|
StandardPlane::NegXZ => {
|
||||||
|
plane.origin.y += offset;
|
||||||
|
}
|
||||||
|
StandardPlane::NegYZ => {
|
||||||
|
plane.origin.x -= offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PlaneData::Plane {
|
||||||
|
origin: Box::new(plane.origin),
|
||||||
|
x_axis: Box::new(plane.x_axis),
|
||||||
|
y_axis: Box::new(plane.y_axis),
|
||||||
|
z_axis: Box::new(plane.z_axis),
|
||||||
|
})
|
||||||
|
}
|
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_loft0.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_loft1.png
Normal file
After Width: | Height: | Size: 126 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 56 KiB |
@ -55,10 +55,10 @@ const bracketBody = bs
|
|||||||
|> fillet({
|
|> fillet({
|
||||||
radius: radius,
|
radius: radius,
|
||||||
tags: [
|
tags: [
|
||||||
getNextAdjacentEdge(bs.tags.edge7),
|
getPreviousAdjacentEdge(bs.tags.edge7),
|
||||||
getNextAdjacentEdge(bs.tags.edge2),
|
getPreviousAdjacentEdge(bs.tags.edge2),
|
||||||
getNextAdjacentEdge(bs.tags.edge3),
|
getPreviousAdjacentEdge(bs.tags.edge3),
|
||||||
getNextAdjacentEdge(bs.tags.edge6)
|
getPreviousAdjacentEdge(bs.tags.edge6)
|
||||||
]
|
]
|
||||||
}, %)
|
}, %)
|
||||||
|
|
||||||
|
82
yarn.lock
@ -2314,6 +2314,11 @@
|
|||||||
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"
|
||||||
@ -3242,7 +3247,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.7, array-includes@^3.1.8:
|
array-includes@^3.1.6, 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==
|
||||||
@ -3271,7 +3276,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.3:
|
array.prototype.findlastindex@^1.2.5:
|
||||||
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==
|
||||||
@ -3668,6 +3673,14 @@ 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"
|
||||||
@ -4613,12 +4626,12 @@ electron-to-chromium@^1.5.4:
|
|||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6"
|
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.2.1:
|
electron-updater@^6.3.0:
|
||||||
version "6.2.1"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
|
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.0.tgz#13a5c3c3f0b2b114fe33181e24a8270096734b3e"
|
||||||
integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
|
integrity sha512-3Xlezhk+dKaSQrOnkQNqCGiuGSSUPO9BV9TQZ4Iig6AyTJ4FzJONE5gFFc382sY53Sh9dwJfzKsA3DxRHt2btw==
|
||||||
dependencies:
|
dependencies:
|
||||||
builder-util-runtime "9.2.4"
|
builder-util-runtime "9.2.5"
|
||||||
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"
|
||||||
@ -4935,10 +4948,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.8.0:
|
eslint-module-utils@^2.9.0:
|
||||||
version "2.8.1"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34"
|
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342"
|
||||||
integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==
|
integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^3.2.7"
|
debug "^3.2.7"
|
||||||
|
|
||||||
@ -4958,26 +4971,27 @@ 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.0, eslint-plugin-import@^2.25.3:
|
eslint-plugin-import@^2.25.3, eslint-plugin-import@^2.30.0:
|
||||||
version "2.29.1"
|
version "2.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449"
|
||||||
integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==
|
integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes "^3.1.7"
|
"@rtsao/scc" "^1.1.0"
|
||||||
array.prototype.findlastindex "^1.2.3"
|
array-includes "^3.1.8"
|
||||||
|
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.8.0"
|
eslint-module-utils "^2.9.0"
|
||||||
hasown "^2.0.0"
|
hasown "^2.0.2"
|
||||||
is-core-module "^2.13.1"
|
is-core-module "^2.15.1"
|
||||||
is-glob "^4.0.3"
|
is-glob "^4.0.3"
|
||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
object.fromentries "^2.0.7"
|
object.fromentries "^2.0.8"
|
||||||
object.groupby "^1.0.1"
|
object.groupby "^1.0.3"
|
||||||
object.values "^1.1.7"
|
object.values "^1.2.0"
|
||||||
semver "^6.3.1"
|
semver "^6.3.1"
|
||||||
tsconfig-paths "^3.15.0"
|
tsconfig-paths "^3.15.0"
|
||||||
|
|
||||||
@ -6258,10 +6272,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.13.1:
|
is-core-module@^2.13.0, is-core-module@^2.15.1:
|
||||||
version "2.15.0"
|
version "2.15.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea"
|
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
|
||||||
integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==
|
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
|
|
||||||
@ -7384,7 +7398,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.7, object.fromentries@^2.0.8:
|
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==
|
||||||
@ -7394,7 +7408,7 @@ object.fromentries@^2.0.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.1:
|
object.groupby@^1.0.3:
|
||||||
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==
|
||||||
@ -7403,7 +7417,7 @@ object.groupby@^1.0.1:
|
|||||||
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.1.7, object.values@^1.2.0:
|
object.values@^1.1.6, 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==
|
||||||
@ -8045,10 +8059,10 @@ react-hot-toast@^2.4.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
goober "^2.1.10"
|
goober "^2.1.10"
|
||||||
|
|
||||||
react-hotkeys-hook@^4.5.0:
|
react-hotkeys-hook@^4.5.1:
|
||||||
version "4.5.0"
|
version "4.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz#807b389b15256daf6a813a1ec09e6698064fe97f"
|
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.1.tgz#990260ecc7e5a431414148a93b02a2f1a9707897"
|
||||||
integrity sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==
|
integrity sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==
|
||||||
|
|
||||||
react-is@^16.13.1:
|
react-is@^16.13.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
|