Compare commits

..

10 Commits

390 changed files with 108041 additions and 116593 deletions

View File

@ -165,6 +165,7 @@ jobs:
- name: Build the app (release) - name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env: env:
PUBLISH_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -172,6 +173,7 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always
@ -227,6 +229,7 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always
@ -362,7 +365,7 @@ jobs:
- name: Set more complete nightly release notes - name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
run: | run: |
# Note: preferred going this way instead of a full clone in the checkout step, # Note: prefered going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471 # see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}" export TAG="nightly-${VERSION}"
@ -391,10 +394,6 @@ jobs:
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Invalidate bucket cache on latest*.yml and last_download.json files
if: ${{ env.IS_NIGHTLY == 'true' }}
run: yarn files:invalidate-bucket:nightly
- name: Tag nightly commit - name: Tag nightly commit
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: actions/github-script@v7 uses: actions/github-script@v7

View File

@ -126,7 +126,11 @@ jobs:
destination: 'dl.kittycad.io/releases/modeling-app' destination: 'dl.kittycad.io/releases/modeling-app'
- name: Invalidate bucket cache on latest*.yml and last_download.json files - name: Invalidate bucket cache on latest*.yml and last_download.json files
run: yarn files:invalidate-bucket run: |
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
- name: Upload release files to Github - name: Upload release files to Github
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}

1
.gitignore vendored
View File

@ -61,7 +61,6 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg src/wasm-lib/pkg
.eslintcache
venv venv
.vite/ .vite/

2
.nvmrc
View File

@ -1 +1 @@
v22.12.0 v21.7.3

View File

@ -1,43 +0,0 @@
# Setting Up Zoo Modeling App
Compared to other CAD software, getting Zoo Modeling App up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install.
## Windows
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type.
2. Once downloaded, run the installer `Zoo Modeling App-{version}-{arch}-win.exe` which should take a few seconds.
3. The installation happens at `C:\Program Files\Zoo Modeling App`. A shortcut in the start menu is also created so you can run the app easily by clicking on it.
## macOS
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type.
2. Once downloaded, open the disk image `Zoo Modeling App-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open.
## Linux
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type.
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
- On Ubuntu, install the FUSE library with these commands in a terminal.
```bash
sudo apt update
sudo apt install libfuse2
```
- Optionally, follow [these steps](https://github.com/probonopd/go-appimage/blob/master/src/appimaged/README.md#initial-setup) to install `appimaged`. It is a daemon that makes interacting with AppImage files more seamless.
- Once installed, copy the downloaded `Zoo Modeling App-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`.
- `appimaged` should automatically find it and make it executable. If not, run:
```bash
chmod a+x ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```
3. You can double-click on the AppImage to run it, or in a terminal with this command:
```bash
~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```

View File

@ -22,5 +22,3 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects) * [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX) * [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY) * [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc) * [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo) * [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin) * [`asin`](kcl/asin)

View File

@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js ```js
r = 10 // radius r = 10 // radius
// Call `map`, using an anonymous function instead of a named one. // Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], fn(id) { circles = map([1..3], (id) {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
}) })

View File

@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
// an anonymous `add` function as its parameter, instead of declaring a // an anonymous `add` function as its parameter, instead of declaring a
// named function outside. // named function outside.
arr = [1, 2, 3] arr = [1, 2, 3]
sum = reduce(arr, 0, fn(i, result_so_far) { sum = reduce(arr, 0, (i, result_so_far) {
return i + result_so_far return i + result_so_far
}) })
@ -84,7 +84,7 @@ fn decagon(radius) {
// Use a `reduce` to draw the remaining decagon sides. // Use a `reduce` to draw the remaining decagon sides.
// For each number in the array 1..10, run the given function, // For each number in the array 1..10, run the given function,
// which takes a partially-sketched decagon and adds one more edge to it. // which takes a partially-sketched decagon and adds one more edge to it.
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
// Draw one edge of the decagon. // Draw one edge of the decagon.
x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

View File

@ -458,8 +458,8 @@ test.describe('Editor tests', () => {
/* add the following code to the editor ($ error is not a valid line) /* add the following code to the editor ($ error is not a valid line)
$ error $ error
topAng = 30 const topAng = 30
bottomAng = 25 const bottomAng = 25
*/ */
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type('$ error') await page.keyboard.type('$ error')
@ -474,14 +474,12 @@ test.describe('Editor tests', () => {
await page.keyboard.type('bottomAng = 25') await page.keyboard.type('bottomAng = 25')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// error in gutter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect( await expect(page.getByText('Unexpected token: $').first()).toBeVisible()
page.getByText('Tag names must not be empty').first()
).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
await page.getByText('$ error').click() await page.getByText('$ error').click()

View File

@ -7,7 +7,6 @@ export class ToolbarFixture {
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator loftButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
@ -29,7 +28,6 @@ export class ToolbarFixture {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft') this.loftButton = page.getByTestId('loft')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')

View File

@ -768,168 +768,3 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
}) })
}) })
}) })
const shellPointAndClickCapCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
app,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
extrude001 = extrude(30, sketch001)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration =
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
})
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected faces`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await app.page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the cap`, async () => {
await clickOnCap()
await app.page.waitForTimeout(500)
})
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
})
})
})
test('Shell point-and-click wall', async ({
app,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-20, 20], %)
|> xLine(40, %)
|> yLine(-60, %)
|> xLine(-40, %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(40, sketch001)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 580, y: 180 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
const mutatedCode = 'xLine(-40, %, $seg01)'
const shellDeclaration =
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
const formattedOutLastLine = '}, extrude001)'
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await page.keyboard.down('Shift')
await clickOnWall()
await app.page.waitForTimeout(500)
await page.keyboard.up('Shift')
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap, 1 face',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(mutatedCode)
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [formattedOutLastLine],
highlightedCode: '',
})
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
})
})

View File

@ -136,335 +136,6 @@ test(
} }
) )
test(
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const errorDir = join(dir, 'broken-code')
await fsp.mkdir(errorDir, { recursive: true })
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(errorDir, 'main.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// 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]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home.
await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const emptyDir = join(dir, 'empty')
await fsp.mkdir(emptyDir, { recursive: true })
await fsp.writeFile(join(emptyDir, 'main.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// 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]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening empty code project should clear the scene', async () => {
// Go back home.
await expect(page.getByText('empty')).toBeVisible()
await page.getByText('empty').click()
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.writeFile(join(bracketDir, 'empty.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// 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]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('creating a empty file should clear the scene', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'empty.kcl' })
await expect(file).toBeVisible()
await file.click()
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(bracketDir, 'broken-code-test.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// 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]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('opening broken code file should clear the scene and show the error', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'broken-code-test.kcl' })
await expect(file).toBeVisible()
await file.click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test( test(
'when code with error first loads you get errors in console', 'when code with error first loads you get errors in console',
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -950,75 +950,7 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => { test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird. // FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos') test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test('Grid turned off', async ({ page }) => { test('Grid turned off', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 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: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -26,17 +26,7 @@ test.describe('Testing constraints', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
// constants and locators const PUR = 400 / 37.5 //pixeltoUnitRatio
const lengthValue = {
old: '20',
new: '25',
}
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -46,26 +36,26 @@ test.describe('Testing constraints', () => {
await u.closeDebugPanel() await u.closeDebugPanel()
// Click the line of code for line. // Click the line of code for line.
// TODO remove this and reinstate `await topHorzSegmentClick()` await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.down('Shift')
await page.mouse.click(834, 244)
await page.keyboard.up('Shift')
await page await page
.getByRole('button', { name: 'dimension Length', exact: true }) .getByRole('button', { name: 'dimension Length', exact: true })
.click() .click()
await expect(cmdBarKclInput).toHaveText('20') await page.getByText('Add constraining value').click()
await cmdBarKclInput.fill(lengthValue.new)
await expect(
page.getByText(`Can't calculate`),
`Something went wrong with the KCL expression evaluation`
).not.toBeVisible()
await cmdBarSubmitButton.click()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)` `length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
) )
// Make sure we didn't pop out of sketch mode. // Make sure we didn't pop out of sketch mode.
@ -76,6 +66,7 @@ test.describe('Testing constraints', () => {
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
// Exit sketch // Exit sketch
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
@ -533,7 +524,7 @@ part002 = startSketchOn('XZ')
}) })
} }
}) })
test.describe('Test Angle constraint single selection', () => { test.describe('Test Angle/Length constraint single selection', () => {
const cases = [ const cases = [
{ {
testName: 'Angle - Add variable', testName: 'Angle - Add variable',
@ -547,6 +538,18 @@ part002 = startSketchOn('XZ')
constraint: 'angle', constraint: 'angle',
value: '83, 78.33', value: '83, 78.33',
}, },
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const ] as const
for (const { testName, addVariable, value, constraint } of cases) { for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => { test(`${testName}`, async ({ page }) => {
@ -605,90 +608,6 @@ part002 = startSketchOn('XZ')
}) })
} }
}) })
test.describe('Test Length constraint single selection', () => {
const cases = [
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => {
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarKclVariableNameInput =
page.getByPlaceholder('Variable name')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(
`[data-overlay-index="${2}"]`
)
await page.mouse.click(line3.x, line3.y)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByTestId('dropdown-constraint-' + constraint).click()
if (!addVariable) {
await test.step(`Clear the variable input`, async () => {
await cmdBarKclVariableNameInput.clear()
await cmdBarKclVariableNameInput.press('Backspace')
})
}
await expect(cmdBarKclInput).toHaveText('78.33')
await cmdBarSubmitButton.click()
const changedCode = `|> angledLine([${value}], %)`
await expect(page.locator('.cm-content')).toContainText(changedCode)
// checking active assures the cursor is where it should be
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Many segments - no modal constraints', () => { test.describe('Many segments - no modal constraints', () => {
const cases = [ const cases = [
{ {
@ -949,15 +868,6 @@ part002 = startSketchOn('XZ')
|> line([3.13, -2.4], %)` |> line([3.13, -2.4], %)`
) )
}) })
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -1018,8 +928,8 @@ part002 = startSketchOn('XZ')
// await page.getByRole('button', { name: 'length', exact: true }).click() // await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('dropdown-constraint-length').click() await page.getByTestId('dropdown-constraint-length').click()
await cmdBarKclInput.fill('10') await page.getByLabel('length Value').fill('10')
await cmdBarSubmitButton.click() await page.getByRole('button', { name: 'Add constraining value' }).click()
activeLinesContent = await page.locator('.cm-activeLine').all() activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`) await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)

View File

@ -91,14 +91,7 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count() await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0) ).toBeGreaterThan(0)
await unconstrainedLocator.click() await unconstrainedLocator.click()
await expect( await page.getByText('Add variable').click()
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await expect(page.locator('.cm-content')).toContainText(expectFinal) await expect(page.locator('.cm-content')).toContainText(expectFinal)
} }
@ -158,14 +151,7 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count() await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0) ).toBeGreaterThan(0)
await unconstrainedLocator.click() await unconstrainedLocator.click()
await expect( await page.getByText('Add variable').click()
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
expectAfterUnconstrained expectAfterUnconstrained
) )

View File

@ -1,9 +1,20 @@
import type { ForgeConfig } from '@electron-forge/shared-types' import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite' import { VitePlugin } from '@electron-forge/plugin-vite'
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
import { FusesPlugin } from '@electron-forge/plugin-fuses' import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses' import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path' import path from 'path'
interface ExtendedMakerWixConfig extends MakerWixConfig {
// see https://github.com/electron/forge/issues/3673
// this is an undocumented property of electron-wix-msi
associateExtensions?: string
}
const rootDir = process.cwd() const rootDir = process.cwd()
const config: ForgeConfig = { const config: ForgeConfig = {
@ -28,7 +39,26 @@ const config: ForgeConfig = {
extendInfo: 'Info.plist', // Information for file associations. extendInfo: 'Info.plist', // Information for file associations.
}, },
rebuildConfig: {}, rebuildConfig: {},
makers: [], makers: [
new MakerSquirrel({
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
}),
new MakerWix({
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
associateExtensions: 'kcl',
} as ExtendedMakerWixConfig),
new MakerZIP({}, ['darwin']),
new MakerRpm({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
new MakerDeb({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
],
plugins: [ plugins: [
new VitePlugin({ new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.

View File

@ -39,6 +39,7 @@
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "6.3.0", "electron-updater": "6.3.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
@ -68,7 +69,7 @@
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"scripts": { "scripts": {
"start": "vite --port=3000 --host=0.0.0.0", "start": "vite",
"start:prod": "vite preview --port=3000", "start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000", "serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build", "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
@ -80,7 +81,6 @@
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver:stop": "kill-port 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
@ -95,14 +95,14 @@
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild", "postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev", "make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start", "tron:start": "electron-forge start",
"tron:package": "electron-forge package", "tron:package": "electron-forge package",
"tron:make": "electron-forge make",
"tron:publish": "electron-forge publish",
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron", "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts", "tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package": "electron-builder --config electron-builder.yml", "tronb:package": "electron-builder --config electron-builder.yml",
@ -145,13 +145,19 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "7.4.0", "@electron-forge/cli": "^7.4.0",
"@electron-forge/plugin-fuses": "7.4.0", "@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/plugin-vite": "7.4.0", "@electron-forge/maker-rpm": "^7.4.0",
"@electron/fuses": "1.8.0", "@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-wix": "^7.5.0",
"@electron-forge/maker-zip": "^7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/rebuild": "^3.6.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
@ -164,7 +170,7 @@
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.2.25",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
@ -178,9 +184,9 @@
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"electron": "32.1.2", "electron": "^32.1.2",
"electron-builder": "24.13.3", "electron-builder": "^24.13.3",
"electron-notarize": "1.2.2", "electron-notarize": "^1.2.2",
"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",
@ -201,6 +207,7 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.6", "vite": "^5.4.6",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",

View File

@ -1,11 +0,0 @@
#!/bin/bash
base_dir="/releases/modeling-app"
if [[ $1 = "--nightly" ]]; then
base_dir="/releases/modeling-app/nightly"
fi
echo "Invalidating json and yml files at $base_dir in the download bucket"
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/last_download.json" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest-linux-arm64.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest.yml" --async

View File

@ -3,7 +3,7 @@ import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData, useLocation, useNavigate } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -22,7 +22,14 @@ import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu' import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { homeDefaultStatusBarItems } from 'components/statusBar/homeDefaultStatusBarItems'
import { StatusBar } from 'components/StatusBar'
import { useModelStateStatus } from 'components/ModelStateIndicator'
import { useNetworkHealthStatus } from 'components/NetworkHealthIndicator'
import { useModelingContext } from 'hooks/useModelingContext'
import { xStateValueToString } from 'lib/xStateValueToString'
import { maybeWriteToDisk } from 'lib/telemetry' import { maybeWriteToDisk } from 'lib/telemetry'
import { useNetworkMachineStatus } from 'components/NetworkMachineIndicator'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
.catch(() => {}) .catch(() => {})
@ -31,11 +38,10 @@ export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
useRefreshSettings(PATHS.FILE + 'SETTINGS') useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext() const { onProjectOpen } = useLspContext()
// We need the ref for the outermost div so we can screenshot the app for const { state: modelingState, streamRef } = useModelingContext()
// the coredump.
const ref = useRef<HTMLDivElement>(null)
const projectName = project?.name || null const projectName = project?.name || null
const projectPath = project?.path || null const projectPath = project?.path || null
@ -77,21 +83,44 @@ export function App() {
useEngineConnectionSubscriptions() useEngineConnectionSubscriptions()
return ( return (
<div className="relative h-full flex flex-col" ref={ref}> <div className="h-screen flex flex-col overflow-hidden select-none">
<AppHeader <div className="relative flex flex-1 flex-col" ref={streamRef}>
className={'transition-opacity transition-duration-75 ' + paneOpacity} <AppHeader
project={{ project, file }} className={'transition-opacity transition-duration-75 ' + paneOpacity}
enableMenu={true} project={{ project, file }}
enableMenu={true}
/>
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<Stream />
{/* <CamToggle /> */}
<LowerRightControls coreDumpManager={coreDumpManager}>
<UnitsMenu />
<Gizmo />
<CameraProjectionToggle />
</LowerRightControls>
</div>
<StatusBar
globalItems={[
useNetworkHealthStatus(),
useNetworkMachineStatus(),
...homeDefaultStatusBarItems({ coreDumpManager, location }),
]}
localItems={[
{
id: 'modeling-state',
element: 'text',
label:
modelingState.value instanceof Object
? xStateValueToString(modelingState.value) ?? ''
: modelingState.value,
toolTip: {
children: 'The current state of the modeler',
},
},
useModelStateStatus(),
]}
/> />
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<Stream />
{/* <CamToggle /> */}
<LowerRightControls coreDumpManager={coreDumpManager}>
<UnitsMenu />
<Gizmo />
<CameraProjectionToggle />
</LowerRightControls>
</div> </div>
) )
} }

View File

@ -505,8 +505,7 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom' verticalPosition: 'top' | 'bottom'
}) => { }) => {
const { commandBarSend } = useCommandsContext() const { context, send } = useModelingContext()
const { context } = useModelingContext()
const varNameMap: { const varNameMap: {
[key in ConstrainInfo['type']]: { [key in ConstrainInfo['type']]: {
varName: string varName: string
@ -625,18 +624,11 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => { onClick={toSync(async () => {
if (!isConstrained) { if (!isConstrained) {
commandBarSend({ send({
type: 'Find and select command', type: 'Convert to variable',
data: { data: {
name: 'Constrain with named value', pathToNode,
groupId: 'modeling', variableName: varName,
argDefaultValues: {
currentValue: {
pathToNode,
variableName: varName,
valueText: value,
},
},
}, },
}) })
} else if (isConstrained) { } else if (isConstrained) {

View File

@ -701,7 +701,8 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = _node1.node?.declaration.id?.name || '' const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
kclManager.programMemory.get(variableDeclarationName), kclManager.programMemory.get(variableDeclarationName),
@ -901,9 +902,10 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = _node1.node?.declaration.id?.name || '' const variableDeclarationName =
const startSketchOn = _node1.node?.declaration _node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOnInit = startSketchOn?.init const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [ const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentA'),
@ -911,7 +913,7 @@ export class SceneEntities {
findUniqueName(_ast, 'rectangleSegmentC'), findUniqueName(_ast, 'rectangleSegmentC'),
] ]
startSketchOn.init = createPipeExpression([ startSketchOn[0].init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
@ -941,7 +943,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return Promise.reject(_node) if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declaration.init const sketchInit = _node.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
@ -990,7 +992,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declaration.init const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type !== 'PipeExpression') { if (sketchInit.type !== 'PipeExpression') {
return return
@ -1056,9 +1058,10 @@ export class SceneEntities {
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
// startSketchOn already exists // startSketchOn already exists
const variableDeclarationName = _node1.node?.declaration.id?.name || '' const variableDeclarationName =
const startSketchOn = _node1.node?.declaration _node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOnInit = startSketchOn?.init const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [ const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'), findUniqueName(_ast, 'rectangleSegmentA'),
@ -1066,7 +1069,7 @@ export class SceneEntities {
findUniqueName(_ast, 'rectangleSegmentC'), findUniqueName(_ast, 'rectangleSegmentC'),
] ]
startSketchOn.init = createPipeExpression([ startSketchOn[0].init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
@ -1096,7 +1099,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return Promise.reject(_node) if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declaration.init const sketchInit = _node.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0] const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
@ -1152,7 +1155,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declaration.init const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch( updateCenterRectangleSketch(
@ -1221,11 +1224,12 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = _node1.node?.declaration.id?.name || '' const variableDeclarationName =
const startSketchOn = _node1.node?.declaration _node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOnInit = startSketchOn?.init const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn.init = createPipeExpression([ startSketchOn[0].init = createPipeExpression([
startSketchOnInit, startSketchOnInit,
createCallExpressionStdLib('circle', [ createCallExpressionStdLib('circle', [
createObjectExpression({ createObjectExpression({
@ -1267,7 +1271,7 @@ export class SceneEntities {
) )
let modded = structuredClone(truncatedAst) let modded = structuredClone(truncatedAst)
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node.declaration.init const sketchInit = _node.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0] const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1] const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
@ -1335,7 +1339,7 @@ export class SceneEntities {
'VariableDeclaration' 'VariableDeclaration'
) )
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declaration.init const sketchInit = _node.node?.declarations?.[0]?.init
let modded = structuredClone(_ast) let modded = structuredClone(_ast)
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
@ -2056,7 +2060,7 @@ function prepareTruncatedMemoryAndAst(
'VariableDeclaration' 'VariableDeclaration'
) )
if (err(_node)) return _node if (err(_node)) return _node
const variableDeclarationName = _node.node?.declaration.id?.name || '' const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
variableDeclarationName variableDeclarationName
@ -2081,7 +2085,7 @@ function prepareTruncatedMemoryAndAst(
]) ])
} }
;( ;(
(_ast.body[bodyIndex] as VariableDeclaration).declaration (_ast.body[bodyIndex] as VariableDeclaration).declarations[0]
.init as PipeExpression .init as PipeExpression
).body.push(newSegment) ).body.push(newSegment)
// update source ranges to section we just added. // update source ranges to section we just added.
@ -2092,19 +2096,19 @@ function prepareTruncatedMemoryAndAst(
const updatedSrcRangeAst = pResult.program const updatedSrcRangeAst = pResult.program
const lastPipeItem = ( const lastPipeItem = (
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration (updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration)
.init as PipeExpression .declarations[0].init as PipeExpression
).body.slice(-1)[0] ).body.slice(-1)[0]
;( ;(
(_ast.body[bodyIndex] as VariableDeclaration).declaration (_ast.body[bodyIndex] as VariableDeclaration).declarations[0]
.init as PipeExpression .init as PipeExpression
).body.slice(-1)[0].start = lastPipeItem.start ).body.slice(-1)[0].start = lastPipeItem.start
_ast.end = lastPipeItem.end _ast.end = lastPipeItem.end
const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration> const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration>
varDec.end = lastPipeItem.end varDec.end = lastPipeItem.end
const declarator = varDec.declaration const declarator = varDec.declarations[0]
declarator.end = lastPipeItem.end declarator.end = lastPipeItem.end
const init = declarator.init as Node<PipeExpression> const init = declarator.init as Node<PipeExpression>
init.end = lastPipeItem.end init.end = lastPipeItem.end
@ -2141,7 +2145,7 @@ function prepareTruncatedMemoryAndAst(
if (node.type !== 'VariableDeclaration') { if (node.type !== 'VariableDeclaration') {
continue continue
} }
const name = node.declaration.id.name const name = node.declarations[0].id.name
const memoryItem = programMemory.get(name) const memoryItem = programMemory.get(name)
if (!memoryItem) { if (!memoryItem) {
continue continue

View File

@ -169,11 +169,11 @@ export function useCalc({
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declaration.id?.name === '__result__' a.declarations?.[0]?.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declaration.init resultDeclaration?.declarations?.[0]?.init
const result = execState.memory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)

View File

@ -8,16 +8,11 @@ import { getSystemTheme } from 'lib/theme'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression' import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { varMentions } from 'lib/varCompletionExtension' import { varMentions } from 'lib/varCompletionExtension'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css' import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst' import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor' import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
}) => snapshot?.context
function CommandBarKclInput({ function CommandBarKclInput({
arg, arg,
@ -36,44 +31,12 @@ function CommandBarKclInput({
arg.name arg.name
] as KclCommandValue | undefined ] as KclCommandValue | undefined
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const argMachineContext = useSelector( const defaultValue = (arg.defaultValue as string) || ''
arg.machineActor,
machineContextSelector
)
const defaultValue = useMemo(
() =>
arg.defaultValue
? arg.defaultValue instanceof Function
? arg.defaultValue(commandBarState.context, argMachineContext)
: arg.defaultValue
: '',
[arg.defaultValue, commandBarState.context, argMachineContext]
)
const initialVariableName = useMemo(() => {
// Use the configured variable name if it exists
if (arg.variableName !== undefined) {
return arg.variableName instanceof Function
? arg.variableName(commandBarState.context, argMachineContext)
: arg.variableName
}
// or derive it from the previously set value or the argument name
return previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name
}, [
arg.variableName,
commandBarState.context,
argMachineContext,
arg.name,
previouslySetValue,
])
const [value, setValue] = useState( const [value, setValue] = useState(
previouslySetValue?.valueText || defaultValue || '' previouslySetValue?.valueText || defaultValue || ''
) )
const [createNewVariable, setCreateNewVariable] = useState( const [createNewVariable, setCreateNewVariable] = useState(
(previouslySetValue && 'variableName' in previouslySetValue) || previouslySetValue && 'variableName' in previouslySetValue
arg.createVariableByDefault ||
false
) )
const [canSubmit, setCanSubmit] = useState(true) const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
@ -89,7 +52,10 @@ function CommandBarKclInput({
isNewVariableNameUnique, isNewVariableNameUnique,
} = useCalculateKclExpression({ } = useCalculateKclExpression({
value, value,
initialVariableName, initialVariableName:
previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name,
}) })
const varMentionData: Completion[] = prevVariables.map((v) => ({ const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key, label: v.key,

View File

@ -636,6 +636,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
loading: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.5001 6.25839C11.76 5.76392 10.89 5.5 10 5.5V4.5C11.0878 4.5 12.1512 4.82257 13.0556 5.42692C13.9601 6.03126 14.6651 6.89025 15.0813 7.89524C15.4976 8.90023 15.6065 10.0061 15.3943 11.073C15.1821 12.1399 14.6583 13.1199 13.8891 13.8891C13.1199 14.6583 12.1399 15.1821 11.073 15.3943C10.0061 15.6065 8.90023 15.4976 7.89524 15.0813C6.89025 14.6651 6.03126 13.9601 5.42692 13.0556C4.82257 12.1512 4.5 11.0878 4.5 10H5.5C5.5 10.89 5.76392 11.76 6.25839 12.5001C6.75285 13.2401 7.45566 13.8169 8.27792 14.1575C9.10019 14.4981 10.005 14.5872 10.8779 14.4135C11.7508 14.2399 12.5526 13.8113 13.182 13.182C13.8113 12.5526 14.2399 11.7508 14.4135 10.8779C14.5872 10.005 14.4981 9.10019 14.1575 8.27792C13.8169 7.45566 13.2401 6.75285 12.5001 6.25839Z"
fill="currentColor"
/>
</svg>
),
lockClosed: ( lockClosed: (
<svg <svg
viewBox="0 0 20 20" viewBox="0 0 20 20"

View File

@ -266,7 +266,6 @@ const FileTreeItem = ({
// 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)
onFileOpen(fileOrDir.path, project?.path || null) onFileOpen(fileOrDir.path, project?.path || null)
kclManager.switchedFiles = true
// Open kcl files // Open kcl files
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`) navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)

View File

@ -1,139 +1,14 @@
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { PATHS } from 'lib/paths'
import { NetworkHealthIndicator } from 'components/NetworkHealthIndicator'
import { HelpMenu } from './HelpMenu'
import { Link, useLocation } from 'react-router-dom'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { coreDump } from 'lang/wasm'
import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator'
import { reportRejection } from 'lib/trap'
export function LowerRightControls({ export function LowerRightControls({
children, children,
coreDumpManager,
}: { }: {
children?: React.ReactNode children?: React.ReactNode
coreDumpManager?: CoreDumpManager coreDumpManager?: CoreDumpManager
}) { }) {
const location = useLocation()
const filePath = useAbsoluteFilePath()
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
}) {
event?.preventDefault()
event?.stopPropagation()
if (!coreDumpManager) {
// open default reporting option
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
} else {
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Preparing bug report...',
success: 'Bug report opened in new window',
error: 'Unable to export a core dump. Using default reporting.',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.catch((err: Error) => {
if (err) {
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
}
})
}
}
return ( return (
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none"> <section className="absolute 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">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()}
target="_blank"
rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
>
v{APP_VERSION}
</a>
<a
onClick={reportbug}
href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
target="_blank"
rel="noopener noreferrer"
>
<CustomIcon
name="bug"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<Tooltip position="top" contentClassName="text-xs">
Report a bug
</Tooltip>
</a>
<Link
to={
location.pathname.includes(PATHS.FILE)
? filePath + PATHS.TELEMETRY + '?tab=project'
: PATHS.HOME + PATHS.TELEMETRY
}
data-testid="telemetry-link"
>
<CustomIcon
name="stopwatch"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<span className="sr-only">Telemetry</span>
<Tooltip position="top" contentClassName="text-xs">
Telemetry
</Tooltip>
</Link>
<Link
to={
location.pathname.includes(PATHS.FILE)
? filePath + PATHS.SETTINGS + '?tab=project'
: PATHS.HOME + PATHS.SETTINGS
}
data-testid="settings-link"
>
<CustomIcon
name="settings"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<span className="sr-only">Settings</span>
<Tooltip position="top" contentClassName="text-xs">
Settings
</Tooltip>
</Link>
<NetworkMachineIndicator className={linkOverrideClassName} />
{!location.pathname.startsWith(PATHS.HOME) && (
<NetworkHealthIndicator />
)}
<HelpMenu />
</menu>
</section> </section>
) )
} }

View File

@ -69,7 +69,14 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext() const {
auth,
settings: {
context: {
modeling: { defaultUnit },
},
},
} = useSettingsAuthContext()
const token = auth?.context.token const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
@ -85,6 +92,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = { const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
baseUnit: defaultUnit.current,
apiBaseUrl: VITE_KC_API_BASE_URL, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({

View File

@ -1,6 +1,39 @@
import { useEngineCommands } from './EngineCommands' import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner' import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { StatusBarItemType } from './statusBar/statusBarTypes'
export const useModelStateStatus = (): StatusBarItemType => {
const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type
let icon: StatusBarItemType['icon'] = 'loading'
const baseDataTestId = 'model-state-indicator'
let dataTestId = baseDataTestId
if (lastCommandType === 'receive-reliable') {
icon = 'checkmark'
dataTestId = `${baseDataTestId}-receive-reliable`
} else if (lastCommandType === 'execution-done') {
icon = 'checkmark'
dataTestId = `${baseDataTestId}-execution-done`
} else if (lastCommandType === 'export-done') {
icon = 'checkmark'
dataTestId = `${baseDataTestId}-export-done`
}
return {
id: 'model-state-indicator',
label: '',
icon,
toolTip: {
children: 'Model state indicator',
},
element: 'button',
onClick: () => {},
'data-testid': dataTestId,
}
}
export const ModelStateIndicator = () => { export const ModelStateIndicator = () => {
const [commands] = useEngineCommands() const [commands] = useEngineCommands()

View File

@ -41,10 +41,7 @@ import {
angleBetweenInfo, angleBetweenInfo,
applyConstraintAngleBetween, applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween' } from './Toolbar/SetAngleBetween'
import { import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
applyConstraintAngleLength,
applyConstraintLength,
} from './Toolbar/setAngleLength'
import { import {
canSweepSelection, canSweepSelection,
handleSelectionBatch, handleSelectionBatch,
@ -54,8 +51,6 @@ import {
Selections, Selections,
updateSelections, updateSelections,
canLoftSelection, canLoftSelection,
canRevolveSelection,
canShellSelection,
} from 'lib/selections' } from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
@ -67,15 +62,13 @@ import {
getSketchOrientationDetails, getSketchOrientationDetails,
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { import {
insertNamedConstant, moveValueIntoNewVariablePath,
replaceValueAtNodePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane, sketchOnOffsetPlane,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' import { Program, parse, recast, resultIsOk } from 'lang/wasm'
import { import {
doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
getNodePathFromSourceRange, getNodePathFromSourceRange,
isSingleCursorInPipe, isSingleCursorInPipe,
@ -86,6 +79,7 @@ import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
@ -105,6 +99,7 @@ type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
context: ContextFrom<T> context: ContextFrom<T>
send: Prop<Actor<T>, 'send'> send: Prop<Actor<T>, 'send'>
streamRef: React.RefObject<HTMLDivElement>
} }
export const ModelingMachineContext = createContext( export const ModelingMachineContext = createContext(
@ -576,26 +571,6 @@ export const ModelingMachineProvider = ({
if (err(canSweep)) return false if (err(canSweep)) return false
return canSweep return canSweep
}, },
'has valid revolve selection': ({ context: { selectionRanges } }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time
const hasNoSelection =
selectionRanges.graphSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
if (hasNoSelection) {
// they have no selection, we should enable the button
// so they can select the face through the cmdbar
// BUT only if there's extrudable geometry
return doesSceneHaveSweepableSketch(kclManager.ast)
}
if (!isSketchPipe(selectionRanges)) return false
const canSweep = canRevolveSelection(selectionRanges)
if (err(canSweep)) return false
return canSweep
},
'has valid loft selection': ({ context: { selectionRanges } }) => { 'has valid loft selection': ({ context: { selectionRanges } }) => {
const hasNoSelection = const hasNoSelection =
selectionRanges.graphSelections.length === 0 || selectionRanges.graphSelections.length === 0 ||
@ -611,24 +586,6 @@ export const ModelingMachineProvider = ({
if (err(canLoft)) return false if (err(canLoft)) return false
return canLoft return canLoft
}, },
'has valid shell selection': ({
context: { selectionRanges },
event,
}) => {
const hasNoSelection =
selectionRanges.graphSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
if (hasNoSelection) {
return doesSceneHaveExtrudedSketch(kclManager.ast)
}
const canShell = canShellSelection(selectionRanges)
console.log('canShellSelection', canShellSelection(selectionRanges))
if (err(canShell)) return false
return canShell
},
'has valid selection for deletion': ({ 'has valid selection for deletion': ({
context: { selectionRanges }, context: { selectionRanges },
}) => { }) => {
@ -913,18 +870,12 @@ export const ModelingMachineProvider = ({
} }
} }
), ),
astConstrainLength: fromPromise( 'Get length info': fromPromise(
async ({ async ({ input: { selectionRanges, sketchDetails } }) => {
input: { selectionRanges, sketchDetails, lengthValue }, const { modifiedAst, pathToNodeMap } =
}) => { await applyConstraintAngleLength({
if (!lengthValue) selectionRanges,
return Promise.reject(new Error('No length value')) })
const constraintResult = await applyConstraintLength({
selectionRanges,
length: lengthValue,
})
if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap } = constraintResult
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
@ -1093,88 +1044,38 @@ export const ModelingMachineProvider = ({
} }
} }
), ),
'Apply named value constraint': fromPromise( 'Get convert to variable info': fromPromise(
async ({ input: { selectionRanges, sketchDetails, data } }) => { async ({ input: { selectionRanges, sketchDetails, data } }) => {
if (!sketchDetails) { if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
} const { variableName } = await getVarNameModal({
if (!data) { valueName: data?.variableName || 'var',
return Promise.reject(new Error('No data from command flow')) })
}
let pResult = parse(recast(kclManager.ast)) let pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program let parsed = pResult.program
let result: { const { modifiedAst: _modifiedAst, pathToReplacedNode } =
modifiedAst: Node<Program> moveValueIntoNewVariablePath(
pathToReplaced: PathToNode | null parsed,
} = { kclManager.programMemory,
modifiedAst: parsed, data?.pathToNode || [],
pathToReplaced: null, variableName
}
// If the user provided a constant name,
// we need to insert the named constant
// and then replace the node with the constant's name.
if ('variableName' in data.namedValue) {
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.variableName,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
const parseResultAfterInsertion = parse(
recast(
insertNamedConstant({
node: astAfterReplacement.modifiedAst,
newExpression: data.namedValue,
})
)
) )
if ( pResult = parse(recast(_modifiedAst))
trap(parseResultAfterInsertion) ||
!resultIsOk(parseResultAfterInsertion)
)
return Promise.reject(parseResultAfterInsertion)
result = {
modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced,
}
} else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name,
// just replace the node with the value.
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.valueText,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
// The `replacer` function returns a pathToNode that assumes
// an identifier is also being inserted into the AST, creating an off-by-one error.
// This corrects that error, but TODO we should fix this upstream
// to avoid this kind of error in the future.
astAfterReplacement.pathToReplaced[1][0] =
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
result = astAfterReplacement
}
pResult = parse(recast(result.modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
parsed = pResult.program parsed = pResult.program
if (trap(parsed)) return Promise.reject(parsed) if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Node<Program> parsed = parsed as Node<Program>
if (!result.pathToReplaced) if (!pathToReplacedNode)
return Promise.reject(new Error('No path to replaced node')) return Promise.reject(new Error('No path to replaced node'))
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
result.pathToReplaced || [], pathToReplacedNode || [],
parsed, parsed,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1187,7 +1088,7 @@ export const ModelingMachineProvider = ({
) )
const selection = updateSelections( const selection = updateSelections(
{ 0: result.pathToReplaced }, { 0: pathToReplacedNode },
selectionRanges, selectionRanges,
updatedAst.newAst updatedAst.newAst
) )
@ -1195,7 +1096,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode: result.pathToReplaced, updatedPathToNode: pathToReplacedNode,
} }
} }
), ),
@ -1305,13 +1206,10 @@ export const ModelingMachineProvider = ({
state: modelingState, state: modelingState,
context: modelingState.context, context: modelingState.context,
send: modelingSend, send: modelingSend,
streamRef,
}} }}
> >
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly? {children}
since realistically it won't ever have generic children that isn't app.tsx */}
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
{children}
</div>
</ModelingMachineContext.Provider> </ModelingMachineContext.Provider>
) )
} }

View File

@ -76,7 +76,7 @@ export const ModelingPane = ({
return ( return (
<section <section
{...props} {...props}
aria-label={title && typeof title === 'string' ? title : ''} title={title && typeof title === 'string' ? title : ''}
data-testid={detailsTestId} data-testid={detailsTestId}
id={id} id={id}
className={ className={

View File

@ -40,9 +40,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50"> <Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item> <Menu.Item>
<button <button
onClick={() => { onClick={() => kclManager.format()}
kclManager.format().catch(reportRejection)
}}
className={styles.button} className={styles.button}
> >
<span>Format code</span> <span>Format code</span>

View File

@ -6,6 +6,7 @@ import { useNetworkContext } from '../hooks/useNetworkContext'
import { NetworkHealthState } from '../hooks/useNetworkStatus' import { NetworkHealthState } from '../hooks/useNetworkStatus'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { StatusBarItemType } from './statusBar/statusBarTypes'
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = { export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected', [NetworkHealthState.Ok]: 'Connected',
@ -64,14 +65,28 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
}, },
} }
const overallConnectionStateIcon: Record< const overallConnectionStateIcon = {
NetworkHealthState,
ActionIconProps['icon']
> = {
[NetworkHealthState.Ok]: 'network', [NetworkHealthState.Ok]: 'network',
[NetworkHealthState.Weak]: 'network', [NetworkHealthState.Weak]: 'network',
[NetworkHealthState.Issue]: 'networkCrossedOut', [NetworkHealthState.Issue]: 'networkCrossedOut',
[NetworkHealthState.Disconnected]: 'networkCrossedOut', [NetworkHealthState.Disconnected]: 'networkCrossedOut',
} as const
export const useNetworkHealthStatus = (): StatusBarItemType => {
const { overallState } = useNetworkContext()
return {
id: 'network-health',
label: `Network health (${NETWORK_HEALTH_TEXT[overallState]})`,
hideLabel: true,
element: 'popover',
className: overallConnectionStateColor[overallState].icon,
toolTip: {
children: `Network health (${NETWORK_HEALTH_TEXT[overallState]})`,
},
icon: overallConnectionStateIcon[overallState],
popoverContent: <NetworkHealthPopoverContent />,
}
} }
export const NetworkHealthIndicator = () => { export const NetworkHealthIndicator = () => {
@ -109,81 +124,95 @@ export const NetworkHealthIndicator = () => {
Network health ({NETWORK_HEALTH_TEXT[overallState]}) Network health ({NETWORK_HEALTH_TEXT[overallState]})
</Tooltip> </Tooltip>
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel>
className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm" <NetworkHealthPopoverContent />
data-testid="network-popover"
>
<div
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
>
<h2 className="text-sm font-sans font-normal">Network health</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
>
{NETWORK_HEALTH_TEXT[overallState]}
</p>
</div>
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{Object.keys(steps).map((name) => (
<li
key={name}
className={'flex flex-col px-2 py-4 gap-1 last:mb-0 '}
>
<div className="flex items-center text-left gap-1">
<p className="flex-1">{name}</p>
{internetConnected ? (
<ActionIcon
size="lg"
icon={
hasIssueToIcon[
String(issues[name as ConnectingTypeGroup])
]
}
iconClassName={
hasIssueToIconColors[
String(issues[name as ConnectingTypeGroup])
].icon
}
bgClassName={
'rounded-sm ' +
hasIssueToIconColors[
String(issues[name as ConnectingTypeGroup])
].bg
}
/>
) : (
<ActionIcon
icon={hasIssueToIcon.true}
bgClassName={hasIssueToIconColors.true.bg}
iconClassName={hasIssueToIconColors.true.icon}
/>
)}
</div>
{issues[name as ConnectingTypeGroup] && (
<button
onClick={toSync(async () => {
await navigator.clipboard.writeText(
JSON.stringify(error, null, 2) || ''
)
setHasCopied(true)
setTimeout(() => setHasCopied(false), 5000)
}, reportRejection)}
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
>
{hasCopied ? 'Copied' : 'Copy Error'}
<ActionIcon
size="lg"
icon={hasCopied ? 'clipboardCheckmark' : 'clipboardPlus'}
iconClassName="text-inherit dark:text-inherit"
bgClassName="!bg-transparent"
/>
</button>
)}
</li>
))}
</ul>
</Popover.Panel> </Popover.Panel>
</Popover> </Popover>
) )
} }
const NetworkHealthPopoverContent = () => {
const {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
} = useNetworkContext()
return (
<div
className="absolute left-2 bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"
data-testid="network-popover"
>
<div
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
>
<h2 className="text-sm font-sans font-normal">Network health</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
>
{NETWORK_HEALTH_TEXT[overallState]}
</p>
</div>
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{Object.keys(steps).map((name) => (
<li key={name} className={'flex flex-col px-2 py-4 gap-1 last:mb-0 '}>
<div className="flex items-center text-left gap-1">
<p className="flex-1">{name}</p>
{internetConnected ? (
<ActionIcon
size="lg"
icon={
hasIssueToIcon[String(issues[name as ConnectingTypeGroup])]
}
iconClassName={
hasIssueToIconColors[
String(issues[name as ConnectingTypeGroup])
].icon
}
bgClassName={
'rounded-sm ' +
hasIssueToIconColors[
String(issues[name as ConnectingTypeGroup])
].bg
}
/>
) : (
<ActionIcon
icon={hasIssueToIcon.true}
bgClassName={hasIssueToIconColors.true.bg}
iconClassName={hasIssueToIconColors.true.icon}
/>
)}
</div>
{issues[name as ConnectingTypeGroup] && (
<button
onClick={toSync(async () => {
await navigator.clipboard.writeText(
JSON.stringify(error, null, 2) || ''
)
setHasCopied(true)
setTimeout(() => setHasCopied(false), 5000)
}, reportRejection)}
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
>
{hasCopied ? 'Copied' : 'Copy Error'}
<ActionIcon
size="lg"
icon={hasCopied ? 'clipboardCheckmark' : 'clipboardPlus'}
iconClassName="text-inherit dark:text-inherit"
bgClassName="!bg-transparent"
/>
</button>
)}
</li>
))}
</ul>
</div>
)
}

View File

@ -5,6 +5,7 @@ import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api' import { components } from 'lib/machine-api'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { StatusBarItemType } from './statusBar/statusBarTypes'
export const NetworkMachineIndicator = ({ export const NetworkMachineIndicator = ({
className, className,
@ -27,12 +28,7 @@ export const NetworkMachineIndicator = ({
} }
data-testid="network-machine-toggle" data-testid="network-machine-toggle"
> >
<CustomIcon name="printer3d" className="w-5 h-5" /> <NetworkMachinesIcon machineCount={machineCount} />
{machineCount > 0 && (
<p aria-hidden className="flex items-center justify-center text-xs">
{machineCount}
</p>
)}
<Tooltip position="top-right" wrapperClassName="ui-open:hidden"> <Tooltip position="top-right" wrapperClassName="ui-open:hidden">
Network machines ({machineCount}) {reason && `: ${reason}`} Network machines ({machineCount}) {reason && `: ${reason}`}
</Tooltip> </Tooltip>
@ -41,50 +37,92 @@ export const NetworkMachineIndicator = ({
className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm" className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"
data-testid="network-popover" data-testid="network-popover"
> >
<div className="flex items-center justify-between p-2 rounded-t-sm bg-chalkboard-20 dark:bg-chalkboard-80"> <NetworkMachinesPopoverContent machines={machines} />
<h2 className="text-sm font-sans font-normal">Network machines</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
>
{machineCount}
</p>
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map(
(machine: components['schemas']['MachineInfoResponse']) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
</p>
</li>
)
}
)}
</ul>
)}
</Popover.Panel> </Popover.Panel>
</Popover> </Popover>
) : null ) : null
} }
export const useNetworkMachineStatus = (): StatusBarItemType => {
const {
noMachinesReason,
machines,
machines: { length: machineCount },
} = useContext(MachineManagerContext)
const reason = noMachinesReason()
return {
id: 'network-machines',
label: `Network machines (${machineCount}) ${reason && `: ${reason}`}`,
hideLabel: true,
element: 'popover',
toolTip: {
children: `Network machines (${machineCount}) ${reason && `: ${reason}`}`,
},
icon: 'printer3d',
popoverContent: <NetworkMachinesPopoverContent machines={machines} />,
}
}
function NetworkMachinesIcon({ machineCount }: { machineCount: number }) {
return (
<>
<CustomIcon name="printer3d" className="w-5 h-5" />
{machineCount > 0 && (
<p aria-hidden className="flex items-center justify-center text-xs">
{machineCount}
</p>
)}
</>
)
}
function NetworkMachinesPopoverContent({ machines }: { machines: components['schemas']['MachineInfoResponse'][] }) {
return (
<>
<div className="flex items-center justify-between p-2 rounded-t-sm bg-chalkboard-20 dark:bg-chalkboard-80">
<h2 className="text-sm font-sans font-normal">Network machines</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
>
{machines.length}
</p>
</div>
{machines.length > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map(
(machine: components['schemas']['MachineInfoResponse']) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
</p>
</li>
)
}
)}
</ul>
)}
</>
)
}

View File

@ -10,7 +10,7 @@ import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
@ -68,7 +68,8 @@ function AppLogoLink({
data-testid="app-logo" data-testid="app-logo"
onClick={() => { onClick={() => {
onProjectClose(file || null, project?.path || null, false) onProjectClose(file || null, project?.path || null, false)
kclManager.switchedFiles = true // Clear the scene.
engineCommandManager.clearScene()
}} }}
to={PATHS.HOME} to={PATHS.HOME}
className={wrapperClassName + ' hover:before:brightness-110'} className={wrapperClassName + ' hover:before:brightness-110'}
@ -189,7 +190,8 @@ function ProjectMenuPopover({
className: !isDesktop() ? 'hidden' : '', className: !isDesktop() ? 'hidden' : '',
onClick: () => { onClick: () => {
onProjectClose(file || null, project?.path || null, true) onProjectClose(file || null, project?.path || null, true)
kclManager.switchedFiles = true // Clear the scene.
engineCommandManager.clearScene()
}, },
}, },
].filter( ].filter(

View File

@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
export const AllKeybindingsFields = forwardRef( export const AllKeybindingsFields = forwardRef(
( (
_props: AllKeybindingsFieldsProps, props: AllKeybindingsFieldsProps,
scrollRef: ForwardedRef<HTMLDivElement> scrollRef: ForwardedRef<HTMLDivElement>
) => { ) => {
// This is how we will get the interaction map from the context // This is how we will get the interaction map from the context
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
.map(([category, categoryItems]) => ( .map(([category, categoryItems]) => (
<div className="flex flex-col gap-4 px-2 pr-4"> <div className="flex flex-col gap-4 px-2 pr-4">
<h2 <h2
id={`category-${category.replaceAll(/\s/g, '-')}`} id={`category-${category}`}
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold" className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
> >
{category} {category}

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput' import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings' import { APP_VERSION } from 'lib/appVersion'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
createAndOpenNewTutorialProject, createAndOpenNewTutorialProject,
@ -25,6 +25,7 @@ import { useLspContext } from 'components/LspProvider'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { PACKAGE_NAME } from 'routes/Settings'
interface AllSettingsFieldsProps { interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel searchParamTab: SettingsLevel
@ -246,8 +247,10 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */} to inject the version from package.json */}
App version {APP_VERSION}.{' '} App version {APP_VERSION}.{' '}
<a <a
onClick={openExternalBrowserIfDesktop(getReleaseUrl())} onClick={openExternalBrowserIfDesktop(
href={getReleaseUrl()} `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -269,7 +272,7 @@ export const AllSettingsFields = forwardRef(
, and start a discussion if you don't see it! Your feedback will , and start a discussion if you don't see it! Your feedback will
help us prioritize what to build next. help us prioritize what to build next.
</p> </p>
{!IS_NIGHTLY && ( {PACKAGE_NAME.indexOf('-nightly') === -1 && (
<p className="max-w-2xl mt-6"> <p className="max-w-2xl mt-6">
Want to experience the latest and (hopefully) greatest from our Want to experience the latest and (hopefully) greatest from our
main development branch?{' '} main development branch?{' '}

View File

@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
key={category} key={category}
onClick={() => onClick={() =>
scrollRef.current scrollRef.current
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`) ?.querySelector(`#category-${category}`)
?.scrollIntoView({ ?.scrollIntoView({
block: 'center', block: 'center',
behavior: 'smooth', behavior: 'smooth',

View File

@ -1,5 +1,5 @@
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react' import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths' import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
@ -23,6 +23,7 @@ import {
engineCommandManager, engineCommandManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings' import { settings } from 'lib/settings/initialSettings'
import { import {
@ -54,15 +55,11 @@ type SettingsAuthContextType = {
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
/** // a little hacky for sure, open to changing it
* This variable is used to store the last snapshot of the settings context // this implies that we should only even have one instance of this provider mounted at any one time
* for use outside of React, such as in `wasm.ts`. It is updated every time // but I think that's a safe assumption
* the settings machine changes with `useSelector`. let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts` export const getSettingsState = () => settingsStateRef
*/
export let lastSettingsContextSnapshot:
| ContextFrom<typeof settingsMachine>
| undefined
export const SettingsAuthContext = createContext({} as SettingsAuthContextType) export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -132,11 +129,27 @@ export const SettingsAuthProviderBase = ({
.setTheme(context.app.theme.current) .setTheme(context.app.theme.current)
.catch(reportRejection) .catch(reportRejection)
}, },
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: ({ context }) => { setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme) sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
}, },
setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
hidden: !context.modeling.highlightEdges.current,
},
})
},
toastSuccess: ({ event }) => { toastSuccess: ({ event }) => {
if (!('data' in event)) return if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [ const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -162,27 +175,17 @@ export const SettingsAuthProviderBase = ({
}, },
'Execute AST': ({ context, event }) => { 'Execute AST': ({ context, event }) => {
try { try {
const relevantSetting = (s: typeof settings) => {
return (
s.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current ||
s.modeling.showScaleGrid.current !==
context.modeling.showScaleGrid.current ||
s.modeling?.highlightEdges.current !==
context.modeling.highlightEdges.current
)
}
const allSettingsIncludesUnitChange = const allSettingsIncludesUnitChange =
event.type === 'Set all settings' && event.type === 'Set all settings' &&
relevantSetting(event.settings) event.settings?.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current
const resetSettingsIncludesUnitChange = const resetSettingsIncludesUnitChange =
event.type === 'Reset settings' && relevantSetting(settings) event.type === 'Reset settings' &&
context.modeling.defaultUnit.current !==
settings?.modeling?.defaultUnit?.default
if ( if (
event.type === 'set.modeling.defaultUnit' || event.type === 'set.modeling.defaultUnit' ||
event.type === 'set.modeling.showScaleGrid' ||
event.type === 'set.modeling.highlightEdges' ||
allSettingsIncludesUnitChange || allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
@ -211,10 +214,7 @@ export const SettingsAuthProviderBase = ({
}), }),
{ input: loadedSettings } { input: loadedSettings }
) )
// Any time the actor changes, update the settings state for external use settingsStateRef = settingsState.context
useSelector(settingsActor, (s) => {
lastSettingsContextSnapshot = s.context
})
useEffect(() => { useEffect(() => {
if (!isDesktop()) return if (!isDesktop()) return

View File

@ -0,0 +1,148 @@
import { useEffect } from 'react'
import { ActionButton } from './ActionButton'
import { StatusBarItemType } from './statusBar/statusBarTypes'
import Tooltip, { TooltipProps } from './Tooltip'
import { ActionIcon } from './ActionIcon'
import { Popover } from '@headlessui/react'
export function StatusBar({
globalItems,
localItems,
}: {
globalItems: StatusBarItemType[]
localItems: StatusBarItemType[]
}) {
return (
<footer
id="statusbar"
className="relative z-10 flex justify-between items-center bg-chalkboard-20 dark:bg-chalkboard-90 text-chalkboard-80 dark:text-chalkboard-30 border-t border-t-chalkboard-30 dark:border-t-chalkboard-80"
>
<menu id="statusbar-globals" className="flex items-stretch">
{globalItems.map((item) => (
<StatusBarItem key={item.id} {...item} position="left" />
))}
</menu>
<menu id="statusbar-locals" className="flex items-stretch">
{localItems.map((item) => (
<StatusBarItem key={item.id} {...item} position="right" />
))}
</menu>
</footer>
)
}
function StatusBarItem(
props: StatusBarItemType & { position: 'left' | 'middle' | 'right' }
) {
const defaultClassNames = `px-2 py-1 text-xs text-chalkboard-80 dark:text-chalkboard-30 rounded-none border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-80 focus:bg-chalkboard-30 dark:focus:bg-chalkboard-80 hover:text-chalkboard-100 dark:hover:text-chalkboard-10 focustext-chalkboard-100 dark:focus:text-chalkboard-10 focus:outline-none focus-visible:ring-2 focus:ring-primary focus:ring-opacity-50`
const tooltipPosition: TooltipProps['position'] =
props.position === 'middle' ? 'top' : `top-${props.position}`
switch (props.element) {
case 'button':
return (
<ActionButton
Element="button"
iconStart={
props.icon && {
icon: props.icon,
iconClassName: props.icon === 'loading' ? 'animate-spin' : '',
bgClassName: 'bg-transparent dark:bg-transparent',
}
}
className={defaultClassNames + ' ' + props.className}
data-testid={props['data-testid']}
>
{props.label && (
<span className={props.hideLabel ? 'sr-only' : ''}>
{props.label}
</span>
)}
{props.toolTip && (
<Tooltip {...props.toolTip} position={tooltipPosition} />
)}
</ActionButton>
)
case 'popover':
return (
<Popover className="relative">
<Popover.Button
as={ActionButton}
Element="button"
iconStart={
props.icon && {
icon: props.icon,
iconClassName: props.icon === 'loading' ? 'animate-spin' : '',
bgClassName: 'bg-transparent dark:bg-transparent',
}
}
className={defaultClassNames + ' ' + props.className}
data-testid={props['data-testid']}
>
{props.label && (
<span className={props.hideLabel ? 'sr-only' : ''}>
{props.label}
</span>
)}
{props.toolTip && (
<Tooltip
{...props.toolTip}
wrapperClassName={`${
props.toolTip?.wrapperClassName || ''
} ui-open:hidden`}
position={tooltipPosition}
/>
)}
</Popover.Button>
<Popover.Panel>{props.popoverContent}</Popover.Panel>
</Popover>
)
case 'text':
return (
<div
role="tooltip"
className={defaultClassNames + ' ' + props.className}
>
{props.icon && (
<ActionIcon
icon={props.icon}
iconClassName={props.icon === 'loading' ? 'animate-spin' : ''}
bgClassName="bg-transparent dark:bg-transparent"
/>
)}
{props.label && (
<span className={props.hideLabel ? 'sr-only' : ''}>
{props.label}
</span>
)}
{props.toolTip && (
<Tooltip {...props.toolTip} position={tooltipPosition} />
)}
</div>
)
default:
return (
<ActionButton
Element={props.element}
to={props.href}
iconStart={
props.icon && {
icon: props.icon,
bgClassName: 'bg-transparent dark:bg-transparent',
}
}
className={defaultClassNames + ' ' + props.className}
data-testid={props['data-testid']}
>
{props.label && (
<span className={props.hideLabel ? 'sr-only' : ''}>
{props.label}
</span>
)}
{props.toolTip && (
<Tooltip {...props.toolTip} position={tooltipPosition} />
)}
</ActionButton>
)
}
}

View File

@ -2,7 +2,6 @@ import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings'
export function ToastUpdate({ export function ToastUpdate({
version, version,
@ -33,8 +32,10 @@ export function ToastUpdate({
A new update has downloaded and will be available next time you A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '} start the app. You can view the release notes{' '}
<a <a
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))} onClick={openExternalBrowserIfDesktop(
href={getReleaseUrl(version)} `https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >

View File

@ -22,7 +22,6 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils' import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { KclCommandValue } from 'lib/commandTypes'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -64,57 +63,6 @@ export function angleLengthInfo({
return { enabled, transforms } return { enabled, transforms }
} }
export async function applyConstraintLength({
length,
selectionRanges,
}: {
length: KclCommandValue
selectionRanges: Selections
}) {
const ast = kclManager.ast
const angleLength = angleLengthInfo({ selectionRanges })
if (err(angleLength)) return angleLength
const { transforms } = angleLength
let distanceExpression: Expr = length.valueAst
/**
* To be "constrained", the value must be a binary expression, a named value, or a function call.
* If it has a variable name, we need to insert a variable declaration at the correct index.
*/
if (
'variableName' in length &&
length.variableName &&
length.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(length.insertIndex, 0, length.variableDeclarationAst)
ast.body = newBody
distanceExpression = createIdentifier(length.variableName)
}
if (!isExprBinaryPart(distanceExpression)) {
return new Error('Invalid valueNode, is not a BinaryPart')
}
const retval = transformAstSketchLines({
ast,
selectionRanges,
transformInfos: transforms,
programMemory: kclManager.programMemory,
referenceSegName: '',
forceValueUsedInTransform: distanceExpression,
})
if (err(retval)) return Promise.reject(retval)
const { modifiedAst: _modifiedAst, pathToNodeMap } = retval
return {
modifiedAst: _modifiedAst,
pathToNodeMap,
}
}
export async function applyConstraintAngleLength({ export async function applyConstraintAngleLength({
selectionRanges, selectionRanges,
angleOrLength = 'setLength', angleOrLength = 'setLength',

View File

@ -8,7 +8,7 @@ type LeftOrRight = 'left' | 'right'
type Corner = `${TopOrBottom}-${LeftOrRight}` type Corner = `${TopOrBottom}-${LeftOrRight}`
type TooltipPosition = TopOrBottom | LeftOrRight | Corner type TooltipPosition = TopOrBottom | LeftOrRight | Corner
interface TooltipProps extends React.PropsWithChildren { export interface TooltipProps extends React.PropsWithChildren {
position?: TooltipPosition position?: TooltipPosition
wrapperClassName?: string wrapperClassName?: string
contentClassName?: string contentClassName?: string

View File

@ -41,10 +41,7 @@ export function UnitsMenu() {
close() close()
}} }}
> >
<span className="flex-1">{baseUnitLabels[unit]}</span> {baseUnitLabels[unit]}
{unit === settings.context.modeling.defaultUnit.current && (
<span className="text-chalkboard-60">current</span>
)}
</button> </button>
</li> </li>
))} ))}

View File

@ -0,0 +1,96 @@
import openWindow from 'lib/openWindow'
import { StatusBarItemType } from './statusBarTypes'
import { reportRejection } from 'lib/trap'
import { CoreDumpManager } from 'lib/coredump'
import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm'
import { APP_VERSION } from 'lib/appVersion'
import { Location } from 'react-router-dom'
import { PATHS } from 'lib/paths'
export const homeDefaultStatusBarItems = ({
coreDumpManager,
location,
}: {
coreDumpManager?: CoreDumpManager
location: Location
}): StatusBarItemType[] => [
{
id: 'version',
element: 'externalLink',
label: `v${APP_VERSION}`,
href: `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`,
toolTip: {
children: 'View the release notes on GitHub',
},
},
{
id: 'report-bug',
element: 'button',
icon: 'bug',
label: 'Report a bug',
onClick: (event) => reportBug(event, { coreDumpManager }),
toolTip: {
children: 'Send your current app state to the developers for debugging',
},
},
{
id: 'settings',
element: 'link',
icon: 'settings',
href:
'.' +
PATHS.SETTINGS +
(location.pathname.includes(PATHS.FILE) ? '?tab=project' : ''),
'data-testid': 'settings-link',
label: 'Settings',
toolTip: {
children: 'Settings',
},
},
]
function reportBug(
event: {
preventDefault: () => void
stopPropagation: () => void
},
dependencies: {
coreDumpManager: CoreDumpManager | undefined
}
) {
event?.preventDefault()
event?.stopPropagation()
const { coreDumpManager } = dependencies
if (!coreDumpManager) {
// open default reporting option
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
} else {
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Preparing bug report...',
success: 'Bug report opened in new window',
error: 'Unable to export a core dump. Using default reporting.',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.catch((err: Error) => {
if (err) {
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
}
})
}
}

View File

@ -0,0 +1,28 @@
import { CustomIconName } from 'components/CustomIcon'
import { TooltipProps } from 'components/Tooltip'
export type StatusBarItemType = {
id: string
label: string
icon?: CustomIconName
hideLabel?: boolean
toolTip?: Omit<TooltipProps, 'position'>
className?: string
['data-testid']?: string
} & (
| {
element: 'button'
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
}
| {
element: 'popover'
popoverContent: React.ReactNode
}
| {
element: 'link' | 'externalLink'
href: string
}
| {
element: 'text'
}
)

View File

@ -1,5 +1,7 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client' import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker { export enum LspWorker {
Kcl = 'kcl', Kcl = 'kcl',
Copilot = 'copilot', Copilot = 'copilot',
@ -7,6 +9,7 @@ export enum LspWorker {
export interface KclWorkerOptions { export interface KclWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
baseUnit: UnitLength
apiBaseUrl: string apiBaseUrl: string
} }

View File

@ -17,6 +17,7 @@ import {
KclWorkerOptions, KclWorkerOptions,
CopilotWorkerOptions, CopilotWorkerOptions,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer() const intoServer: IntoServer = new IntoServer()
@ -45,12 +46,14 @@ export async function copilotLspRun(
export async function kclLspRun( export async function kclLspRun(
config: ServerConfig, config: ServerConfig,
engineCommandManager: EngineCommandManager | null,
token: string, token: string,
baseUnit: string,
baseUrl: string baseUrl: string
) { ) {
try { try {
console.log('start kcl lsp') console.log('start kcl lsp')
await kcl_lsp_run(config, null, undefined, token, baseUrl) await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('kcl lsp failed', e) console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -79,7 +82,13 @@ onmessage = function (event: MessageEvent) {
switch (worker) { switch (worker) {
case LspWorker.Kcl: case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions const kclData = eventData as KclWorkerOptions
await kclLspRun(config, kclData.token, kclData.apiBaseUrl) await kclLspRun(
config,
null,
kclData.token,
kclData.baseUnit,
kclData.apiBaseUrl
)
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions

View File

@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes } from 'lang/wasm' import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState' import { useAppState, useAppStream } from 'AppState'
@ -56,6 +56,9 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager) return makeDefaultPlanes(kclManager.engineCommandManager)
}, },
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
}) })
hasSetNonZeroDimensions.current = true hasSetNonZeroDimensions.current = true
} }

View File

@ -24,8 +24,6 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
// Return early if there are no selection ranges for whatever reason
if (!context.selectionRanges) return
const parsed = ast const parsed = ast
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(

View File

@ -317,8 +317,3 @@ code {
#code-mirror-override .cm-editor { #code-mirror-override .cm-editor {
height: 100% !important; height: 100% !important;
} }
/* Can't use #code-mirror-override here as we're outside of this div */
.body-bg .cm-diagnosticAction {
@apply bg-primary;
}

View File

@ -12,7 +12,6 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { import {
CallExpression, CallExpression,
clearSceneAndBustCache,
emptyExecState, emptyExecState,
ExecState, ExecState,
initPromise, initPromise,
@ -61,7 +60,6 @@ export class KclManager {
private _executeIsStale: ExecuteArgs | null = null private _executeIsStale: ExecuteArgs | null = null
private _wasmInitFailed = true private _wasmInitFailed = true
private _hasErrors = false private _hasErrors = false
private _switchedFiles = false
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
@ -81,10 +79,6 @@ export class KclManager {
this._astCallBack(ast) this._astCallBack(ast)
} }
set switchedFiles(switchedFiles: boolean) {
this._switchedFiles = switchedFiles
}
get programMemory() { get programMemory() {
return this._programMemory return this._programMemory
} }
@ -172,12 +166,8 @@ export class KclManager {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(async () => { this.ensureWasmInit().then(() => {
await this.safeParse(codeManager.code).then((ast) => { this.ast = this.safeParse(codeManager.code) || this.ast
if (ast) {
this.ast = ast
}
})
}) })
} }
@ -221,25 +211,7 @@ export class KclManager {
} }
} }
// (jess) I'm not in love with this, but it ensures we clear the scene and safeParse(code: string): Node<Program> | null {
// bust the cache on
// errors from parsing when opening new files.
// Why not just clear the cache on all parse errors, you ask? well its actually
// really nice to keep the cache on parse errors within the same file, and
// only bust on engine errors esp if they take a long time to execute and
// you hit the wrong key!
private async checkIfSwitchedFilesShouldClear() {
// If we were switching files and we hit an error on parse we need to bust
// the cache and clear the scene.
if (this._hasErrors && this._switchedFiles) {
await clearSceneAndBustCache(this.engineCommandManager)
} else if (this._switchedFiles) {
// Reset the switched files boolean.
this._switchedFiles = false
}
}
async safeParse(code: string): Promise<Node<Program> | null> {
const result = parse(code) const result = parse(code)
this.diagnostics = [] this.diagnostics = []
this._hasErrors = false this._hasErrors = false
@ -248,8 +220,6 @@ export class KclManager {
const kclerror: KCLError = result as KCLError const kclerror: KCLError = result as KCLError
this.diagnostics = kclErrorsToDiagnostics([kclerror]) this.diagnostics = kclErrorsToDiagnostics([kclerror])
this._hasErrors = true this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null return null
} }
@ -258,7 +228,6 @@ export class KclManager {
if (result.errors.length > 0) { if (result.errors.length > 0) {
this._hasErrors = true this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null return null
} }
@ -384,7 +353,7 @@ export class KclManager {
console.error(newCode) console.error(newCode)
return return
} }
const newAst = await this.safeParse(newCode) const newAst = this.safeParse(newCode)
if (!newAst) { if (!newAst) {
this.clearAst() this.clearAst()
return return
@ -439,7 +408,7 @@ export class KclManager {
}) })
} }
async executeCode(zoomToFit?: boolean): Promise<void> { async executeCode(zoomToFit?: boolean): Promise<void> {
const ast = await this.safeParse(codeManager.code) const ast = this.safeParse(codeManager.code)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
@ -447,9 +416,9 @@ export class KclManager {
this.ast = { ...ast } this.ast = { ...ast }
return this.executeAst({ zoomToFit }) return this.executeAst({ zoomToFit })
} }
async format() { format() {
const originalCode = codeManager.code const originalCode = codeManager.code
const ast = await this.safeParse(originalCode) const ast = this.safeParse(originalCode)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
@ -489,7 +458,7 @@ export class KclManager {
const newCode = recast(ast) const newCode = recast(ast)
if (err(newCode)) return Promise.reject(newCode) if (err(newCode)) return Promise.reject(newCode)
const astWithUpdatedSource = await this.safeParse(newCode) const astWithUpdatedSource = this.safeParse(newCode)
if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast')) if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast'))
let returnVal: Selections | undefined = undefined let returnVal: Selections | undefined = undefined

View File

@ -60,7 +60,8 @@ const b1 = cube([0,0], 10)`
expect(nodePath).toEqual([ expect(nodePath).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['params', 'FunctionExpression'], ['params', 'FunctionExpression'],
[0, 'index'], [0, 'index'],
@ -95,12 +96,14 @@ const b1 = cube([0,0], 10)`
expect(nodePath).toEqual([ expect(nodePath).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'FunctionExpression'], ['body', 'FunctionExpression'],
['body', 'FunctionExpression'], ['body', 'FunctionExpression'],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[2, 'index'], [2, 'index'],

View File

@ -82,11 +82,11 @@ describe('Testing createVariableDeclaration', () => {
it('should create a variable declaration', () => { it('should create a variable declaration', () => {
const result = createVariableDeclaration('myVar', createLiteral(5)) const result = createVariableDeclaration('myVar', createLiteral(5))
expect(result.type).toBe('VariableDeclaration') expect(result.type).toBe('VariableDeclaration')
expect(result.declaration.type).toBe('VariableDeclarator') expect(result.declarations[0].type).toBe('VariableDeclarator')
expect(result.declaration.id.type).toBe('Identifier') expect(result.declarations[0].id.type).toBe('Identifier')
expect(result.declaration.id.name).toBe('myVar') expect(result.declarations[0].id.name).toBe('myVar')
expect(result.declaration.init.type).toBe('Literal') expect(result.declarations[0].init.type).toBe('Literal')
expect((result.declaration.init as any).value).toBe(5) expect((result.declarations[0].init as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeExpression', () => { describe('Testing createPipeExpression', () => {

View File

@ -45,7 +45,6 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine' import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -67,7 +66,8 @@ export function startSketchOnDefault(
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
@ -94,7 +94,7 @@ export function addStartProfileAt(
return new Error('variableDeclaration.init.type !== PipeExpression') return new Error('variableDeclaration.init.type !== PipeExpression')
} }
const _node = { ...node } const _node = { ...node }
const init = variableDeclaration.declaration.init const init = variableDeclaration.declarations[0].init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [ const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([ createArrayExpression([
createLiteral(roundOff(at[0])), createLiteral(roundOff(at[0])),
@ -105,7 +105,7 @@ export function addStartProfileAt(
if (init.type === 'PipeExpression') { if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt) init.body.splice(1, 0, startProfileAt)
} else { } else {
variableDeclaration.declaration.init = createPipeExpression([ variableDeclaration.declarations[0].init = createPipeExpression([
init, init,
startProfileAt, startProfileAt,
]) ])
@ -149,7 +149,8 @@ export function addSketchTo(
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
if (axis !== 'xy') { if (axis !== 'xy') {
@ -332,7 +333,8 @@ export function extrudeSketch(
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
['body', ''], ['body', ''],
[sketchIndexInBody + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -362,7 +364,8 @@ export function loftSketches(
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
['body', ''], ['body', ''],
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -457,7 +460,8 @@ export function revolveSketch(
const pathToRevolveArg: PathToNode = [ const pathToRevolveArg: PathToNode = [
['body', ''], ['body', ''],
[sketchIndexInBody + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -543,7 +547,8 @@ export function sketchOnExtrudedFace(
const newpathToNode: PathToNode = [ const newpathToNode: PathToNode = [
['body', ''], ['body', ''],
[expressionIndex + 1, 'index'], [expressionIndex + 1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
] ]
@ -580,7 +585,8 @@ export function addOffsetPlane({
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
['body', ''], ['body', ''],
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],
@ -591,25 +597,6 @@ export function addOffsetPlane({
} }
} }
/**
* Return a modified clone of an AST with a named constant inserted into the body
*/
export function insertNamedConstant({
node,
newExpression,
}: {
node: Node<Program>
newExpression: KclExpressionWithVariable
}): Node<Program> {
const ast = structuredClone(node)
ast.body.splice(
newExpression.insertIndex,
0,
newExpression.variableDeclarationAst
)
return ast
}
/** /**
* Modify the AST to create a new sketch using the variable declaration * Modify the AST to create a new sketch using the variable declaration
* of an offset plane. The new sketch just has to come after the offset * of an offset plane. The new sketch just has to come after the offset
@ -836,15 +823,17 @@ export function createVariableDeclaration(
end: 0, end: 0,
moduleId: 0, moduleId: 0,
declaration: { declarations: [
type: 'VariableDeclarator', {
start: 0, type: 'VariableDeclarator',
end: 0, start: 0,
moduleId: 0, end: 0,
moduleId: 0,
id: createIdentifier(varName), id: createIdentifier(varName),
init, init,
}, },
],
visibility, visibility,
kind, kind,
} }
@ -953,31 +942,6 @@ export function giveSketchFnCallTag(
} }
} }
/**
* Replace a
*/
export function replaceValueAtNodePath({
ast,
pathToNode,
newExpressionString,
}: {
ast: Node<Program>
pathToNode: PathToNode
newExpressionString: string
}) {
const replaceCheckResult = isNodeSafeToReplacePath(ast, pathToNode)
if (err(replaceCheckResult)) {
return replaceCheckResult
}
const { isSafe, value, replacer } = replaceCheckResult
if (!isSafe || value.type === 'Identifier') {
return new Error('Not safe to replace')
}
return replacer(ast, newExpressionString)
}
export function moveValueIntoNewVariablePath( export function moveValueIntoNewVariablePath(
ast: Node<Program>, ast: Node<Program>,
programMemory: ProgramMemory, programMemory: ProgramMemory,
@ -1156,7 +1120,7 @@ export async function deleteFromSelection(
traverse(astClone, { traverse(astClone, {
enter: (node, path) => { enter: (node, path) => {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const dec = node.declaration const dec = node.declarations[0]
if ( if (
dec.init.type === 'CallExpression' && dec.init.type === 'CallExpression' &&
(dec.init.callee.name === 'extrude' || (dec.init.callee.name === 'extrude' ||
@ -1191,7 +1155,7 @@ export async function deleteFromSelection(
enter: (node, path) => { enter: (node, path) => {
;(async () => { ;(async () => {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
currentVariableName = node.declaration.id.name currentVariableName = node.declarations[0].id.name
} }
if ( if (
// match startSketchOn(${extrudeNameToDelete}) // match startSketchOn(${extrudeNameToDelete})

View File

@ -22,7 +22,7 @@ import {
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst' import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_DEV_TOKEN } from 'env'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
@ -40,6 +40,7 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: () => { callbackOnEngineLiteConnect: () => {
resolve(true) resolve(true)
}, },
@ -117,8 +118,13 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
true, true,
] ]
const selection: Selection = { const selection: Selections = {
codeRef: codeRefFromRange(segmentRange, ast), graphSelections: [
{
codeRef: codeRefFromRange(segmentRange, ast),
},
],
otherSelections: [],
} }
// executeAst and artifactGraph // executeAst and artifactGraph

View File

@ -29,7 +29,7 @@ import {
sketchLineHelperMap, sketchLineHelperMap,
} from '../std/sketch' } from '../std/sketch'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import {
Artifact, Artifact,
@ -99,9 +99,14 @@ export function modifyAstWithEdgeTreatmentAndTag(
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selection of selections.graphSelections) { for (const selection of selections.graphSelections) {
const singleSelection = {
graphSelections: [selection],
otherSelections: [],
}
const result = getPathToExtrudeForSegmentSelection( const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude, clonedAstForGetExtrude,
selection, singleSelection,
artifactGraph artifactGraph
) )
if (err(result)) return result if (err(result)) return result
@ -254,12 +259,12 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection( export function getPathToExtrudeForSegmentSelection(
ast: Program, ast: Program,
selection: Selection, selection: Selections,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error { ): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange( const pathToSegmentNode = getNodePathFromSourceRange(
ast, ast,
selection.codeRef?.range selection.graphSelections[0]?.codeRef?.range
) )
const varDecNode = getNodeFromPath<VariableDeclaration>( const varDecNode = getNodeFromPath<VariableDeclaration>(
@ -268,7 +273,7 @@ export function getPathToExtrudeForSegmentSelection(
'VariableDeclaration' 'VariableDeclaration'
) )
if (err(varDecNode)) return varDecNode if (err(varDecNode)) return varDecNode
const sketchVar = varDecNode.node.declaration.id.name const sketchVar = varDecNode.node.declarations[0].id.name
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
kclManager.programMemory.get(sketchVar), kclManager.programMemory.get(sketchVar),
@ -303,7 +308,7 @@ async function updateAstAndFocus(
} }
} }
export function mutateAstWithTagForSketchSegment( function mutateAstWithTagForSketchSegment(
astClone: Node<Program>, astClone: Node<Program>,
pathToSegmentNode: PathToNode pathToSegmentNode: PathToNode
): { modifiedAst: Program; tag: string } | Error { ): { modifiedAst: Program; tag: string } | Error {
@ -335,7 +340,7 @@ export function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag } return { modifiedAst: astClone, tag }
} }
export function getEdgeTagCall( function getEdgeTagCall(
tag: string, tag: string,
artifact: Artifact artifact: Artifact
): Node<Identifier | CallExpression> { ): Node<Identifier | CallExpression> {
@ -362,7 +367,7 @@ function locateExtrudeDeclarator(
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
const { node: extrudeVarDecl } = nodeOfExtrudeCall const { node: extrudeVarDecl } = nodeOfExtrudeCall
const extrudeDeclarator = extrudeVarDecl.declaration const extrudeDeclarator = extrudeVarDecl.declarations[0]
if (!extrudeDeclarator) { if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.') return new Error('Extrude Declarator not found.')
} }

View File

@ -1,154 +0,0 @@
import { err } from 'lib/trap'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import {
Program,
PathToNode,
Expr,
CallExpression,
PipeExpression,
VariableDeclarator,
} from 'lang/wasm'
import { Selections } from 'lib/selections'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
createLiteral,
createCallExpressionStdLib,
createObjectExpression,
createIdentifier,
createPipeExpression,
findUniqueName,
createVariableDeclaration,
} from 'lang/modifyAst'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import {
mutateAstWithTagForSketchSegment,
getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment'
export function revolveSketch(
ast: Node<Program>,
pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4),
axis: Selections
):
| {
modifiedAst: Node<Program>
pathToSketchNode: PathToNode
pathToRevolveArg: PathToNode
}
| Error {
const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode
// testing code
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
axis.graphSelections[0]?.codeRef.range
)
const lineNode = getNodeFromPath<CallExpression>(
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
// TODO Kevin: What if |> close(%)?
// TODO Kevin: What if opposite edge
// TODO Kevin: What if the edge isn't planar to the sketch?
// TODO Kevin: add a tag.
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
/* Original Code */
const { node: sketchExpression } = sketchNode
// determine if sketchExpression is in a pipeExpression or not
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
clonedAst,
pathToSketchNode,
'PipeExpression'
)
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
const { node: sketchPipeExpression } = sketchPipeExpressionNode
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst,
pathToSketchNode,
'VariableDeclarator'
)
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const {
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const axisSelection = axis?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Axis selection is missing.')
const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({
angle: angle,
axis: getEdgeTagCall(tag, axisSelection),
}),
createIdentifier(sketchVariableDeclarator.id.name),
])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToRevolveArg: PathToNode = [
['body', ''],
[sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
pathToRevolveArg,
}
}

View File

@ -1,123 +0,0 @@
import { ArtifactGraph } from 'lang/std/artifactGraph'
import { Selections } from 'lib/selections'
import { Expr } from 'wasm-lib/kcl/bindings/Expr'
import { Program } from 'wasm-lib/kcl/bindings/Program'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { PathToNode, VariableDeclarator } from 'lang/wasm'
import {
getPathToExtrudeForSegmentSelection,
mutateAstWithTagForSketchSegment,
} from './addEdgeTreatment'
import { getNodeFromPath } from 'lang/queryAst'
import { err } from 'lib/trap'
import {
createLiteral,
createIdentifier,
findUniqueName,
createCallExpressionStdLib,
createObjectExpression,
createArrayExpression,
createVariableDeclaration,
} from 'lang/modifyAst'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
export function addShell({
node,
selection,
artifactGraph,
thickness,
}: {
node: Node<Program>
selection: Selections
artifactGraph: ArtifactGraph
thickness: Expr
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
// Look up the corresponding extrude
const clonedAstForGetExtrude = structuredClone(modifiedAst)
const expressions: Expr[] = []
let pathToExtrudeNode: PathToNode | undefined = undefined
for (const graphSelection of selection.graphSelections) {
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
artifactGraph
)
if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude")
}
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
// Get the sketch ref from the selection
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
// We must find a technique for these situations that is robust to intermediate declarations
const sketchNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
graphSelection.codeRef.pathToNode,
'VariableDeclarator'
)
if (err(sketchNode)) {
return sketchNode
}
const selectedArtifact = graphSelection.artifact
if (!selectedArtifact) {
return new Error('Bad artifact')
}
// Check on the selection, and handle the wall vs cap casees
let expr: Expr
if (selectedArtifact.type === 'cap') {
expr = createLiteral(selectedArtifact.subType)
} else if (selectedArtifact.type === 'wall') {
const tagResult = mutateAstWithTagForSketchSegment(
modifiedAst,
extrudeLookupResult.pathToSegmentNode
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
expr = createIdentifier(tag)
} else {
continue
}
expressions.push(expr)
}
if (!pathToExtrudeNode) return new Error('No extrude found')
const extrudeNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
pathToExtrudeNode,
'VariableDeclarator'
)
if (err(extrudeNode)) {
return extrudeNode
}
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
const shell = createCallExpressionStdLib('shell', [
createObjectExpression({
faces: createArrayExpression(expressions),
thickness,
}),
createIdentifier(extrudeNode.node.id.name),
])
const declaration = createVariableDeclaration(name, shell)
// TODO: check if we should append at the end like here or right after the extrude
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}

View File

@ -17,7 +17,6 @@ import {
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
traverse, traverse,
getNodeFromPath, getNodeFromPath,
doesSceneHaveExtrudedSketch,
} from './queryAst' } from './queryAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { import {
@ -231,7 +230,8 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[2, 'index'], [2, 'index'],
@ -250,7 +250,8 @@ describe('testing getNodePathFromSourceRange', () => {
const expected = [ const expected = [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['body', 'PipeExpression'], ['body', 'PipeExpression'],
[3, 'index'], [3, 'index'],
@ -292,7 +293,8 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[1, 'index'], [1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['cond', 'IfExpression'], ['cond', 'IfExpression'],
['left', 'BinaryExpression'], ['left', 'BinaryExpression'],
@ -322,7 +324,8 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[1, 'index'], [1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', ''], ['init', ''],
['then_val', 'IfExpression'], ['then_val', 'IfExpression'],
['body', 'IfExpression'], ['body', 'IfExpression'],
@ -350,8 +353,7 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['selector', 'ImportStatement'], ['items', 'ImportStatement'],
['items', 'ImportSelector'],
[1, 'index'], [1, 'index'],
['name', 'ImportItem'], ['name', 'ImportItem'],
]) ])
@ -655,38 +657,6 @@ extrude001 = extrude(10, sketch001)
}) })
}) })
describe('Testing doesSceneHaveExtrudedSketch', () => {
it('finds extruded sketch as variable', async () => {
const exampleCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
extrude001 = extrude(1, sketch001)
`
const ast = assertParse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy()
})
it('finds extruded sketch in pipe', async () => {
const exampleCode = `extrude001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
|> extrude(1, %)
`
const ast = assertParse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy()
})
it('finds no extrusion with sketch only', async () => {
const exampleCode = `extrude001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
`
const ast = assertParse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeFalsy()
})
})
describe('Testing traverse and pathToNode', () => { describe('Testing traverse and pathToNode', () => {
it.each([ it.each([
['basic', '2.73'], ['basic', '2.73'],

View File

@ -259,26 +259,34 @@ function moreNodePathFromSourceRange(
return moreNodePathFromSourceRange(expression, sourceRange, path) return moreNodePathFromSourceRange(expression, sourceRange, path)
} }
if (_node.type === 'VariableDeclaration' && isInRange) { if (_node.type === 'VariableDeclaration' && isInRange) {
const declaration = _node.declaration const declarations = _node.declarations
if (declaration.start <= start && declaration.end >= end) { for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
path.push(['declaration', 'VariableDeclaration']) const declaration = declarations[decIndex]
const init = declaration.init if (declaration.start <= start && declaration.end >= end) {
if (init.start <= start && init.end >= end) { path.push(['declarations', 'VariableDeclaration'])
path.push(['init', '']) path.push([decIndex, 'index'])
return moreNodePathFromSourceRange(init, sourceRange, path) const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
} }
} }
} }
if (_node.type === 'VariableDeclaration' && isInRange) { if (_node.type === 'VariableDeclaration' && isInRange) {
const declaration = _node.declaration const declarations = _node.declarations
if (declaration.start <= start && declaration.end >= end) { for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
const init = declaration.init const declaration = declarations[decIndex]
if (init.start <= start && init.end >= end) { if (declaration.start <= start && declaration.end >= end) {
path.push(['declaration', 'VariableDeclaration']) const init = declaration.init
path.push(['init', '']) if (init.start <= start && init.end >= end) {
return moreNodePathFromSourceRange(init, sourceRange, path) path.push(['declarations', 'VariableDeclaration'])
path.push([decIndex, 'index'])
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
} }
} }
return path return path
@ -372,31 +380,24 @@ function moreNodePathFromSourceRange(
} }
if (_node.type === 'ImportStatement' && isInRange) { if (_node.type === 'ImportStatement' && isInRange) {
if (_node.selector && _node.selector.type === 'List') { const { items } = _node
path.push(['selector', 'ImportStatement']) for (let i = 0; i < items.length; i++) {
const { items } = _node.selector const item = items[i]
for (let i = 0; i < items.length; i++) { if (item.start <= start && item.end >= end) {
const item = items[i] path.push(['items', 'ImportStatement'])
if (item.start <= start && item.end >= end) { path.push([i, 'index'])
path.push(['items', 'ImportSelector']) if (item.name.start <= start && item.name.end >= end) {
path.push([i, 'index']) path.push(['name', 'ImportItem'])
if (item.name.start <= start && item.name.end >= end) {
path.push(['name', 'ImportItem'])
return path
}
if (
item.alias &&
item.alias.start <= start &&
item.alias.end >= end
) {
path.push(['alias', 'ImportItem'])
return path
}
return path return path
} }
if (item.alias && item.alias.start <= start && item.alias.end >= end) {
path.push(['alias', 'ImportItem'])
return path
}
return path
} }
return path
} }
return path
} }
console.error('not implemented: ' + node.type) console.error('not implemented: ' + node.type)
@ -450,10 +451,13 @@ export function traverse(
traverse(node, option, pathToNode) traverse(node, option, pathToNode)
if (_node.type === 'VariableDeclaration') { if (_node.type === 'VariableDeclaration') {
_traverse(_node.declaration, [ _node.declarations.forEach((declaration, index) =>
...pathToNode, _traverse(declaration, [
['declaration', 'VariableDeclaration'], ...pathToNode,
]) ['declarations', 'VariableDeclaration'],
[index, 'index'],
])
)
} else if (_node.type === 'VariableDeclarator') { } else if (_node.type === 'VariableDeclarator') {
_traverse(_node.init, [...pathToNode, ['init', '']]) _traverse(_node.init, [...pathToNode, ['init', '']])
} else if (_node.type === 'PipeExpression') { } else if (_node.type === 'PipeExpression') {
@ -563,7 +567,7 @@ export function findAllPreviousVariablesPath(
const variables: PrevVariable<any>[] = [] const variables: PrevVariable<any>[] = []
bodyItems?.forEach?.((item) => { bodyItems?.forEach?.((item) => {
if (item.type !== 'VariableDeclaration' || item.end > startRange) return if (item.type !== 'VariableDeclaration' || item.end > startRange) return
const varName = item.declaration.id.name const varName = item.declarations[0].id.name
const varValue = programMemory?.get(varName) const varValue = programMemory?.get(varName)
if (!varValue || typeof varValue?.value !== type) return if (!varValue || typeof varValue?.value !== type) return
variables.push({ variables.push({
@ -757,7 +761,7 @@ export function isLinesParallelAndConstrained(
const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration') const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration')
if (err(_varDec)) return _varDec if (err(_varDec)) return _varDec
const varDec = _varDec.node const varDec = _varDec.node
const varName = (varDec as VariableDeclaration)?.declaration.id?.name const varName = (varDec as VariableDeclaration)?.declarations[0]?.id?.name
const sg = sketchFromKclValue(programMemory?.get(varName), varName) const sg = sketchFromKclValue(programMemory?.get(varName), varName)
if (err(sg)) return sg if (err(sg)) return sg
const _primarySegment = getSketchSegmentFromSourceRange( const _primarySegment = getSketchSegmentFromSourceRange(
@ -877,7 +881,7 @@ export function hasExtrudeSketch({
} }
const varDec = varDecMeta.node const varDec = varDecMeta.node
if (varDec.type !== 'VariableDeclaration') return false if (varDec.type !== 'VariableDeclaration') return false
const varName = varDec.declaration.id.name const varName = varDec.declarations[0].id.name
const varValue = programMemory?.get(varName) const varValue = programMemory?.get(varName)
return ( return (
varValue?.type === 'Solid' || varValue?.type === 'Solid' ||
@ -1064,35 +1068,6 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
return Object.keys(theMap).length >= count return Object.keys(theMap).length >= count
} }
export function doesSceneHaveExtrudedSketch(ast: Node<Program>) {
const theMap: any = {}
traverse(ast as any, {
enter(node) {
if (
node.type === 'VariableDeclarator' &&
node.init?.type === 'PipeExpression'
) {
for (const pipe of node.init.body) {
if (
pipe.type === 'CallExpression' &&
pipe.callee.name === 'extrude'
) {
theMap[node.id.name] = true
break
}
}
} else if (
node.type === 'CallExpression' &&
node.callee.name === 'extrude' &&
node.arguments[1]?.type === 'Identifier'
) {
theMap[node.moduleId] = true
}
},
})
return Object.keys(theMap).length > 0
}
export function getObjExprProperty( export function getObjExprProperty(
node: ObjectExpression, node: ObjectExpression,
propName: string propName: string

View File

@ -139,6 +139,7 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [ const cacheEntries = Object.entries(codeToWriteCacheFor) as [

View File

@ -871,15 +871,3 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
pathToNode: getNodePathFromSourceRange(ast, range), pathToNode: getNodePathFromSourceRange(ast, range),
} }
} }
export function isSolid2D(artifact: Artifact): artifact is solid2D {
return (artifact as solid2D).pathId !== undefined
}
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
return (artifact as SegmentArtifact).pathId !== undefined
}
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
return (artifact as SweepArtifact).pathId !== undefined
}

View File

@ -1399,6 +1399,7 @@ export class EngineCommandManager extends EventTarget {
} }
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {} private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {} private onEngineConnectionClosed = () => {}
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
height, height,
token, token,
makeDefaultPlanes, makeDefaultPlanes,
modifyGrid,
settings = { settings = {
pool: null, pool: null,
theme: Themes.Dark, theme: Themes.Dark,
@ -1450,12 +1452,14 @@ export class EngineCommandManager extends EventTarget {
height: number height: number
token?: string token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes> makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: SettingsViaQueryString settings?: SettingsViaQueryString
}) { }) {
if (settings) { if (settings) {
this.settings = settings this.settings = settings
} }
this.makeDefaultPlanes = makeDefaultPlanes this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return return
} }
@ -1535,15 +1539,21 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings', type: 'default_camera_get_settings',
}, },
}) })
await this.initPlanes() // We want modify the grid first because we don't want it to flash.
setIsStreamReady(true) // Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
setIsStreamReady(true)
// Other parts of the application should use this to react on scene ready. // Other parts of the application should use this to react on scene ready.
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, { new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection, detail: this.engineConnection,
}) })
) )
})
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
@ -1869,6 +1879,17 @@ export class EngineCommandManager extends EventTarget {
} }
return JSON.stringify(this.defaultPlanes) return JSON.stringify(this.defaultPlanes)
} }
clearScene(): void {
const deleteCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'scene_clear_all',
},
}
this.clearDefaultPlanes()
this.engineConnection?.send(deleteCmd)
}
addCommandLog(message: CommandLog) { addCommandLog(message: CommandLog) {
if (this.commandLogs.length > 500) { if (this.commandLogs.length > 500) {
this.commandLogs.shift() this.commandLogs.shift()
@ -2202,6 +2223,15 @@ export class EngineCommandManager extends EventTarget {
}).catch(reportRejection) }).catch(reportRejection)
} }
/**
* Set the visibility of the scale grid in the engine scene.
* @param visible - whether to show or hide the scale grid
*/
setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible)
}
// Some "objects" have the same source range, such as sketch_mode_start and start_path. // Some "objects" have the same source range, such as sketch_mode_start and start_path.
// So when passing a range, we need to also specify the command type // So when passing a range, we need to also specify the command type
mapRangeToObjectId( mapRangeToObjectId(

View File

@ -164,7 +164,8 @@ mySketch001 = startSketchOn('XY')
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
], ],
}) })
@ -188,7 +189,8 @@ mySketch001 = startSketchOn('XY')
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
], ],
}) })

View File

@ -1701,7 +1701,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
if (err(nodeMeta2)) return nodeMeta2 if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2 const { node: varDec } = nodeMeta2
const varName = varDec.declaration.id.name const varName = varDec.declarations[0].id.name
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
previousProgramMemory.get(varName), previousProgramMemory.get(varName),
varName varName

View File

@ -111,10 +111,12 @@ export function isSketchVariablesLinked(
let nextVarDec: VariableDeclarator | undefined let nextVarDec: VariableDeclarator | undefined
for (const node of ast.body) { for (const node of ast.body) {
if (node.type !== 'VariableDeclaration') continue if (node.type !== 'VariableDeclaration') continue
if (node.declaration.id.name === secondArg.name) { const found = node.declarations.find(
nextVarDec = node.declaration ({ id }) => id?.name === secondArg.name
break )
} if (!found) continue
nextVarDec = found
break
} }
if (!nextVarDec) return false if (!nextVarDec) return false
return isSketchVariablesLinked(nextVarDec, primaryVarDec, ast) return isSketchVariablesLinked(nextVarDec, primaryVarDec, ast)

View File

@ -1,13 +1,9 @@
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { initPromise, parse, ParseResult } from './wasm' import { parse, ParseResult } from './wasm'
import { enginelessExecutor } from 'lib/testHelpers' import { enginelessExecutor } from 'lib/testHelpers'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Program } from '../wasm-lib/kcl/bindings/Program' import { Program } from '../wasm-lib/kcl/bindings/Program'
beforeEach(async () => {
await initPromise
})
it('can execute parsed AST', async () => { it('can execute parsed AST', async () => {
const code = `x = 1 const code = `x = 1
// A comment.` // A comment.`

View File

@ -1,13 +1,14 @@
import init, { import init, {
parse_wasm, parse_wasm,
recast_wasm, recast_wasm,
execute, execute_wasm,
kcl_lint, kcl_lint,
modify_ast_for_sketch_wasm, modify_ast_for_sketch_wasm,
is_points_ccw, is_points_ccw,
get_tangential_arc_to_info, get_tangential_arc_to_info,
program_memory_init, program_memory_init,
make_default_planes, make_default_planes,
modify_grid,
coredump, coredump,
toml_stringify, toml_stringify,
default_app_settings, default_app_settings,
@ -15,7 +16,6 @@ import init, {
parse_project_settings, parse_project_settings,
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -42,9 +42,7 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError' import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange' import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression' export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
@ -93,26 +91,12 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue' export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface' export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
/**
* The first two items are the start and end points (byte offsets from the start of the file).
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
*/
export type SourceRange = [number, number, boolean] export type SourceRange = [number, number, boolean]
/**
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
* so as not to expose details of the interpreter's current representation of module ids throughout
* the frontend).
*/
export function sourceRangeFromRust(s: RustSourceRange): SourceRange { export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
return [s[0], s[1], s[2] === 0] return [s[0], s[1], s[2] === 0]
} }
/**
* Create a default SourceRange for testing or as a placeholder.
*/
export function defaultSourceRange(): SourceRange { export function defaultSourceRange(): SourceRange {
return [0, 0, true] return [0, 0, true]
} }
@ -137,7 +121,7 @@ const initialise = async () => {
const fullUrl = wasmUrl() const fullUrl = wasmUrl()
const input = await fetch(fullUrl) const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer() const buffer = await input.arrayBuffer()
return await init({ module_or_path: buffer }) return await init(buffer)
} catch (e) { } catch (e) {
console.log('Error initialising WASM', e) console.log('Error initialising WASM', e)
return Promise.reject(e) return Promise.reject(e)
@ -178,10 +162,6 @@ export class ParseResult {
} }
} }
/**
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
* not be warnings or non-fatal errors.
*/
class SuccessParseResult extends ParseResult { class SuccessParseResult extends ParseResult {
program: Node<Program> program: Node<Program>
@ -512,19 +492,18 @@ export const _executor = async (
return Promise.reject(programMemoryOverride) return Promise.reject(programMemoryOverride)
try { try {
let jsAppSettings = default_app_settings() let baseUnit = 'mm'
if (!TEST) { if (!TEST) {
const lastSettingsSnapshot = await import( const getSettingsState = import('components/SettingsAuthProvider').then(
'components/SettingsAuthProvider' (module) => module.getSettingsState
).then((module) => module.lastSettingsContextSnapshot) )
if (lastSettingsSnapshot) { baseUnit =
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot) (await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
}
} }
const execState: RawExecState = await execute( const execState: RawExecState = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemoryOverride?.toRaw() || null), JSON.stringify(programMemoryOverride?.toRaw() || null),
JSON.stringify({ settings: jsAppSettings }), baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager fileSystemManager
) )
@ -572,6 +551,20 @@ export const makeDefaultPlanes = async (
} }
} }
export const modifyGrid = async (
engineCommandManager: EngineCommandManager,
hidden: boolean
): Promise<void> => {
try {
await modify_grid(engineCommandManager, hidden)
return
} catch (e) {
// TODO: do something real with the error.
console.log('modify grid error', e)
return Promise.reject(e)
}
}
export const modifyAstForSketch = async ( export const modifyAstForSketch = async (
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
ast: Node<Program>, ast: Node<Program>,
@ -705,21 +698,6 @@ export function defaultAppSettings(): DeepPartial<Configuration> | Error {
return default_app_settings() return default_app_settings()
} }
export async function clearSceneAndBustCache(
engineCommandManager: EngineCommandManager
): Promise<null | Error> {
try {
await clear_scene_and_bust_cache(engineCommandManager)
} catch (e: any) {
console.error('clear_scene_and_bust_cache: error', e)
return Promise.reject(
new Error(`Error on clear_scene_and_bust_cache: ${e}`)
)
}
return null
}
export function parseAppSettings( export function parseAppSettings(
toml: string toml: string
): DeepPartial<Configuration> | Error { ): DeepPartial<Configuration> | Error {

12
src/lib/appVersion.ts Normal file
View File

@ -0,0 +1,12 @@
import { NODE_ENV } from 'env'
import { isDesktop } from './isDesktop'
import { isTestEnv } from './isTestEnv'
/** Version number of the app */
export const APP_VERSION =
isTestEnv && NODE_ENV === 'development'
? '11.22.33'
: isDesktop()
? // @ts-ignore
window.electron.packageJson.version
: 'main'

View File

@ -32,14 +32,8 @@ export function mouseControlsToCameraSystem(
mouseControl: MouseControlType | undefined mouseControl: MouseControlType | undefined
): CameraSystem | undefined { ): CameraSystem | undefined {
switch (mouseControl) { switch (mouseControl) {
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'kittycad':
case 'kitty_cad': case 'kitty_cad':
return 'KittyCAD' return 'KittyCAD'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'onshape':
case 'on_shape': case 'on_shape':
return 'OnShape' return 'OnShape'
case 'trackpad_friendly': case 'trackpad_friendly':
@ -50,9 +44,6 @@ export function mouseControlsToCameraSystem(
return 'NX' return 'NX'
case 'creo': case 'creo':
return 'Creo' return 'Creo'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'autocad':
case 'auto_cad': case 'auto_cad':
return 'AutoCAD' return 'AutoCAD'
default: default:

View File

@ -1,15 +1,9 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
import { transformAstSketchLines } from 'lang/std/sketchcombos'
import { PathToNode } from 'lang/wasm'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants' import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api' import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { modelingMachine, SketchTool } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
import { revolveAxisValidator } from './validators'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -40,14 +34,9 @@ export type ModelingCommandSchema = {
Loft: { Loft: {
selection: Selections selection: Selections
} }
Shell: {
selection: Selections
thickness: KclCommandValue
}
Revolve: { Revolve: {
selection: Selections selection: Selections
angle: KclCommandValue angle: KclCommandValue
axis: Selections
} }
Fillet: { Fillet: {
// todo // todo
@ -61,18 +50,6 @@ export type ModelingCommandSchema = {
'change tool': { 'change tool': {
tool: SketchTool tool: SketchTool
} }
'Constrain length': {
selection: Selections
length: KclCommandValue
}
'Constrain with named value': {
currentValue: {
valueText: string
pathToNode: PathToNode
variableName: string
}
namedValue: KclCommandValue
}
'Text-to-CAD': { 'Text-to-CAD': {
prompt: string prompt: string
} }
@ -300,25 +277,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Shell: {
description: 'Hollow out a 3D solid.',
icon: 'shell',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
skip: false,
},
thickness: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection // TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
Revolve: { Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.', description: 'Create a 3D body by rotating a sketch region about an axis.',
@ -332,13 +290,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: true, required: true,
skip: true, skip: true,
}, },
axis: {
required: true,
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,
validation: revolveAxisValidator,
},
angle: { angle: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE, defaultValue: KCL_DEFAULT_DEGREE,
@ -386,88 +337,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
'Constrain length': {
description: 'Constrain the length of one or more segments.',
icon: 'dimension',
args: {
selection: {
inputType: 'selection',
selectionTypes: ['segment'],
multiple: false,
required: true,
skip: true,
},
length: {
inputType: 'kcl',
required: true,
createVariableByDefault: true,
defaultValue(_, machineContext) {
const selectionRanges = machineContext?.selectionRanges
if (!selectionRanges) return KCL_DEFAULT_LENGTH
const angleLength = angleLengthInfo({
selectionRanges,
angleOrLength: 'setLength',
})
if (err(angleLength)) return KCL_DEFAULT_LENGTH
const { transforms } = angleLength
// QUESTION: is it okay to reference kclManager here? will its state be up to date?
const sketched = transformAstSketchLines({
ast: structuredClone(kclManager.ast),
selectionRanges,
transformInfos: transforms,
programMemory: kclManager.programMemory,
referenceSegName: '',
})
if (err(sketched)) return KCL_DEFAULT_LENGTH
const { valueUsedInTransform } = sketched
return valueUsedInTransform?.toString() || KCL_DEFAULT_LENGTH
},
},
},
},
'Constrain with named value': {
description: 'Constrain a value by making it a named constant.',
icon: 'make-variable',
args: {
currentValue: {
description:
'Path to the node in the AST to constrain. This is never shown to the user.',
inputType: 'text',
required: false,
skip: true,
},
namedValue: {
inputType: 'kcl',
required: true,
createVariableByDefault: true,
variableName(commandBarContext, machineContext) {
const { currentValue } = commandBarContext.argumentsToSubmit
if (
!currentValue ||
!(currentValue instanceof Object) ||
!('variableName' in currentValue) ||
typeof currentValue.variableName !== 'string'
) {
return 'value'
}
return currentValue.variableName
},
defaultValue: (commandBarContext) => {
const { currentValue } = commandBarContext.argumentsToSubmit
if (
!currentValue ||
!(currentValue instanceof Object) ||
!('valueText' in currentValue) ||
typeof currentValue.valueText !== 'string'
) {
return KCL_DEFAULT_LENGTH
}
return currentValue.valueText
},
},
},
},
'Text-to-CAD': { 'Text-to-CAD': {
description: 'Use the Zoo Text-to-CAD API to generate part starters.', description: 'Use the Zoo Text-to-CAD API to generate part starters.',
icon: 'chat', icon: 'chat',

View File

@ -1,107 +0,0 @@
import { Models } from '@kittycad/lib'
import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'disable_dry_run' },
})
// Exit out since the command was successful
return
} catch (e) {
console.error(e)
console.error('disable_dry_run failed. This is bad!')
}
}
}
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
export const dryRunWrapper = async (callback: () => Promise<any>) => {
// Gotcha: What about race conditions?
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'enable_dry_run' },
})
const result = await callback()
return result
} catch (e) {
console.error(e)
} finally {
await disableDryRunWithRetry(5)
}
}
function isSelections(selections: unknown): selections is Selections {
return (
(selections as Selections).graphSelections !== undefined &&
(selections as Selections).otherSelections !== undefined
)
}
export const revolveAxisValidator = async ({
data,
context,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(context.argumentsToSubmit.selection)) {
return 'Unable to revolve, selections are missing'
}
const artifact =
context.argumentsToSubmit.selection.graphSelections[0].artifact
if (!artifact) {
return 'Unable to revolve, sketch not found'
}
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
return 'Unable to revolve, sketch has no path'
}
const sketchSelection = artifact.pathId
let edgeSelection = data.axis.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
}
if (!edgeSelection) {
return 'Unable to revolve, edge is missing'
}
const angleInDegrees: Models['Angle_type'] = {
unit: 'degrees',
value: 360,
}
const revolveAboutEdgeCommand = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'revolve_about_edge',
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
tolerance: 0.0001,
},
})
}
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (attemptRevolve?.success) {
return true
} else {
// return error message for the toast
return 'Unable to revolve with selected axis'
}
}

View File

@ -7,7 +7,7 @@ import { ReactNode } from 'react'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Artifact } from 'lang/std/artifactGraph' import { Artifact } from 'lang/std/artifactGraph'
import { CommandBarContext } from 'machines/commandBarMachine'
type Icon = CustomIconName type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const const PLATFORMS = ['both', 'web', 'desktop'] as const
const INPUT_TYPES = [ const INPUT_TYPES = [
@ -147,30 +147,8 @@ export type CommandArgumentConfig<
inputType: 'selection' inputType: 'selection'
selectionTypes: Artifact['type'][] selectionTypes: Artifact['type'][]
multiple: boolean multiple: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean
variableName?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => string)
defaultValue?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => string)
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
| { | {
inputType: 'string' inputType: 'string'
defaultValue?: defaultValue?:
@ -243,30 +221,8 @@ export type CommandArgument<
inputType: 'selection' inputType: 'selection'
selectionTypes: Artifact['type'][] selectionTypes: Artifact['type'][]
multiple: boolean multiple: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean
variableName?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => string)
defaultValue?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => string)
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
| { | {
inputType: 'string' inputType: 'string'
defaultValue?: defaultValue?:

View File

@ -53,7 +53,6 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
LOFT: 'loft', LOFT: 'loft',
SHELL: 'shell',
SEGMENT: 'seg', SEGMENT: 'seg',
REVOLVE: 'revolve', REVOLVE: 'revolve',
PLANE: 'plane', PLANE: 'plane',
@ -111,10 +110,3 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
/** Toast id for the app auto-updater toast */ /** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast' export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
export const KCL_AXIS_X = 'X'
export const KCL_AXIS_Y = 'Y'
export const KCL_AXIS_NEG_X = '-X'
export const KCL_AXIS_NEG_Y = '-Y'
export const KCL_DEFAULT_AXIS = 'X'

View File

@ -2,7 +2,7 @@ import { CommandLog, EngineCommandManager } from 'lang/std/engineConnection'
import { WebrtcStats } from 'wasm-lib/kcl/bindings/WebrtcStats' import { WebrtcStats } from 'wasm-lib/kcl/bindings/WebrtcStats'
import { OsInfo } from 'wasm-lib/kcl/bindings/OsInfo' import { OsInfo } from 'wasm-lib/kcl/bindings/OsInfo'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { APP_VERSION } from 'routes/Settings' import { APP_VERSION } from 'lib/appVersion'
import { UAParser } from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import screenshot from 'lib/screenshot' import screenshot from 'lib/screenshot'
import { VITE_KC_API_BASE_URL } from 'env' import { VITE_KC_API_BASE_URL } from 'env'

View File

@ -155,8 +155,6 @@ export function buildCommandArgument<
context: ContextFrom<T>, context: ContextFrom<T>,
machineActor: Actor<T> machineActor: Actor<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } { ): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
// You need to manually add key/value pairs here.
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,
required: arg.required, required: arg.required,
@ -183,13 +181,10 @@ export function buildCommandArgument<
...baseCommandArgument, ...baseCommandArgument,
multiple: arg.multiple, multiple: arg.multiple,
selectionTypes: arg.selectionTypes, selectionTypes: arg.selectionTypes,
validation: arg.validation,
} satisfies CommandArgument<O, T> & { inputType: 'selection' } } satisfies CommandArgument<O, T> & { inputType: 'selection' }
} else if (arg.inputType === 'kcl') { } else if (arg.inputType === 'kcl') {
return { return {
inputType: arg.inputType, inputType: arg.inputType,
createVariableByDefault: arg.createVariableByDefault,
variableName: arg.variableName,
defaultValue: arg.defaultValue, defaultValue: arg.defaultValue,
...baseCommandArgument, ...baseCommandArgument,
} satisfies CommandArgument<O, T> & { inputType: 'kcl' } } satisfies CommandArgument<O, T> & { inputType: 'kcl' }

View File

@ -13,6 +13,7 @@ import {
listProjects, listProjects,
readAppSettingsFile, readAppSettingsFile,
} from './desktop' } from './desktop'
import { engineCommandManager } from './singletons'
export const isHidden = (fileOrDir: FileEntry) => export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.') !!fileOrDir.name?.startsWith('.')
@ -115,6 +116,9 @@ export async function createAndOpenNewTutorialProject({
) => void ) => void
navigate: (path: string) => void navigate: (path: string) => void
}) { }) {
// Clear the scene.
engineCommandManager.clearScene()
// Create a new project with the onboarding project name // Create a new project with the onboarding project name
const configuration = await readAppSettingsFile() const configuration = await readAppSettingsFile()
const projects = await listProjects(configuration) const projects = await listProjects(configuration)

View File

@ -3,27 +3,27 @@ export const bracket = `// Shelf Bracket
// Define constants // Define constants
sigmaAllow = 35000 // psi (6061-T6 aluminum) const sigmaAllow = 35000 // psi (6061-T6 aluminum)
width = 6 // inch const width = 6 // inch
p = 300 // Force on shelf - lbs const p = 300 // Force on shelf - lbs
factorOfSafety = 1.2 // FOS of 1.2 const factorOfSafety = 1.2 // FOS of 1.2
shelfMountL = 5 // inches const shelfMountL = 5 // inches
wallMountL = 2 // inches const wallMountL = 2 // inches
shelfDepth = 12 // Shelf is 12 inches in depth from the wall const shelfDepth = 12 // Shelf is 12 inches in depth from the wall
moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in) const moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in)
filletRadius = .375 // inches const filletRadius = .375 // inches
extFilletRadius = .25 // inches const extFilletRadius = .25 // inches
mountingHoleDiameter = 0.5 // inches const mountingHoleDiameter = 0.5 // inches
// Calculate required thickness of bracket // Calculate required thickness of bracket
thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches) const thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches)
// Sketch the bracket body and fillet the inner and outer edges of the bend // Sketch the bracket body and fillet the inner and outer edges of the bend
bracketLeg1Sketch = startSketchOn('XY') const bracketLeg1Sketch = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([shelfMountL - filletRadius, 0], %, $fillet1) |> line([shelfMountL - filletRadius, 0], %, $fillet1)
|> line([0, width], %, $fillet2) |> line([0, width], %, $fillet2)
@ -47,7 +47,7 @@ bracketLeg1Sketch = startSketchOn('XY')
}, %), %) }, %), %)
// Extrude the leg 2 bracket sketch // Extrude the leg 2 bracket sketch
bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch) const bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = extFilletRadius,
tags = [ tags = [
@ -57,7 +57,7 @@ bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch)
}, %) }, %)
// Sketch the fillet arc // Sketch the fillet arc
filletSketch = startSketchOn('XZ') const filletSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([0, thickness], %) |> line([0, thickness], %)
|> arc({ |> arc({
@ -73,10 +73,10 @@ filletSketch = startSketchOn('XZ')
}, %) }, %)
// Sketch the bend // Sketch the bend
filletExtrude = extrude(-width, filletSketch) const filletExtrude = extrude(-width, filletSketch)
// Create a custom plane for the leg that sits on the wall // Create a custom plane for the leg that sits on the wall
customPlane = { const customPlane = {
plane = { plane = {
origin = { x = -filletRadius, y = 0, z = 0 }, origin = { x = -filletRadius, y = 0, z = 0 },
xAxis = { x = 0, y = 1, z = 0 }, xAxis = { x = 0, y = 1, z = 0 },
@ -86,7 +86,7 @@ customPlane = {
} }
// Create a sketch for the second leg // Create a sketch for the second leg
bracketLeg2Sketch = startSketchOn(customPlane) const bracketLeg2Sketch = startSketchOn(customPlane)
|> startProfileAt([0, -filletRadius], %) |> startProfileAt([0, -filletRadius], %)
|> line([width, 0], %) |> line([width, 0], %)
|> line([0, -wallMountL], %, $fillet3) |> line([0, -wallMountL], %, $fillet3)
@ -102,7 +102,7 @@ bracketLeg2Sketch = startSketchOn(customPlane)
}, %), %) }, %), %)
// Extrude the second leg // Extrude the second leg
bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch) const bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = extFilletRadius,
tags = [ tags = [
@ -135,8 +135,8 @@ function findLineInExampleCode({
} }
export const bracketWidthConstantLine = findLineInExampleCode({ export const bracketWidthConstantLine = findLineInExampleCode({
searchText: 'width =', searchText: 'const width',
}) })
export const bracketThicknessCalculationLine = findLineInExampleCode({ export const bracketThicknessCalculationLine = findLineInExampleCode({
searchText: 'thickness =', searchText: 'const thickness',
}) })

4
src/lib/isTestEnv.ts Normal file
View File

@ -0,0 +1,4 @@
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
export const isTestEnv =
globalThis.window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'

View File

@ -5,7 +5,7 @@ import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants' import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm' import { parseProjectSettings } from 'lang/wasm'
import { err, reportRejection } from './trap' import { err } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils' import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
interface OnSubmitProps { interface OnSubmitProps {
@ -28,7 +28,7 @@ export function kclCommands(
groupId: 'code', groupId: 'code',
icon: 'code', icon: 'code',
onSubmit: () => { onSubmit: () => {
kclManager.format().catch(reportRejection) kclManager.format()
}, },
}, },
{ {

View File

@ -569,17 +569,6 @@ export function canSweepSelection(selection: Selections) {
) )
} }
export function canRevolveSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
)
return (
!!isSketchPipe(selection) &&
(commonNodes.every((n) => nodeHasClose(n)) ||
commonNodes.every((n) => nodeHasCircle(n)))
)
}
export function canLoftSelection(selection: Selections) { export function canLoftSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) => const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i) buildCommonNodeFromSelection(selection, i)
@ -596,17 +585,6 @@ export function canLoftSelection(selection: Selections) {
) )
} }
export function canShellSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
)
return commonNodes.every(
(n) =>
n.selection.artifact?.type === 'cap' ||
n.selection.artifact?.type === 'wall'
)
}
// This accounts for non-geometry selections under "other" // This accounts for non-geometry selections under "other"
export type ResolvedSelectionType = Artifact['type'] | 'other' export type ResolvedSelectionType = Artifact['type'] | 'other'
export type SelectionCountsByType = Map<ResolvedSelectionType, number> export type SelectionCountsByType = Map<ResolvedSelectionType, number>
@ -641,29 +619,12 @@ export function getSelectionCountByType(
} }
}) })
selection.graphSelections.forEach((graphSelection) => { selection.graphSelections.forEach((selection) => {
if (!graphSelection.artifact) { if (!selection.artifact) {
/** incrementOrInitializeSelectionType('other')
* TODO: remove this heuristic-based selection type detection. return
* Currently, if you've created a sketch and have not left sketch mode,
* the selection will be a segment selection with no artifact.
* This is because the mock execution does not update the artifact graph.
* Once we move the artifactGraph creation to WASM, we can remove this,
* as the artifactGraph will always be up-to-date.
*/
if (isSingleCursorInPipe(selection, kclManager.ast)) {
incrementOrInitializeSelectionType('segment')
return
} else {
console.warn(
'Selection is outside of a sketch but has no artifact. Sketch segment selections are the only kind that can have a valid selection with no artifact.',
JSON.stringify(graphSelection)
)
incrementOrInitializeSelectionType('other')
return
}
} }
incrementOrInitializeSelectionType(graphSelection.artifact.type) incrementOrInitializeSelectionType(selection.artifact.type)
}) })
return selectionsByType return selectionsByType

View File

@ -12,7 +12,7 @@ export type InteractionMapItem = {
* Controls both the available names for interaction map categories * Controls both the available names for interaction map categories
* and the order in which they are displayed. * and the order in which they are displayed.
*/ */
const interactionMapCategories = [ export const interactionMapCategories = [
'Sketching', 'Sketching',
'Modeling', 'Modeling',
'Command Palette', 'Command Palette',

View File

@ -2,7 +2,6 @@ import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { import {
configurationToSettingsPayload, configurationToSettingsPayload,
getAllCurrentSettings,
projectConfigurationToSettingsPayload, projectConfigurationToSettingsPayload,
setSettingsAtLevel, setSettingsAtLevel,
} from './settingsUtils' } from './settingsUtils'
@ -66,48 +65,3 @@ describe(`testing settings initialization`, () => {
expect(settings.app.themeColor.current).toBe('200') expect(settings.app.themeColor.current).toBe('200')
}) })
}) })
describe(`testing getAllCurrentSettings`, () => {
it(`returns the correct settings`, () => {
// Set up the settings
let settings = createSettings()
const appConfiguration: DeepPartial<Configuration> = {
settings: {
app: {
appearance: {
theme: 'dark',
color: 190,
},
},
},
}
const projectConfiguration: DeepPartial<Configuration> = {
settings: {
app: {
appearance: {
theme: 'light',
color: 200,
},
},
modeling: {
base_unit: 'ft',
},
},
}
const appSettingsPayload = configurationToSettingsPayload(appConfiguration)
const projectSettingsPayload =
projectConfigurationToSettingsPayload(projectConfiguration)
setSettingsAtLevel(settings, 'user', appSettingsPayload)
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
// Now the test: get all the settings' current resolved values
const allCurrentSettings = getAllCurrentSettings(settings)
// This one gets the 'user'-level theme because it's ignored at the project level
// (see the test "doesn't read theme from project settings")
expect(allCurrentSettings.app.theme).toBe('dark')
expect(allCurrentSettings.app.themeColor).toBe('200')
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
})
})

View File

@ -286,27 +286,6 @@ export function getChangedSettingsAtLevel(
return changedSettings return changedSettings
} }
export function getAllCurrentSettings(
allSettings: typeof settings
): SaveSettingsPayload {
const currentSettings = {} as SaveSettingsPayload
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
const categoryKey = category as keyof typeof settings
Object.entries(settingsCategory).forEach(
([setting, settingValue]: [string, Setting]) => {
const settingKey =
setting as keyof (typeof settings)[typeof categoryKey]
currentSettings[categoryKey] = {
...currentSettings[categoryKey],
[settingKey]: settingValue.current,
}
}
)
})
return currentSettings
}
export function setSettingsAtLevel( export function setSettingsAtLevel(
allSettings: typeof settings, allSettings: typeof settings,
level: SettingsLevel, level: SettingsLevel,

View File

@ -112,6 +112,9 @@ export async function executor(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes)) return new Promise((resolve) => resolve(defaultPlanes))
}, },
modifyGrid: (hidden: boolean) => {
return new Promise((resolve) => resolve())
},
}) })
return new Promise((resolve) => { return new Promise((resolve) => {

View File

@ -190,15 +190,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}, },
{ {
id: 'shell', id: 'shell',
onClick: ({ commandBarSend }) => { onClick: () => console.error('Shell not yet implemented'),
commandBarSend({
type: 'Find and select command',
data: { name: 'Shell', groupId: 'modeling' },
})
},
disabled: (state) => !state.can({ type: 'Shell' }),
icon: 'shell', icon: 'shell',
status: 'available', status: 'kcl-only',
title: 'Shell', title: 'Shell',
description: 'Hollow out a 3D solid.', description: 'Hollow out a 3D solid.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }], links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
@ -540,15 +534,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[ [
{ {
id: 'constraint-length', id: 'constraint-length',
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }), disabled: (state) =>
onClick: ({ commandBarSend }) => !(
commandBarSend({ state.matches({ Sketch: 'SketchIdle' }) &&
type: 'Find and select command', state.can({ type: 'Constrain length' })
data: { ),
name: 'Constrain length', onClick: ({ modelingSend }) =>
groupId: 'modeling', modelingSend({ type: 'Constrain length' }),
},
}),
icon: 'dimension', icon: 'dimension',
status: 'available', status: 'available',
title: 'Length', title: 'Length',

View File

@ -109,11 +109,11 @@ export function useCalculateKclExpression({
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declaration.id?.name === '__result__' a.declarations?.[0]?.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declaration.init resultDeclaration?.declarations?.[0]?.init
const result = execState.memory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)

Some files were not shown because too many files have changed in this diff Show More