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

View File

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

View File

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

View File

@ -56,7 +56,6 @@ layout: manual
* [`line`](kcl/line)
* [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log)
* [`log10`](kcl/log10)
* [`log2`](kcl/log2)
@ -64,7 +63,6 @@ layout: manual
* [`max`](kcl/max)
* [`min`](kcl/min)
* [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d)
* [`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)
// Load the app with the working starter code
await page.addInitScript(() => {
localStorage.setItem(
'persistCode',
`// Extruded Triangle
const sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([10, 0], %)
|> line([-5, 10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)`
)
})
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, bracket)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
@ -271,7 +261,10 @@ test(
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.
@ -299,7 +292,16 @@ test(
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 () => {

View File

@ -43,6 +43,12 @@ test(
// open the project
await page.getByText(`bracket`).click()
// wait for the project to load
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -50,17 +56,6 @@ test(
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// Wait for the model to finish loading
const modelStateIndicator = page.getByTestId(
'model-state-indicator-execution-done'
)
const modelStateIndicatorLoading = page.getByTestId(
'model-state-indicator-loading'
)
await expect(modelStateIndicatorLoading).toBeVisible()
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
@ -109,7 +104,7 @@ test(
},
{ timeout: 15_000 }
)
.toBe(477481)
.toBe(477327)
// clean up 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)
page.on('console', console.log)
await electronApp.context().addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
})
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
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(10)
})
const exportLocations: Array<Paths> = []
@ -204,7 +207,7 @@ test.describe('Can export from electron app', () => {
},
{ timeout: 15_000 }
)
.toBe(477481)
.toBe(477327)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -492,6 +495,10 @@ test(
await file.click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
)
@ -849,10 +856,10 @@ const extrude001 = extrude(200, sketch001)`)
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(10)
await expect(async () => {
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)
// check user can interact with model by checking it turns yellow
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
.toBeLessThan(15)
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
.toBeLessThan(10)
}).toPass({ timeout: 40_000, intervals: [1_000] })
await page.getByTestId('app-logo').click()
@ -935,15 +942,24 @@ test(
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
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(10)
})
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 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
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(10)
})
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 page.waitForTimeout(2000)
await page.waitForTimeout(60000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

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

View File

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

View File

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

1
interface.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -179,7 +179,10 @@ const FileTreeItem = ({
codeManager.writeToFile()
// 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 {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)

View File

@ -11,8 +11,6 @@ import {
import { engineCommandManager } from '../lib/singletons'
import { Spinner } from './Spinner'
const Loading = ({ children }: React.PropsWithChildren) => {
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"
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={

View File

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

View File

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

View File

@ -193,7 +193,10 @@ export const SettingsAuthProviderBase = ({
resetSettingsIncludesUnitChange
) {
// Unit changes requires a re-exec of code
kclManager.executeCode(true)
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
} else {
// For any future logging we'd like to do
// 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.
*/
async function executeCodeAndPlayStream() {
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
kclManager.isFirstRender = false
setStreamState(StreamState.Playing)
})
}
@ -217,7 +219,7 @@ export const Stream = () => {
* Play the vid
*/
useEffect(() => {
if (!kclManager.isExecuting) {
if (!kclManager.isFirstRender) {
setTimeout(() =>
// execute in the next event loop
videoRef.current?.play().catch((e) => {
@ -225,7 +227,7 @@ export const Stream = () => {
})
)
}
}, [kclManager.isExecuting])
}, [kclManager.isFirstRender])
useEffect(() => {
if (
@ -380,15 +382,15 @@ export const Stream = () => {
</div>
</div>
)}
{(!isNetworkOkay || isLoading) && (
{(!isNetworkOkay || isLoading || kclManager.isFirstRender) && (
<div className="text-center absolute inset-0">
<Loading>
{!isNetworkOkay && !isLoading ? (
{!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? (
<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>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,6 +81,7 @@ export class MachineManager {
}
this._machines = await window.electron.listMachines()
console.log('Machines:', this._machines)
}
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 { appThemeToTheme } from 'lib/theme'
import {
getInitialDefaultDir,
readAppSettingsFile,
readProjectSettingsFile,
writeAppSettingsFile,
@ -177,11 +176,6 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings()
// Because getting the default directory is async, we need to set it after
if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir()
}
setSettingsAtLevel(
settings,
'user',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -294,13 +294,6 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_sketch_groups_and_data<'a, T>(&'a self) -> Result<(Vec<SketchGroup>, Option<T>), KclError>
where
T: FromArgs<'a> + serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
@ -367,13 +360,6 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_float<'a, T>(&'a self) -> Result<(T, f64), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> {
FromArgs::from_args(self, 0)
}
@ -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!(crate::std::import::ImportFormat);
impl_from_arg_via_json!(crate::std::polar::PolarCoordsData);
impl_from_arg_via_json!(crate::std::loft::LoftData);
impl_from_arg_via_json!(crate::std::planes::StandardPlane);
impl_from_arg_via_json!(SketchGroup);
impl_from_arg_via_json!(FaceTag);
impl_from_arg_via_json!(String);
@ -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 import;
pub mod kcl_stdlib;
pub mod loft;
pub mod math;
pub mod patterns;
pub mod planes;
pub mod polar;
pub mod revolve;
pub mod segment;
@ -100,8 +98,6 @@ lazy_static! {
Box::new(crate::std::shell::Shell),
Box::new(crate::std::shell::Hollow),
Box::new(crate::std::revolve::Revolve),
Box::new(crate::std::loft::Loft),
Box::new(crate::std::planes::OffsetPlane),
Box::new(crate::std::import::Import),
Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin),

View File

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