Compare commits

..

1 Commits

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

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

View File

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

View File

@ -303,61 +303,6 @@ 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' },

View File

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

@ -31,6 +31,7 @@ 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
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@ if (process.defaultApp) {
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() registerStartupListeners()
const createWindow = (filePath?: string): BrowserWindow => { const createWindow = (): BrowserWindow => {
const newWindow = new BrowserWindow({ const newWindow = new BrowserWindow({
autoHideMenuBar: true, autoHideMenuBar: true,
show: false, show: false,
@ -81,26 +81,9 @@ const createWindow = (filePath?: string): 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 {
getProjectPathAtStartup(filePath).then((projectPath) => { newWindow.loadFile(
const startIndex = path.join( path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
) )
if (projectPath === null) {
newWindow.loadFile(startIndex)
return
}
console.log('Loading file', projectPath)
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl)
newWindow.loadFile(startIndex, {
hash: fullUrl,
})
})
} }
// Open the DevTools. // Open the DevTools.
@ -111,11 +94,13 @@ const createWindow = (filePath?: string): BrowserWindow => {
return newWindow return newWindow
} }
// Quit when all windows are closed, even on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q, but it is a really weird behavior with our app. // explicitly with Cmd + Q.
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
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
@ -250,9 +235,7 @@ app.on('ready', async () => {
}) })
}) })
const getProjectPathAtStartup = async ( ipcMain.handle('loadProjectAtStartup', async () => {
filePath?: string
): Promise<string | null> => {
// If we are in development mode, we don't want to load a project at // If we are in development mode, we don't want to load a project at
// startup. // startup.
// Since the args passed are always '.' // Since the args passed are always '.'
@ -260,8 +243,7 @@ const getProjectPathAtStartup = async (
return null return null
} }
let projectPath: string | null = filePath || null let projectPath: string | null = null
if (projectPath === null) {
// macOS: open-file events that were received before the app is ready // macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles const macOpenFiles: string[] = (global as any).macOpenFiles
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) { if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
@ -290,25 +272,24 @@ const getProjectPathAtStartup = async (
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)
if (currentFile instanceof Error) {
console.error(currentFile)
return null
}
console.log(`Project loaded: ${currentFile}`) console.log(`Project loaded: ${currentFile}`)
return currentFile return currentFile
} catch (e) {
console.error(e)
} }
return null return null
} }
return null
})
function parseCLIArgs(): minimist.ParsedArgs { function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {}) return minimist(process.argv, {})
} }
@ -324,11 +305,10 @@ function registerStartupListeners() {
app.on('open-file', function (event, path) { app.on('open-file', function (event, path) {
event.preventDefault() event.preventDefault()
macOpenFiles.push(path)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow(path) createWindow()
} else {
macOpenFiles.push(path)
} }
}) })
@ -344,11 +324,10 @@ function registerStartupListeners() {
) { ) {
event.preventDefault() event.preventDefault()
openUrls.push(url)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow(url) createWindow()
} else {
openUrls.push(url)
} }
} }

View File

@ -60,6 +60,9 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> => const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api') ipcRenderer.invoke('find_machine_api')
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
login, login,
// Passing fs directly is not recommended since it gives a lot of power // Passing fs directly is not recommended since it gives a lot of power
@ -93,6 +96,7 @@ 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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.13" version = "0.2.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -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.26", path = "../derive-docs" } derive-docs = { version = "0.1.24", path = "../derive-docs" }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
git_rev = "0.1.0" git_rev = "0.1.0"

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

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

View File

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