Compare commits
10 Commits
paultag/di
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
f70847c407 | |||
9510849a3a | |||
5b208356b4 | |||
84209e764e | |||
2b85e7abd6 | |||
9155a5efc8 | |||
2ccc27112a | |||
ee160b67f4 | |||
5117b6f5d6 | |||
bc8a7a364d |
9
.github/workflows/build-apps.yml
vendored
@ -165,6 +165,7 @@ jobs:
|
||||
- name: Build the app (release)
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||
env:
|
||||
PUBLISH_FOR_PULL_REQUEST: true
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@ -172,6 +173,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: yarn electron-builder --config --publish always
|
||||
|
||||
@ -227,6 +229,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: yarn electron-builder --config --publish always
|
||||
|
||||
@ -362,7 +365,7 @@ jobs:
|
||||
- name: Set more complete nightly release notes
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
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
|
||||
git fetch --prune --unshallow --tags
|
||||
export TAG="nightly-${VERSION}"
|
||||
@ -391,10 +394,6 @@ jobs:
|
||||
parent: false
|
||||
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
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: actions/github-script@v7
|
||||
|
2
.github/workflows/cargo-test.yml
vendored
@ -71,7 +71,7 @@ jobs:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{secrets.CODECOV_TOKEN}}
|
||||
fail_ci_if_error: true
|
||||
|
6
.github/workflows/publish-apps-release.yml
vendored
@ -126,7 +126,11 @@ jobs:
|
||||
destination: 'dl.kittycad.io/releases/modeling-app'
|
||||
|
||||
- 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
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
|
1
.gitignore
vendored
@ -61,7 +61,6 @@ Mac_App_Distribution.provisionprofile
|
||||
*.tsbuildinfo
|
||||
src/wasm-lib/pkg
|
||||
|
||||
.eslintcache
|
||||
venv
|
||||
.vite/
|
||||
|
||||
|
43
INSTALL.md
@ -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
|
||||
```
|
@ -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
|
||||
chamfer cases work currently.
|
||||
|
||||
- **Appearance**: Changing the appearance on a loft does not work.
|
||||
|
@ -19,7 +19,6 @@ layout: manual
|
||||
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
||||
* [`angledLineToX`](kcl/angledLineToX)
|
||||
* [`angledLineToY`](kcl/angledLineToY)
|
||||
* [`appearance`](kcl/appearance)
|
||||
* [`arc`](kcl/arc)
|
||||
* [`arcTo`](kcl/arcTo)
|
||||
* [`asin`](kcl/asin)
|
||||
@ -102,7 +101,6 @@ layout: manual
|
||||
* [`startProfileAt`](kcl/startProfileAt)
|
||||
* [`startSketchAt`](kcl/startSketchAt)
|
||||
* [`startSketchOn`](kcl/startSketchOn)
|
||||
* [`sweep`](kcl/sweep)
|
||||
* [`tan`](kcl/tan)
|
||||
* [`tangentToEnd`](kcl/tangentToEnd)
|
||||
* [`tangentialArc`](kcl/tangentialArc)
|
||||
|
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
|
||||
```js
|
||||
r = 10 // radius
|
||||
// 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")
|
||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||
})
|
||||
|
@ -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
|
||||
// named function outside.
|
||||
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
|
||||
})
|
||||
|
||||
@ -84,7 +84,7 @@ fn decagon(radius) {
|
||||
// Use a `reduce` to draw the remaining decagon sides.
|
||||
// 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.
|
||||
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
|
||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
|
||||
// Draw one edge of the decagon.
|
||||
x = cos(stepAngle * i) * radius
|
||||
y = sin(stepAngle * i) * radius
|
||||
|
7152
docs/kcl/std.json
@ -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 |
|
||||
|
||||
|
@ -12,10 +12,5 @@ KCL value for an optional parameter which was not given an argument. (remember,
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
---
|
||||
title: "SweepData"
|
||||
excerpt: "Data for a sweep."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for a sweep.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||
|
||||
|
@ -458,8 +458,8 @@ test.describe('Editor tests', () => {
|
||||
|
||||
/* add the following code to the editor ($ error is not a valid line)
|
||||
$ error
|
||||
topAng = 30
|
||||
bottomAng = 25
|
||||
const topAng = 30
|
||||
const bottomAng = 25
|
||||
*/
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type('$ error')
|
||||
@ -474,14 +474,12 @@ test.describe('Editor tests', () => {
|
||||
await page.keyboard.type('bottomAng = 25')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// error in gutter
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(
|
||||
page.getByText('Tag names must not be empty').first()
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('Unexpected token: $').first()).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('$ error').click()
|
||||
|
@ -7,7 +7,6 @@ export class ToolbarFixture {
|
||||
|
||||
extrudeButton!: Locator
|
||||
loftButton!: Locator
|
||||
shellButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
lineBtn!: Locator
|
||||
@ -29,7 +28,6 @@ export class ToolbarFixture {
|
||||
this.page = page
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.loftButton = page.getByTestId('loft')
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.lineBtn = page.getByTestId('line')
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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(
|
||||
'when code with error first loads you get errors in console',
|
||||
{ tag: '@electron' },
|
||||
|
@ -950,75 +950,7 @@ test(
|
||||
|
||||
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
// 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.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
|
||||
test('Grid turned off', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
@ -1164,109 +1096,3 @@ test.fixme('theme persists', async ({ page, context }) => {
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||
test('code color goober', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line([0, 7], %)
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> line([-3, 0], %)
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> line([0, 7], %)
|
||||
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> startProfileAt([2, 0], %)
|
||||
|> arc({
|
||||
angleEnd = 360,
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('code color goober opening window', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line([0, 7], %)
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> line([-3, 0], %)
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> line([0, 7], %)
|
||||
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> startProfileAt([2, 0], %)
|
||||
|> arc({
|
||||
angleEnd = 360,
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
|
||||
|
||||
// Click the color widget
|
||||
await page.locator('.cm-css-color-picker-wrapper input').click()
|
||||
|
||||
await expect(
|
||||
page,
|
||||
'expect small color widget to have window open'
|
||||
).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: 'in',
|
||||
mouseControls: 'Zoo',
|
||||
mouseControls: 'KittyCAD',
|
||||
cameraProjection: 'perspective',
|
||||
showDebugPanel: true,
|
||||
},
|
||||
|
@ -479,26 +479,4 @@ test.describe('Testing Camera Movement', () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Right-click opens context menu when not dragged', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await test.step(`The menu should not show if we drag the mouse`, async () => {
|
||||
await page.mouse.move(900, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(900, 300)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`The menu should show if we don't drag the mouse`, async () => {
|
||||
await page.mouse.move(900, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -26,17 +26,7 @@ test.describe('Testing constraints', () => {
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
// constants and locators
|
||||
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',
|
||||
})
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -46,26 +36,26 @@ test.describe('Testing constraints', () => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Click the line of code for line.
|
||||
// TODO remove this and reinstate `await topHorzSegmentClick()`
|
||||
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
|
||||
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// enter sketch again
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
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
|
||||
.getByRole('button', { name: 'dimension Length', exact: true })
|
||||
.click()
|
||||
await expect(cmdBarKclInput).toHaveText('20')
|
||||
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 page.getByText('Add constraining value').click()
|
||||
|
||||
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.
|
||||
@ -76,6 +66,7 @@ test.describe('Testing constraints', () => {
|
||||
await page.waitForTimeout(500) // wait for animation
|
||||
|
||||
// Exit sketch
|
||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(
|
||||
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 = [
|
||||
{
|
||||
testName: 'Angle - Add variable',
|
||||
@ -547,6 +538,18 @@ part002 = startSketchOn('XZ')
|
||||
constraint: 'angle',
|
||||
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
|
||||
for (const { testName, addVariable, value, constraint } of cases) {
|
||||
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', () => {
|
||||
const cases = [
|
||||
{
|
||||
@ -949,15 +868,6 @@ part002 = startSketchOn('XZ')
|
||||
|> 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)
|
||||
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.getByTestId('dropdown-constraint-length').click()
|
||||
|
||||
await cmdBarKclInput.fill('10')
|
||||
await cmdBarSubmitButton.click()
|
||||
await page.getByLabel('length Value').fill('10')
|
||||
await page.getByRole('button', { name: 'Add constraining value' }).click()
|
||||
|
||||
activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
||||
|
@ -91,14 +91,7 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByTestId('constraint-symbol-popover').count()
|
||||
).toBeGreaterThan(0)
|
||||
await unconstrainedLocator.click()
|
||||
await expect(
|
||||
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||
).toBeFocused()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
await page.getByText('Add variable').click()
|
||||
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
||||
}
|
||||
|
||||
@ -158,14 +151,7 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByTestId('constraint-symbol-popover').count()
|
||||
).toBeGreaterThan(0)
|
||||
await unconstrainedLocator.click()
|
||||
await expect(
|
||||
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||
).toBeFocused()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
await page.getByText('Add variable').click()
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
expectAfterUnconstrained
|
||||
)
|
||||
|
@ -1,9 +1,20 @@
|
||||
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 { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||
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 config: ForgeConfig = {
|
||||
@ -28,7 +39,26 @@ const config: ForgeConfig = {
|
||||
extendInfo: 'Info.plist', // Information for file associations.
|
||||
},
|
||||
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: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
|
33
package.json
@ -39,6 +39,7 @@
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
@ -68,7 +69,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --port=3000 --host=0.0.0.0",
|
||||
"start": "vite",
|
||||
"start:prod": "vite preview --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",
|
||||
@ -80,7 +81,6 @@
|
||||
"simpleserver": "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:stop": "kill-port 3000",
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"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-notes": "./scripts/set-files-notes.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",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
"make:dev": "make dev",
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||
"tron:start": "electron-forge start",
|
||||
"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",
|
||||
"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",
|
||||
@ -145,13 +145,19 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@electron-forge/cli": "7.4.0",
|
||||
"@electron-forge/plugin-fuses": "7.4.0",
|
||||
"@electron-forge/plugin-vite": "7.4.0",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@electron-forge/cli": "^7.4.0",
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.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",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
@ -164,7 +170,7 @@
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.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/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
@ -178,9 +184,9 @@
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "32.1.2",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
@ -201,6 +207,7 @@
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
|
@ -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
|
65
src/App.tsx
@ -3,7 +3,7 @@ import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
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 { PATHS } from 'lib/paths'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -22,7 +22,14 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
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 { useNetworkMachineStatus } from 'components/NetworkMachineIndicator'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
@ -31,11 +38,10 @@ export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
// We need the ref for the outermost div so we can screenshot the app for
|
||||
// the coredump.
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const { state: modelingState, streamRef } = useModelingContext()
|
||||
|
||||
const projectName = project?.name || null
|
||||
const projectPath = project?.path || null
|
||||
@ -77,21 +83,44 @@ export function App() {
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
return (
|
||||
<div className="relative h-full flex flex-col" ref={ref}>
|
||||
<AppHeader
|
||||
className={'transition-opacity transition-duration-75 ' + paneOpacity}
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
<div className="h-screen flex flex-col overflow-hidden select-none">
|
||||
<div className="relative flex flex-1 flex-col" ref={streamRef}>
|
||||
<AppHeader
|
||||
className={'transition-opacity transition-duration-75 ' + paneOpacity}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ export class CameraControls {
|
||||
pendingZoom: number | null = null
|
||||
pendingRotation: Vector2 | null = null
|
||||
pendingPan: Vector2 | null = null
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
||||
isFovAnimationInProgress = false
|
||||
perspectiveFovBeforeOrtho = 45
|
||||
get isPerspective() {
|
||||
|
@ -505,8 +505,7 @@ const ConstraintSymbol = ({
|
||||
constrainInfo: ConstrainInfo
|
||||
verticalPosition: 'top' | 'bottom'
|
||||
}) => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { context } = useModelingContext()
|
||||
const { context, send } = useModelingContext()
|
||||
const varNameMap: {
|
||||
[key in ConstrainInfo['type']]: {
|
||||
varName: string
|
||||
@ -625,18 +624,11 @@ const ConstraintSymbol = ({
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={toSync(async () => {
|
||||
if (!isConstrained) {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
send({
|
||||
type: 'Convert to variable',
|
||||
data: {
|
||||
name: 'Constrain with named value',
|
||||
groupId: 'modeling',
|
||||
argDefaultValues: {
|
||||
currentValue: {
|
||||
pathToNode,
|
||||
variableName: varName,
|
||||
valueText: value,
|
||||
},
|
||||
},
|
||||
pathToNode,
|
||||
variableName: varName,
|
||||
},
|
||||
})
|
||||
} else if (isConstrained) {
|
||||
|
@ -701,7 +701,8 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
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(
|
||||
kclManager.programMemory.get(variableDeclarationName),
|
||||
@ -901,9 +902,10 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declaration
|
||||
const startSketchOnInit = startSketchOn?.init
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const tags: [string, string, string] = [
|
||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
||||
@ -911,7 +913,7 @@ export class SceneEntities {
|
||||
findUniqueName(_ast, 'rectangleSegmentC'),
|
||||
]
|
||||
|
||||
startSketchOn.init = createPipeExpression([
|
||||
startSketchOn[0].init = createPipeExpression([
|
||||
startSketchOnInit,
|
||||
...getRectangleCallExpressions(rectangleOrigin, tags),
|
||||
])
|
||||
@ -941,7 +943,7 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
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 y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
||||
@ -990,7 +992,7 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return
|
||||
const sketchInit = _node.node?.declaration.init
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
if (sketchInit.type !== 'PipeExpression') {
|
||||
return
|
||||
@ -1056,9 +1058,10 @@ export class SceneEntities {
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
|
||||
// startSketchOn already exists
|
||||
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declaration
|
||||
const startSketchOnInit = startSketchOn?.init
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const tags: [string, string, string] = [
|
||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
||||
@ -1066,7 +1069,7 @@ export class SceneEntities {
|
||||
findUniqueName(_ast, 'rectangleSegmentC'),
|
||||
]
|
||||
|
||||
startSketchOn.init = createPipeExpression([
|
||||
startSketchOn[0].init = createPipeExpression([
|
||||
startSketchOnInit,
|
||||
...getRectangleCallExpressions(rectangleOrigin, tags),
|
||||
])
|
||||
@ -1096,7 +1099,7 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
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 y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
||||
@ -1152,7 +1155,7 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return
|
||||
const sketchInit = _node.node?.declaration.init
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateCenterRectangleSketch(
|
||||
@ -1221,11 +1224,12 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declaration
|
||||
const startSketchOnInit = startSketchOn?.init
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
startSketchOn.init = createPipeExpression([
|
||||
startSketchOn[0].init = createPipeExpression([
|
||||
startSketchOnInit,
|
||||
createCallExpressionStdLib('circle', [
|
||||
createObjectExpression({
|
||||
@ -1267,7 +1271,7 @@ export class SceneEntities {
|
||||
)
|
||||
let modded = structuredClone(truncatedAst)
|
||||
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 y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
|
||||
@ -1335,7 +1339,7 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return
|
||||
const sketchInit = _node.node?.declaration.init
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
let modded = structuredClone(_ast)
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
@ -2056,7 +2060,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(_node)) return _node
|
||||
const variableDeclarationName = _node.node?.declaration.id?.name || ''
|
||||
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
|
||||
const sg = sketchFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
@ -2081,7 +2085,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
])
|
||||
}
|
||||
;(
|
||||
(_ast.body[bodyIndex] as VariableDeclaration).declaration
|
||||
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0]
|
||||
.init as PipeExpression
|
||||
).body.push(newSegment)
|
||||
// update source ranges to section we just added.
|
||||
@ -2092,19 +2096,19 @@ function prepareTruncatedMemoryAndAst(
|
||||
const updatedSrcRangeAst = pResult.program
|
||||
|
||||
const lastPipeItem = (
|
||||
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration
|
||||
.init as PipeExpression
|
||||
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration)
|
||||
.declarations[0].init as PipeExpression
|
||||
).body.slice(-1)[0]
|
||||
|
||||
;(
|
||||
(_ast.body[bodyIndex] as VariableDeclaration).declaration
|
||||
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0]
|
||||
.init as PipeExpression
|
||||
).body.slice(-1)[0].start = lastPipeItem.start
|
||||
|
||||
_ast.end = lastPipeItem.end
|
||||
const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration>
|
||||
varDec.end = lastPipeItem.end
|
||||
const declarator = varDec.declaration
|
||||
const declarator = varDec.declarations[0]
|
||||
declarator.end = lastPipeItem.end
|
||||
const init = declarator.init as Node<PipeExpression>
|
||||
init.end = lastPipeItem.end
|
||||
@ -2141,7 +2145,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
if (node.type !== 'VariableDeclaration') {
|
||||
continue
|
||||
}
|
||||
const name = node.declaration.id.name
|
||||
const name = node.declarations[0].id.name
|
||||
const memoryItem = programMemory.get(name)
|
||||
if (!memoryItem) {
|
||||
continue
|
||||
|
@ -169,11 +169,11 @@ export function useCalc({
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declaration.id?.name === '__result__'
|
||||
a.declarations?.[0]?.id?.name === '__result__'
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declaration.init
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = execState.memory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
|
@ -8,16 +8,11 @@ import { getSystemTheme } from 'lib/theme'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
import { roundOff } from 'lib/utils'
|
||||
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 styles from './CommandBarKclInput.module.css'
|
||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||
import { useSelector } from '@xstate/react'
|
||||
|
||||
const machineContextSelector = (snapshot?: {
|
||||
context: Record<string, unknown>
|
||||
}) => snapshot?.context
|
||||
|
||||
function CommandBarKclInput({
|
||||
arg,
|
||||
@ -36,44 +31,12 @@ function CommandBarKclInput({
|
||||
arg.name
|
||||
] as KclCommandValue | undefined
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const argMachineContext = useSelector(
|
||||
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 defaultValue = (arg.defaultValue as string) || ''
|
||||
const [value, setValue] = useState(
|
||||
previouslySetValue?.valueText || defaultValue || ''
|
||||
)
|
||||
const [createNewVariable, setCreateNewVariable] = useState(
|
||||
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||
arg.createVariableByDefault ||
|
||||
false
|
||||
previouslySetValue && 'variableName' in previouslySetValue
|
||||
)
|
||||
const [canSubmit, setCanSubmit] = useState(true)
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
@ -89,7 +52,10 @@ function CommandBarKclInput({
|
||||
isNewVariableNameUnique,
|
||||
} = useCalculateKclExpression({
|
||||
value,
|
||||
initialVariableName,
|
||||
initialVariableName:
|
||||
previouslySetValue && 'variableName' in previouslySetValue
|
||||
? previouslySetValue.variableName
|
||||
: arg.name,
|
||||
})
|
||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||
label: v.key,
|
||||
|
@ -1,23 +1,13 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import {
|
||||
MouseEvent,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
export interface ContextMenuProps
|
||||
interface ContextMenuProps
|
||||
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
||||
items?: React.ReactElement[]
|
||||
menuTargetElement?: RefObject<HTMLElement>
|
||||
guard?: (e: globalThis.MouseEvent) => boolean
|
||||
event?: 'contextmenu' | 'mouseup'
|
||||
}
|
||||
|
||||
const DefaultContextMenuItems = [
|
||||
@ -30,8 +20,6 @@ export function ContextMenu({
|
||||
items = DefaultContextMenuItems,
|
||||
menuTargetElement,
|
||||
className,
|
||||
guard,
|
||||
event = 'contextmenu',
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
@ -44,15 +32,6 @@ export function ContextMenu({
|
||||
useHotkeys('esc', () => setOpen(false), {
|
||||
enabled: open,
|
||||
})
|
||||
const handleContextMenu = useCallback(
|
||||
(e: globalThis.MouseEvent) => {
|
||||
if (guard && !guard(e)) return
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.clientX, y: e.clientY })
|
||||
setOpen(true)
|
||||
},
|
||||
[guard, setPosition, setOpen]
|
||||
)
|
||||
|
||||
const dialogPositionStyle = useMemo(() => {
|
||||
if (!dialogRef.current)
|
||||
@ -99,9 +78,21 @@ export function ContextMenu({
|
||||
|
||||
// Add context menu listener to target once mounted
|
||||
useEffect(() => {
|
||||
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
console.log('context menu', e)
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.x, y: e.y })
|
||||
setOpen(true)
|
||||
}
|
||||
menuTargetElement?.current?.addEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
return () => {
|
||||
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
|
||||
menuTargetElement?.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
}
|
||||
}, [menuTargetElement?.current])
|
||||
|
||||
@ -109,10 +100,7 @@ export function ContextMenu({
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 w-screen h-screen"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.clientX, y: e.clientY })
|
||||
}}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
>
|
||||
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
||||
<Dialog.Panel
|
||||
|
@ -636,6 +636,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</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: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -266,7 +266,6 @@ const FileTreeItem = ({
|
||||
// Let the lsp servers know we closed a file.
|
||||
onFileClose(currentFile?.path || null, project?.path || null)
|
||||
onFileOpen(fileOrDir.path, project?.path || null)
|
||||
kclManager.switchedFiles = true
|
||||
|
||||
// Open kcl files
|
||||
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
WebGLRenderer,
|
||||
Scene,
|
||||
@ -19,14 +19,16 @@ import {
|
||||
Intersection,
|
||||
Object3D,
|
||||
} from 'three'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuItem,
|
||||
ContextMenuItemRefresh,
|
||||
} from './ContextMenu'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import {
|
||||
useViewControlMenuItems,
|
||||
ViewControlContextMenu,
|
||||
} from './ViewControlMenu'
|
||||
import { AxisNames } from 'lib/constants'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
@ -38,14 +40,64 @@ enum AxisColors {
|
||||
Z = '#6689ef',
|
||||
Gray = '#c6c7c2',
|
||||
}
|
||||
enum AxisNames {
|
||||
X = 'x',
|
||||
Y = 'y',
|
||||
Z = 'z',
|
||||
NEG_X = '-x',
|
||||
NEG_Y = '-y',
|
||||
NEG_Z = '-z',
|
||||
}
|
||||
const axisNamesSemantic: Record<AxisNames, string> = {
|
||||
[AxisNames.X]: 'Right',
|
||||
[AxisNames.Y]: 'Back',
|
||||
[AxisNames.Z]: 'Top',
|
||||
[AxisNames.NEG_X]: 'Left',
|
||||
[AxisNames.NEG_Y]: 'Front',
|
||||
[AxisNames.NEG_Z]: 'Bottom',
|
||||
}
|
||||
|
||||
export default function Gizmo() {
|
||||
const menuItems = useViewControlMenuItems()
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||
const cameraPassiveUpdateTimer = useRef(0)
|
||||
const raycasterPassiveUpdateTimer = useRef(0)
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)),
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
}}
|
||||
>
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
],
|
||||
[axisNamesSemantic]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
@ -109,7 +161,7 @@ export default function Gizmo() {
|
||||
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
||||
>
|
||||
<canvas ref={canvasRef} />
|
||||
<ViewControlContextMenu menuTargetElement={wrapperRef} />
|
||||
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
|
||||
</div>
|
||||
<GizmoDropdown items={menuItems} />
|
||||
</div>
|
||||
|
@ -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 openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||
import { ModelStateIndicator } from './ModelStateIndicator'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export function LowerRightControls({
|
||||
children,
|
||||
coreDumpManager,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
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 (
|
||||
<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}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -69,7 +69,14 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const {
|
||||
auth,
|
||||
settings: {
|
||||
context: {
|
||||
modeling: { defaultUnit },
|
||||
},
|
||||
},
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -85,6 +92,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const initEvent: KclWorkerOptions = {
|
||||
wasmUrl: wasmUrl(),
|
||||
token: token,
|
||||
baseUnit: defaultUnit.current,
|
||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||
}
|
||||
lspWorker.postMessage({
|
||||
|
@ -1,6 +1,39 @@
|
||||
import { useEngineCommands } from './EngineCommands'
|
||||
import { Spinner } from './Spinner'
|
||||
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 = () => {
|
||||
const [commands] = useEngineCommands()
|
||||
|
@ -41,10 +41,7 @@ import {
|
||||
angleBetweenInfo,
|
||||
applyConstraintAngleBetween,
|
||||
} from './Toolbar/SetAngleBetween'
|
||||
import {
|
||||
applyConstraintAngleLength,
|
||||
applyConstraintLength,
|
||||
} from './Toolbar/setAngleLength'
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import {
|
||||
canSweepSelection,
|
||||
handleSelectionBatch,
|
||||
@ -54,8 +51,6 @@ import {
|
||||
Selections,
|
||||
updateSelections,
|
||||
canLoftSelection,
|
||||
canRevolveSelection,
|
||||
canShellSelection,
|
||||
} from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
@ -67,15 +62,13 @@ import {
|
||||
getSketchOrientationDetails,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import {
|
||||
insertNamedConstant,
|
||||
replaceValueAtNodePath,
|
||||
moveValueIntoNewVariablePath,
|
||||
sketchOnExtrudedFace,
|
||||
sketchOnOffsetPlane,
|
||||
startSketchOnDefault,
|
||||
} from 'lang/modifyAst'
|
||||
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||
import {
|
||||
doesSceneHaveExtrudedSketch,
|
||||
doesSceneHaveSweepableSketch,
|
||||
getNodePathFromSourceRange,
|
||||
isSingleCursorInPipe,
|
||||
@ -86,6 +79,7 @@ import toast from 'react-hot-toast'
|
||||
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
@ -105,6 +99,7 @@ type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
streamRef: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
export const ModelingMachineContext = createContext(
|
||||
@ -576,26 +571,6 @@ export const ModelingMachineProvider = ({
|
||||
if (err(canSweep)) return false
|
||||
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 } }) => {
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
@ -611,24 +586,6 @@ export const ModelingMachineProvider = ({
|
||||
if (err(canLoft)) return false
|
||||
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': ({
|
||||
context: { selectionRanges },
|
||||
}) => {
|
||||
@ -913,18 +870,12 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
}
|
||||
),
|
||||
astConstrainLength: fromPromise(
|
||||
async ({
|
||||
input: { selectionRanges, sketchDetails, lengthValue },
|
||||
}) => {
|
||||
if (!lengthValue)
|
||||
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
|
||||
'Get length info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAngleLength({
|
||||
selectionRanges,
|
||||
})
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
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 } }) => {
|
||||
if (!sketchDetails) {
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
}
|
||||
if (!data) {
|
||||
return Promise.reject(new Error('No data from command flow'))
|
||||
}
|
||||
const { variableName } = await getVarNameModal({
|
||||
valueName: data?.variableName || 'var',
|
||||
})
|
||||
let pResult = parse(recast(kclManager.ast))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
let parsed = pResult.program
|
||||
|
||||
let result: {
|
||||
modifiedAst: Node<Program>
|
||||
pathToReplaced: PathToNode | null
|
||||
} = {
|
||||
modifiedAst: parsed,
|
||||
pathToReplaced: null,
|
||||
}
|
||||
// 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,
|
||||
})
|
||||
)
|
||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
||||
moveValueIntoNewVariablePath(
|
||||
parsed,
|
||||
kclManager.programMemory,
|
||||
data?.pathToNode || [],
|
||||
variableName
|
||||
)
|
||||
if (
|
||||
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))
|
||||
pResult = parse(recast(_modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
parsed = pResult.program
|
||||
|
||||
if (trap(parsed)) return Promise.reject(parsed)
|
||||
parsed = parsed as Node<Program>
|
||||
if (!result.pathToReplaced)
|
||||
if (!pathToReplacedNode)
|
||||
return Promise.reject(new Error('No path to replaced node'))
|
||||
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
result.pathToReplaced || [],
|
||||
pathToReplacedNode || [],
|
||||
parsed,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -1187,7 +1088,7 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
{ 0: result.pathToReplaced },
|
||||
{ 0: pathToReplacedNode },
|
||||
selectionRanges,
|
||||
updatedAst.newAst
|
||||
)
|
||||
@ -1195,7 +1096,7 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode: result.pathToReplaced,
|
||||
updatedPathToNode: pathToReplacedNode,
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -1305,13 +1206,10 @@ export const ModelingMachineProvider = ({
|
||||
state: modelingState,
|
||||
context: modelingState.context,
|
||||
send: modelingSend,
|
||||
streamRef,
|
||||
}}
|
||||
>
|
||||
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly?
|
||||
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>
|
||||
{children}
|
||||
</ModelingMachineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ export const ModelingPane = ({
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
aria-label={title && typeof title === 'string' ? title : ''}
|
||||
title={title && typeof title === 'string' ? title : ''}
|
||||
data-testid={detailsTestId}
|
||||
id={id}
|
||||
className={
|
||||
|
@ -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.Item>
|
||||
<button
|
||||
onClick={() => {
|
||||
kclManager.format().catch(reportRejection)
|
||||
}}
|
||||
onClick={() => kclManager.format()}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Format code</span>
|
||||
|
@ -6,6 +6,7 @@ import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { StatusBarItemType } from './statusBar/statusBarTypes'
|
||||
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[NetworkHealthState.Ok]: 'Connected',
|
||||
@ -64,14 +65,28 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
|
||||
},
|
||||
}
|
||||
|
||||
const overallConnectionStateIcon: Record<
|
||||
NetworkHealthState,
|
||||
ActionIconProps['icon']
|
||||
> = {
|
||||
const overallConnectionStateIcon = {
|
||||
[NetworkHealthState.Ok]: 'network',
|
||||
[NetworkHealthState.Weak]: 'network',
|
||||
[NetworkHealthState.Issue]: '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 = () => {
|
||||
@ -109,81 +124,95 @@ export const NetworkHealthIndicator = () => {
|
||||
Network health ({NETWORK_HEALTH_TEXT[overallState]})
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<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"
|
||||
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>
|
||||
<NetworkHealthPopoverContent />
|
||||
</Popover.Panel>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { components } from 'lib/machine-api'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { StatusBarItemType } from './statusBar/statusBarTypes'
|
||||
|
||||
export const NetworkMachineIndicator = ({
|
||||
className,
|
||||
@ -27,12 +28,7 @@ export const NetworkMachineIndicator = ({
|
||||
}
|
||||
data-testid="network-machine-toggle"
|
||||
>
|
||||
<CustomIcon name="printer3d" className="w-5 h-5" />
|
||||
{machineCount > 0 && (
|
||||
<p aria-hidden className="flex items-center justify-center text-xs">
|
||||
{machineCount}
|
||||
</p>
|
||||
)}
|
||||
<NetworkMachinesIcon machineCount={machineCount} />
|
||||
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
|
||||
Network machines ({machineCount}) {reason && `: ${reason}`}
|
||||
</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"
|
||||
data-testid="network-popover"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
<NetworkMachinesPopoverContent machines={machines} />
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
) : 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -10,7 +10,7 @@ import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
@ -68,7 +68,8 @@ function AppLogoLink({
|
||||
data-testid="app-logo"
|
||||
onClick={() => {
|
||||
onProjectClose(file || null, project?.path || null, false)
|
||||
kclManager.switchedFiles = true
|
||||
// Clear the scene.
|
||||
engineCommandManager.clearScene()
|
||||
}}
|
||||
to={PATHS.HOME}
|
||||
className={wrapperClassName + ' hover:before:brightness-110'}
|
||||
@ -189,7 +190,8 @@ function ProjectMenuPopover({
|
||||
className: !isDesktop() ? 'hidden' : '',
|
||||
onClick: () => {
|
||||
onProjectClose(file || null, project?.path || null, true)
|
||||
kclManager.switchedFiles = true
|
||||
// Clear the scene.
|
||||
engineCommandManager.clearScene()
|
||||
},
|
||||
},
|
||||
].filter(
|
||||
|
@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
|
||||
|
||||
export const AllKeybindingsFields = forwardRef(
|
||||
(
|
||||
_props: AllKeybindingsFieldsProps,
|
||||
props: AllKeybindingsFieldsProps,
|
||||
scrollRef: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
// This is how we will get the interaction map from the context
|
||||
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
|
||||
.map(([category, categoryItems]) => (
|
||||
<div className="flex flex-col gap-4 px-2 pr-4">
|
||||
<h2
|
||||
id={`category-${category.replaceAll(/\s/g, '-')}`}
|
||||
id={`category-${category}`}
|
||||
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
||||
>
|
||||
{category}
|
||||
|
@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||
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 {
|
||||
createAndOpenNewTutorialProject,
|
||||
@ -25,6 +25,7 @@ import { useLspContext } from 'components/LspProvider'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { PACKAGE_NAME } from 'routes/Settings'
|
||||
|
||||
interface AllSettingsFieldsProps {
|
||||
searchParamTab: SettingsLevel
|
||||
@ -246,8 +247,10 @@ export const AllSettingsFields = forwardRef(
|
||||
to inject the version from package.json */}
|
||||
App version {APP_VERSION}.{' '}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||
href={getReleaseUrl()}
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`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"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -269,7 +272,7 @@ export const AllSettingsFields = forwardRef(
|
||||
, and start a discussion if you don't see it! Your feedback will
|
||||
help us prioritize what to build next.
|
||||
</p>
|
||||
{!IS_NIGHTLY && (
|
||||
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
|
||||
<p className="max-w-2xl mt-6">
|
||||
Want to experience the latest and (hopefully) greatest from our
|
||||
main development branch?{' '}
|
||||
|
@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
|
||||
key={category}
|
||||
onClick={() =>
|
||||
scrollRef.current
|
||||
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`)
|
||||
?.querySelector(`#category-${category}`)
|
||||
?.scrollIntoView({
|
||||
block: 'center',
|
||||
behavior: 'smooth',
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
@ -23,6 +23,7 @@ import {
|
||||
engineCommandManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
@ -54,15 +55,11 @@ type SettingsAuthContextType = {
|
||||
settings: MachineContext<typeof settingsMachine>
|
||||
}
|
||||
|
||||
/**
|
||||
* This variable is used to store the last snapshot of the settings context
|
||||
* for use outside of React, such as in `wasm.ts`. It is updated every time
|
||||
* the settings machine changes with `useSelector`.
|
||||
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
|
||||
*/
|
||||
export let lastSettingsContextSnapshot:
|
||||
| ContextFrom<typeof settingsMachine>
|
||||
| undefined
|
||||
// a little hacky for sure, open to changing it
|
||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
||||
// but I think that's a safe assumption
|
||||
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
||||
export const getSettingsState = () => settingsStateRef
|
||||
|
||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||
|
||||
@ -132,11 +129,27 @@ export const SettingsAuthProviderBase = ({
|
||||
.setTheme(context.app.theme.current)
|
||||
.catch(reportRejection)
|
||||
},
|
||||
setEngineScaleGridVisibility: ({ context }) => {
|
||||
engineCommandManager.setScaleGridVisibility(
|
||||
context.modeling.showScaleGrid.current
|
||||
)
|
||||
},
|
||||
setClientTheme: ({ context }) => {
|
||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||
sceneInfra.theme = 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 }) => {
|
||||
if (!('data' in event)) return
|
||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||
@ -162,27 +175,17 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
'Execute AST': ({ context, event }) => {
|
||||
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 =
|
||||
event.type === 'Set all settings' &&
|
||||
relevantSetting(event.settings)
|
||||
event.settings?.modeling?.defaultUnit?.current !==
|
||||
context.modeling.defaultUnit.current
|
||||
const resetSettingsIncludesUnitChange =
|
||||
event.type === 'Reset settings' && relevantSetting(settings)
|
||||
event.type === 'Reset settings' &&
|
||||
context.modeling.defaultUnit.current !==
|
||||
settings?.modeling?.defaultUnit?.default
|
||||
|
||||
if (
|
||||
event.type === 'set.modeling.defaultUnit' ||
|
||||
event.type === 'set.modeling.showScaleGrid' ||
|
||||
event.type === 'set.modeling.highlightEdges' ||
|
||||
allSettingsIncludesUnitChange ||
|
||||
resetSettingsIncludesUnitChange
|
||||
) {
|
||||
@ -211,10 +214,7 @@ export const SettingsAuthProviderBase = ({
|
||||
}),
|
||||
{ input: loadedSettings }
|
||||
)
|
||||
// Any time the actor changes, update the settings state for external use
|
||||
useSelector(settingsActor, (s) => {
|
||||
lastSettingsContextSnapshot = s.context
|
||||
})
|
||||
settingsStateRef = settingsState.context
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
|
148
src/components/StatusBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@ import { IndexLoaderData } from 'lib/types'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
@ -31,7 +30,6 @@ enum StreamState {
|
||||
|
||||
export const Stream = () => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { state, send } = useModelingContext()
|
||||
@ -260,7 +258,7 @@ export const Stream = () => {
|
||||
setIsLoading(false)
|
||||
}, [mediaStream])
|
||||
|
||||
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
// If we've got no stream or connection, don't do anything
|
||||
if (!isNetworkOkay) return
|
||||
if (!videoRef.current) return
|
||||
@ -322,11 +320,10 @@ export const Stream = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={videoWrapperRef}
|
||||
className="absolute inset-0 z-0"
|
||||
id="stream"
|
||||
data-testid="stream"
|
||||
onClick={handleClick}
|
||||
onClick={handleMouseUp}
|
||||
onDoubleClick={enterSketchModeIfSelectingSketch}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
@ -387,14 +384,6 @@ export const Stream = () => {
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
<ViewControlContextMenu
|
||||
event="mouseup"
|
||||
guard={(e) =>
|
||||
sceneInfra.camControls.wasDragging === false &&
|
||||
btnName(e).right === true
|
||||
}
|
||||
menuTargetElement={videoWrapperRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import toast from 'react-hot-toast'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
import { getReleaseUrl } from 'routes/Settings'
|
||||
|
||||
export function ToastUpdate({
|
||||
version,
|
||||
@ -33,8 +32,10 @@ export function ToastUpdate({
|
||||
A new update has downloaded and will be available next time you
|
||||
start the app. You can view the release notes{' '}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
|
||||
href={getReleaseUrl(version)}
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
|
||||
)}
|
||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
@ -22,7 +22,6 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { normaliseAngle } from '../../lib/utils'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
|
||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||
|
||||
@ -64,57 +63,6 @@ export function angleLengthInfo({
|
||||
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({
|
||||
selectionRanges,
|
||||
angleOrLength = 'setLength',
|
||||
|
@ -8,7 +8,7 @@ type LeftOrRight = 'left' | 'right'
|
||||
type Corner = `${TopOrBottom}-${LeftOrRight}`
|
||||
type TooltipPosition = TopOrBottom | LeftOrRight | Corner
|
||||
|
||||
interface TooltipProps extends React.PropsWithChildren {
|
||||
export interface TooltipProps extends React.PropsWithChildren {
|
||||
position?: TooltipPosition
|
||||
wrapperClassName?: string
|
||||
contentClassName?: string
|
||||
|
@ -41,10 +41,7 @@ export function UnitsMenu() {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||
{unit === settings.context.modeling.defaultUnit.current && (
|
||||
<span className="text-chalkboard-60">current</span>
|
||||
)}
|
||||
{baseUnitLabels[unit]}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
@ -1,66 +0,0 @@
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuItem,
|
||||
ContextMenuItemRefresh,
|
||||
ContextMenuProps,
|
||||
} from './ContextMenu'
|
||||
import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useMemo } from 'react'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
|
||||
export function useViewControlMenuItems() {
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)),
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
}}
|
||||
>
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
],
|
||||
[VIEW_NAMES_SEMANTIC]
|
||||
)
|
||||
return menuItems
|
||||
}
|
||||
|
||||
export function ViewControlContextMenu({
|
||||
menuTargetElement: wrapperRef,
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const menuItems = useViewControlMenuItems()
|
||||
return (
|
||||
<ContextMenu
|
||||
data-testid="view-controls-menu"
|
||||
menuTargetElement={wrapperRef}
|
||||
items={menuItems}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
96
src/components/statusBar/homeDefaultStatusBarItems.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
28
src/components/statusBar/statusBarTypes.ts
Normal 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'
|
||||
}
|
||||
)
|
@ -1,327 +0,0 @@
|
||||
import {
|
||||
EditorView,
|
||||
WidgetType,
|
||||
ViewUpdate,
|
||||
ViewPlugin,
|
||||
DecorationSet,
|
||||
Decoration,
|
||||
} from '@codemirror/view'
|
||||
import { Range, Extension, Text } from '@codemirror/state'
|
||||
import { NodeProp, Tree } from '@lezer/common'
|
||||
import { language, syntaxTree } from '@codemirror/language'
|
||||
|
||||
interface PickerState {
|
||||
from: number
|
||||
to: number
|
||||
alpha: string
|
||||
colorType: ColorType
|
||||
}
|
||||
|
||||
export interface WidgetOptions extends PickerState {
|
||||
color: string
|
||||
}
|
||||
|
||||
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
|
||||
|
||||
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
|
||||
|
||||
export enum ColorType {
|
||||
hex = 'HEX',
|
||||
}
|
||||
|
||||
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
|
||||
|
||||
function discoverColorsInKCL(
|
||||
syntaxTree: Tree,
|
||||
from: number,
|
||||
to: number,
|
||||
typeName: string,
|
||||
doc: Text,
|
||||
language?: string
|
||||
): WidgetOptions | Array<WidgetOptions> | null {
|
||||
switch (typeName) {
|
||||
case 'Program':
|
||||
case 'VariableDeclaration':
|
||||
case 'CallExpression':
|
||||
case 'ObjectExpression':
|
||||
case 'ObjectProperty':
|
||||
case 'ArgumentList':
|
||||
case 'PipeExpression': {
|
||||
let innerTree = syntaxTree.resolveInner(from, 0).tree
|
||||
|
||||
if (!innerTree) {
|
||||
innerTree = syntaxTree.resolveInner(from, 1).tree
|
||||
if (!innerTree) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
|
||||
|
||||
if (overlayTree?.type.name !== 'Styles') {
|
||||
return null
|
||||
}
|
||||
|
||||
const ret: Array<WidgetOptions> = []
|
||||
overlayTree.iterate({
|
||||
from: 0,
|
||||
to: overlayTree.length,
|
||||
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
|
||||
const maybeWidgetOptions = discoverColorsInKCL(
|
||||
syntaxTree,
|
||||
// We add one because the tree doesn't include the
|
||||
// quotation mark from the style tag
|
||||
from + 1 + overlayFrom,
|
||||
from + 1 + overlayTo,
|
||||
type.name,
|
||||
doc,
|
||||
language
|
||||
)
|
||||
|
||||
if (maybeWidgetOptions) {
|
||||
if (Array.isArray(maybeWidgetOptions)) {
|
||||
console.error('Unexpected nested overlays')
|
||||
ret.push(...maybeWidgetOptions)
|
||||
} else {
|
||||
ret.push(maybeWidgetOptions)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
case 'String': {
|
||||
const result = parseColorLiteral(doc.sliceString(from, to))
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function parseColorLiteral(colorLiteral: string): ColorData | null {
|
||||
const literal = colorLiteral.replace(/"/g, '')
|
||||
const match = hexRegex.exec(literal)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const [color, alpha] = toFullHex(literal)
|
||||
|
||||
return {
|
||||
colorType: ColorType.hex,
|
||||
color,
|
||||
alpha,
|
||||
}
|
||||
}
|
||||
|
||||
function colorPickersDecorations(
|
||||
view: EditorView,
|
||||
discoverColors: typeof discoverColorsInKCL
|
||||
) {
|
||||
const widgets: Array<Range<Decoration>> = []
|
||||
|
||||
const st = syntaxTree(view.state)
|
||||
|
||||
for (const range of view.visibleRanges) {
|
||||
st.iterate({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
enter: ({ type, from, to }) => {
|
||||
const maybeWidgetOptions = discoverColors(
|
||||
st,
|
||||
from,
|
||||
to,
|
||||
type.name,
|
||||
view.state.doc,
|
||||
view.state.facet(language)?.name
|
||||
)
|
||||
|
||||
if (!maybeWidgetOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(maybeWidgetOptions)) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||
side: 1,
|
||||
}).range(maybeWidgetOptions.from)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const wo of maybeWidgetOptions) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(wo),
|
||||
side: 1,
|
||||
}).range(wo.from)
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return Decoration.set(widgets)
|
||||
}
|
||||
|
||||
function toFullHex(color: string): string[] {
|
||||
if (color.length === 4) {
|
||||
// 3-char hex
|
||||
return [
|
||||
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||
'',
|
||||
]
|
||||
}
|
||||
|
||||
if (color.length === 5) {
|
||||
// 4-char hex (alpha)
|
||||
return [
|
||||
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||
color[4].repeat(2),
|
||||
]
|
||||
}
|
||||
|
||||
if (color.length === 9) {
|
||||
// 8-char hex (alpha)
|
||||
return [`#${color.slice(1, -2)}`, color.slice(-2)]
|
||||
}
|
||||
|
||||
return [color, '']
|
||||
}
|
||||
|
||||
export const wrapperClassName = 'cm-css-color-picker-wrapper'
|
||||
|
||||
class ColorPickerWidget extends WidgetType {
|
||||
private readonly state: PickerState
|
||||
private readonly color: string
|
||||
|
||||
constructor({ color, ...state }: WidgetOptions) {
|
||||
super()
|
||||
this.state = state
|
||||
this.color = color
|
||||
}
|
||||
|
||||
eq(other: ColorPickerWidget) {
|
||||
return (
|
||||
other.state.colorType === this.state.colorType &&
|
||||
other.color === this.color &&
|
||||
other.state.from === this.state.from &&
|
||||
other.state.to === this.state.to &&
|
||||
other.state.alpha === this.state.alpha
|
||||
)
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const picker = document.createElement('input')
|
||||
pickerState.set(picker, this.state)
|
||||
picker.type = 'color'
|
||||
picker.value = this.color
|
||||
|
||||
const wrapper = document.createElement('span')
|
||||
wrapper.appendChild(picker)
|
||||
wrapper.className = wrapperClassName
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const colorPickerTheme = EditorView.baseTheme({
|
||||
[`.${wrapperClassName}`]: {
|
||||
display: 'inline-block',
|
||||
outline: '1px solid #eee',
|
||||
marginRight: '0.6ch',
|
||||
height: '1em',
|
||||
width: '1em',
|
||||
transform: 'translateY(1px)',
|
||||
},
|
||||
[`.${wrapperClassName} input[type="color"]`]: {
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
'&::-webkit-color-swatch-wrapper': {
|
||||
padding: 0,
|
||||
},
|
||||
'&::-webkit-color-swatch': {
|
||||
border: 'none',
|
||||
},
|
||||
'&::-moz-color-swatch': {
|
||||
border: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
interface IFactoryOptions {
|
||||
discoverColors: typeof discoverColorsInKCL
|
||||
}
|
||||
|
||||
export const makeColorPicker = (options: IFactoryOptions) =>
|
||||
ViewPlugin.fromClass(
|
||||
class ColorPickerViewPlugin {
|
||||
decorations: DecorationSet
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = colorPickersDecorations(view, options.discoverColors)
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = colorPickersDecorations(
|
||||
update.view,
|
||||
options.discoverColors
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
eventHandlers: {
|
||||
change: (e, view) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (
|
||||
target.nodeName !== 'INPUT' ||
|
||||
!target.parentElement ||
|
||||
!target.parentElement.classList.contains(wrapperClassName)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data = pickerState.get(target)!
|
||||
|
||||
let converted = '"' + target.value + data.alpha + '"'
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
insert: converted,
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const colorPicker: Extension = [
|
||||
makeColorPicker({ discoverColors: discoverColorsInKCL }),
|
||||
colorPickerTheme,
|
||||
]
|
@ -17,7 +17,6 @@ import { kclPlugin } from '.'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
// @ts-ignore: No types available
|
||||
import { parser } from './kcl.grammar'
|
||||
import { colorPicker } from './colors'
|
||||
|
||||
export interface LanguageOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[]
|
||||
@ -55,14 +54,14 @@ export const KclLanguage = LRLanguage.define({
|
||||
})
|
||||
|
||||
export function kcl(options: LanguageOptions) {
|
||||
return new LanguageSupport(KclLanguage, [
|
||||
colorPicker,
|
||||
return new LanguageSupport(
|
||||
KclLanguage,
|
||||
kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
processLspNotification: options.processLspNotification,
|
||||
}),
|
||||
])
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
||||
|
||||
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
||||
|
||||
export enum LspWorker {
|
||||
Kcl = 'kcl',
|
||||
Copilot = 'copilot',
|
||||
@ -7,6 +9,7 @@ export enum LspWorker {
|
||||
export interface KclWorkerOptions {
|
||||
wasmUrl: string
|
||||
token: string
|
||||
baseUnit: UnitLength
|
||||
apiBaseUrl: string
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
KclWorkerOptions,
|
||||
CopilotWorkerOptions,
|
||||
} from 'editor/plugins/lsp/types'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
@ -45,12 +46,14 @@ export async function copilotLspRun(
|
||||
|
||||
export async function kclLspRun(
|
||||
config: ServerConfig,
|
||||
engineCommandManager: EngineCommandManager | null,
|
||||
token: string,
|
||||
baseUnit: string,
|
||||
baseUrl: string
|
||||
) {
|
||||
try {
|
||||
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) {
|
||||
console.log('kcl lsp failed', e)
|
||||
// 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) {
|
||||
case LspWorker.Kcl:
|
||||
const kclData = eventData as KclWorkerOptions
|
||||
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
|
||||
await kclLspRun(
|
||||
config,
|
||||
null,
|
||||
kclData.token,
|
||||
kclData.baseUnit,
|
||||
kclData.apiBaseUrl
|
||||
)
|
||||
break
|
||||
case LspWorker.Copilot:
|
||||
let copilotData = eventData as CopilotWorkerOptions
|
||||
|
@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { makeDefaultPlanes } from 'lang/wasm'
|
||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { useAppState, useAppStream } from 'AppState'
|
||||
@ -56,6 +56,9 @@ export function useSetupEngineManager(
|
||||
makeDefaultPlanes: () => {
|
||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||
},
|
||||
modifyGrid: (hidden: boolean) => {
|
||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
||||
},
|
||||
})
|
||||
hasSetNonZeroDimensions.current = true
|
||||
}
|
||||
|
@ -24,8 +24,6 @@ export function useConvertToVariable(range?: SourceRange) {
|
||||
}, [enable])
|
||||
|
||||
useEffect(() => {
|
||||
// Return early if there are no selection ranges for whatever reason
|
||||
if (!context.selectionRanges) return
|
||||
const parsed = ast
|
||||
|
||||
const meta = isNodeSafeToReplace(
|
||||
|
@ -317,8 +317,3 @@ code {
|
||||
#code-mirror-override .cm-editor {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Can't use #code-mirror-override here as we're outside of this div */
|
||||
.body-bg .cm-diagnosticAction {
|
||||
@apply bg-primary;
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||
|
||||
import {
|
||||
CallExpression,
|
||||
clearSceneAndBustCache,
|
||||
emptyExecState,
|
||||
ExecState,
|
||||
initPromise,
|
||||
@ -61,7 +60,6 @@ export class KclManager {
|
||||
private _executeIsStale: ExecuteArgs | null = null
|
||||
private _wasmInitFailed = true
|
||||
private _hasErrors = false
|
||||
private _switchedFiles = false
|
||||
|
||||
engineCommandManager: EngineCommandManager
|
||||
|
||||
@ -81,10 +79,6 @@ export class KclManager {
|
||||
this._astCallBack(ast)
|
||||
}
|
||||
|
||||
set switchedFiles(switchedFiles: boolean) {
|
||||
this._switchedFiles = switchedFiles
|
||||
}
|
||||
|
||||
get programMemory() {
|
||||
return this._programMemory
|
||||
}
|
||||
@ -172,12 +166,8 @@ export class KclManager {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ensureWasmInit().then(async () => {
|
||||
await this.safeParse(codeManager.code).then((ast) => {
|
||||
if (ast) {
|
||||
this.ast = ast
|
||||
}
|
||||
})
|
||||
this.ensureWasmInit().then(() => {
|
||||
this.ast = this.safeParse(codeManager.code) || this.ast
|
||||
})
|
||||
}
|
||||
|
||||
@ -221,25 +211,7 @@ export class KclManager {
|
||||
}
|
||||
}
|
||||
|
||||
// (jess) I'm not in love with this, but it ensures we clear the scene and
|
||||
// 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> {
|
||||
safeParse(code: string): Node<Program> | null {
|
||||
const result = parse(code)
|
||||
this.diagnostics = []
|
||||
this._hasErrors = false
|
||||
@ -248,8 +220,6 @@ export class KclManager {
|
||||
const kclerror: KCLError = result as KCLError
|
||||
this.diagnostics = kclErrorsToDiagnostics([kclerror])
|
||||
this._hasErrors = true
|
||||
|
||||
await this.checkIfSwitchedFilesShouldClear()
|
||||
return null
|
||||
}
|
||||
|
||||
@ -258,7 +228,6 @@ export class KclManager {
|
||||
if (result.errors.length > 0) {
|
||||
this._hasErrors = true
|
||||
|
||||
await this.checkIfSwitchedFilesShouldClear()
|
||||
return null
|
||||
}
|
||||
|
||||
@ -384,7 +353,7 @@ export class KclManager {
|
||||
console.error(newCode)
|
||||
return
|
||||
}
|
||||
const newAst = await this.safeParse(newCode)
|
||||
const newAst = this.safeParse(newCode)
|
||||
if (!newAst) {
|
||||
this.clearAst()
|
||||
return
|
||||
@ -439,7 +408,7 @@ export class KclManager {
|
||||
})
|
||||
}
|
||||
async executeCode(zoomToFit?: boolean): Promise<void> {
|
||||
const ast = await this.safeParse(codeManager.code)
|
||||
const ast = this.safeParse(codeManager.code)
|
||||
if (!ast) {
|
||||
this.clearAst()
|
||||
return
|
||||
@ -447,9 +416,9 @@ export class KclManager {
|
||||
this.ast = { ...ast }
|
||||
return this.executeAst({ zoomToFit })
|
||||
}
|
||||
async format() {
|
||||
format() {
|
||||
const originalCode = codeManager.code
|
||||
const ast = await this.safeParse(originalCode)
|
||||
const ast = this.safeParse(originalCode)
|
||||
if (!ast) {
|
||||
this.clearAst()
|
||||
return
|
||||
@ -489,7 +458,7 @@ export class KclManager {
|
||||
const newCode = recast(ast)
|
||||
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'))
|
||||
let returnVal: Selections | undefined = undefined
|
||||
|
||||
|
@ -60,7 +60,8 @@ const b1 = cube([0,0], 10)`
|
||||
expect(nodePath).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['params', 'FunctionExpression'],
|
||||
[0, 'index'],
|
||||
@ -95,12 +96,14 @@ const b1 = cube([0,0], 10)`
|
||||
expect(nodePath).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['body', 'FunctionExpression'],
|
||||
['body', 'FunctionExpression'],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['body', 'PipeExpression'],
|
||||
[2, 'index'],
|
||||
|
@ -82,11 +82,11 @@ describe('Testing createVariableDeclaration', () => {
|
||||
it('should create a variable declaration', () => {
|
||||
const result = createVariableDeclaration('myVar', createLiteral(5))
|
||||
expect(result.type).toBe('VariableDeclaration')
|
||||
expect(result.declaration.type).toBe('VariableDeclarator')
|
||||
expect(result.declaration.id.type).toBe('Identifier')
|
||||
expect(result.declaration.id.name).toBe('myVar')
|
||||
expect(result.declaration.init.type).toBe('Literal')
|
||||
expect((result.declaration.init as any).value).toBe(5)
|
||||
expect(result.declarations[0].type).toBe('VariableDeclarator')
|
||||
expect(result.declarations[0].id.type).toBe('Identifier')
|
||||
expect(result.declarations[0].id.name).toBe('myVar')
|
||||
expect(result.declarations[0].init.type).toBe('Literal')
|
||||
expect((result.declarations[0].init as any).value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createPipeExpression', () => {
|
||||
|
@ -45,7 +45,6 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { KclExpressionWithVariable } from 'lib/commandTypes'
|
||||
|
||||
export function startSketchOnDefault(
|
||||
node: Node<Program>,
|
||||
@ -67,7 +66,8 @@ export function startSketchOnDefault(
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndex, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
|
||||
@ -94,7 +94,7 @@ export function addStartProfileAt(
|
||||
return new Error('variableDeclaration.init.type !== PipeExpression')
|
||||
}
|
||||
const _node = { ...node }
|
||||
const init = variableDeclaration.declaration.init
|
||||
const init = variableDeclaration.declarations[0].init
|
||||
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
||||
createArrayExpression([
|
||||
createLiteral(roundOff(at[0])),
|
||||
@ -105,7 +105,7 @@ export function addStartProfileAt(
|
||||
if (init.type === 'PipeExpression') {
|
||||
init.body.splice(1, 0, startProfileAt)
|
||||
} else {
|
||||
variableDeclaration.declaration.init = createPipeExpression([
|
||||
variableDeclaration.declarations[0].init = createPipeExpression([
|
||||
init,
|
||||
startProfileAt,
|
||||
])
|
||||
@ -149,7 +149,8 @@ export function addSketchTo(
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndex, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
if (axis !== 'xy') {
|
||||
@ -332,7 +333,8 @@ export function extrudeSketch(
|
||||
const pathToExtrudeArg: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndexInBody + 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
@ -362,7 +364,8 @@ export function loftSketches(
|
||||
const pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
@ -457,7 +460,8 @@ export function revolveSketch(
|
||||
const pathToRevolveArg: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndexInBody + 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
@ -543,7 +547,8 @@ export function sketchOnExtrudedFace(
|
||||
const newpathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[expressionIndex + 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
|
||||
@ -580,7 +585,8 @@ export function addOffsetPlane({
|
||||
const pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[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
|
||||
* of an offset plane. The new sketch just has to come after the offset
|
||||
@ -836,15 +823,17 @@ export function createVariableDeclaration(
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
declaration: {
|
||||
type: 'VariableDeclarator',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
id: createIdentifier(varName),
|
||||
init,
|
||||
},
|
||||
id: createIdentifier(varName),
|
||||
init,
|
||||
},
|
||||
],
|
||||
visibility,
|
||||
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(
|
||||
ast: Node<Program>,
|
||||
programMemory: ProgramMemory,
|
||||
@ -1156,7 +1120,7 @@ export async function deleteFromSelection(
|
||||
traverse(astClone, {
|
||||
enter: (node, path) => {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
const dec = node.declaration
|
||||
const dec = node.declarations[0]
|
||||
if (
|
||||
dec.init.type === 'CallExpression' &&
|
||||
(dec.init.callee.name === 'extrude' ||
|
||||
@ -1191,7 +1155,7 @@ export async function deleteFromSelection(
|
||||
enter: (node, path) => {
|
||||
;(async () => {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
currentVariableName = node.declaration.id.name
|
||||
currentVariableName = node.declarations[0].id.name
|
||||
}
|
||||
if (
|
||||
// match startSketchOn(${extrudeNameToDelete})
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { createLiteral } from 'lang/modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
@ -40,6 +40,7 @@ beforeAll(async () => {
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
callbackOnEngineLiteConnect: () => {
|
||||
resolve(true)
|
||||
},
|
||||
@ -117,8 +118,13 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||
true,
|
||||
]
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
const selection: Selections = {
|
||||
graphSelections: [
|
||||
{
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
}
|
||||
|
||||
// executeAst and artifactGraph
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
sketchLineHelperMap,
|
||||
} from '../std/sketch'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import {
|
||||
Artifact,
|
||||
@ -99,9 +99,14 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||
|
||||
for (const selection of selections.graphSelections) {
|
||||
const singleSelection = {
|
||||
graphSelections: [selection],
|
||||
otherSelections: [],
|
||||
}
|
||||
|
||||
const result = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
selection,
|
||||
singleSelection,
|
||||
artifactGraph
|
||||
)
|
||||
if (err(result)) return result
|
||||
@ -254,12 +259,12 @@ function insertParametersIntoAst(
|
||||
|
||||
export function getPathToExtrudeForSegmentSelection(
|
||||
ast: Program,
|
||||
selection: Selection,
|
||||
selection: Selections,
|
||||
artifactGraph: ArtifactGraph
|
||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.codeRef?.range
|
||||
selection.graphSelections[0]?.codeRef?.range
|
||||
)
|
||||
|
||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||
@ -268,7 +273,7 @@ export function getPathToExtrudeForSegmentSelection(
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDecNode)) return varDecNode
|
||||
const sketchVar = varDecNode.node.declaration.id.name
|
||||
const sketchVar = varDecNode.node.declarations[0].id.name
|
||||
|
||||
const sketch = sketchFromKclValue(
|
||||
kclManager.programMemory.get(sketchVar),
|
||||
@ -303,7 +308,7 @@ async function updateAstAndFocus(
|
||||
}
|
||||
}
|
||||
|
||||
export function mutateAstWithTagForSketchSegment(
|
||||
function mutateAstWithTagForSketchSegment(
|
||||
astClone: Node<Program>,
|
||||
pathToSegmentNode: PathToNode
|
||||
): { modifiedAst: Program; tag: string } | Error {
|
||||
@ -335,7 +340,7 @@ export function mutateAstWithTagForSketchSegment(
|
||||
return { modifiedAst: astClone, tag }
|
||||
}
|
||||
|
||||
export function getEdgeTagCall(
|
||||
function getEdgeTagCall(
|
||||
tag: string,
|
||||
artifact: Artifact
|
||||
): Node<Identifier | CallExpression> {
|
||||
@ -362,7 +367,7 @@ function locateExtrudeDeclarator(
|
||||
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
|
||||
|
||||
const { node: extrudeVarDecl } = nodeOfExtrudeCall
|
||||
const extrudeDeclarator = extrudeVarDecl.declaration
|
||||
const extrudeDeclarator = extrudeVarDecl.declarations[0]
|
||||
if (!extrudeDeclarator) {
|
||||
return new Error('Extrude Declarator not found.')
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@ import {
|
||||
doesSceneHaveSweepableSketch,
|
||||
traverse,
|
||||
getNodeFromPath,
|
||||
doesSceneHaveExtrudedSketch,
|
||||
} from './queryAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import {
|
||||
@ -231,7 +230,8 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['body', 'PipeExpression'],
|
||||
[2, 'index'],
|
||||
@ -250,7 +250,8 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const expected = [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['body', 'PipeExpression'],
|
||||
[3, 'index'],
|
||||
@ -292,7 +293,8 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['cond', 'IfExpression'],
|
||||
['left', 'BinaryExpression'],
|
||||
@ -322,7 +324,8 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['then_val', 'IfExpression'],
|
||||
['body', 'IfExpression'],
|
||||
@ -350,8 +353,7 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['selector', 'ImportStatement'],
|
||||
['items', 'ImportSelector'],
|
||||
['items', 'ImportStatement'],
|
||||
[1, 'index'],
|
||||
['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', () => {
|
||||
it.each([
|
||||
['basic', '2.73'],
|
||||
|
@ -259,26 +259,34 @@ function moreNodePathFromSourceRange(
|
||||
return moreNodePathFromSourceRange(expression, sourceRange, path)
|
||||
}
|
||||
if (_node.type === 'VariableDeclaration' && isInRange) {
|
||||
const declaration = _node.declaration
|
||||
const declarations = _node.declarations
|
||||
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
path.push(['declaration', 'VariableDeclaration'])
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push(['init', ''])
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
|
||||
const declaration = declarations[decIndex]
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
path.push(['declarations', 'VariableDeclaration'])
|
||||
path.push([decIndex, 'index'])
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push(['init', ''])
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_node.type === 'VariableDeclaration' && isInRange) {
|
||||
const declaration = _node.declaration
|
||||
const declarations = _node.declarations
|
||||
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push(['declaration', 'VariableDeclaration'])
|
||||
path.push(['init', ''])
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
|
||||
const declaration = declarations[decIndex]
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push(['declarations', 'VariableDeclaration'])
|
||||
path.push([decIndex, 'index'])
|
||||
path.push(['init', ''])
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return path
|
||||
@ -372,31 +380,24 @@ function moreNodePathFromSourceRange(
|
||||
}
|
||||
|
||||
if (_node.type === 'ImportStatement' && isInRange) {
|
||||
if (_node.selector && _node.selector.type === 'List') {
|
||||
path.push(['selector', 'ImportStatement'])
|
||||
const { items } = _node.selector
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (item.start <= start && item.end >= end) {
|
||||
path.push(['items', 'ImportSelector'])
|
||||
path.push([i, 'index'])
|
||||
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
|
||||
}
|
||||
const { items } = _node
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (item.start <= start && item.end >= end) {
|
||||
path.push(['items', 'ImportStatement'])
|
||||
path.push([i, 'index'])
|
||||
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
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
console.error('not implemented: ' + node.type)
|
||||
@ -450,10 +451,13 @@ export function traverse(
|
||||
traverse(node, option, pathToNode)
|
||||
|
||||
if (_node.type === 'VariableDeclaration') {
|
||||
_traverse(_node.declaration, [
|
||||
...pathToNode,
|
||||
['declaration', 'VariableDeclaration'],
|
||||
])
|
||||
_node.declarations.forEach((declaration, index) =>
|
||||
_traverse(declaration, [
|
||||
...pathToNode,
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[index, 'index'],
|
||||
])
|
||||
)
|
||||
} else if (_node.type === 'VariableDeclarator') {
|
||||
_traverse(_node.init, [...pathToNode, ['init', '']])
|
||||
} else if (_node.type === 'PipeExpression') {
|
||||
@ -563,7 +567,7 @@ export function findAllPreviousVariablesPath(
|
||||
const variables: PrevVariable<any>[] = []
|
||||
bodyItems?.forEach?.((item) => {
|
||||
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)
|
||||
if (!varValue || typeof varValue?.value !== type) return
|
||||
variables.push({
|
||||
@ -757,7 +761,7 @@ export function isLinesParallelAndConstrained(
|
||||
const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration')
|
||||
if (err(_varDec)) return _varDec
|
||||
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)
|
||||
if (err(sg)) return sg
|
||||
const _primarySegment = getSketchSegmentFromSourceRange(
|
||||
@ -877,7 +881,7 @@ export function hasExtrudeSketch({
|
||||
}
|
||||
const varDec = varDecMeta.node
|
||||
if (varDec.type !== 'VariableDeclaration') return false
|
||||
const varName = varDec.declaration.id.name
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const varValue = programMemory?.get(varName)
|
||||
return (
|
||||
varValue?.type === 'Solid' ||
|
||||
@ -1064,35 +1068,6 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
|
||||
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(
|
||||
node: ObjectExpression,
|
||||
propName: string
|
||||
|
@ -139,6 +139,7 @@ beforeAll(async () => {
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
callbackOnEngineLiteConnect: async () => {
|
||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||
|
@ -1399,6 +1399,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
|
||||
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
|
||||
|
||||
private onEngineConnectionOpened = () => {}
|
||||
private onEngineConnectionClosed = () => {}
|
||||
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
height,
|
||||
token,
|
||||
makeDefaultPlanes,
|
||||
modifyGrid,
|
||||
settings = {
|
||||
pool: null,
|
||||
theme: Themes.Dark,
|
||||
@ -1450,12 +1452,14 @@ export class EngineCommandManager extends EventTarget {
|
||||
height: number
|
||||
token?: string
|
||||
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||
modifyGrid: (hidden: boolean) => Promise<void>
|
||||
settings?: SettingsViaQueryString
|
||||
}) {
|
||||
if (settings) {
|
||||
this.settings = settings
|
||||
}
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
this.modifyGrid = modifyGrid
|
||||
if (width === 0 || height === 0) {
|
||||
return
|
||||
}
|
||||
@ -1535,15 +1539,21 @@ export class EngineCommandManager extends EventTarget {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await this.initPlanes()
|
||||
setIsStreamReady(true)
|
||||
// We want modify the grid first because we don't want it to flash.
|
||||
// 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.
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
|
||||
detail: this.engineConnection,
|
||||
})
|
||||
)
|
||||
// Other parts of the application should use this to react on scene ready.
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
|
||||
detail: this.engineConnection,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
this.engineConnection.addEventListener(
|
||||
@ -1869,6 +1879,17 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
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) {
|
||||
if (this.commandLogs.length > 500) {
|
||||
this.commandLogs.shift()
|
||||
@ -2202,6 +2223,15 @@ export class EngineCommandManager extends EventTarget {
|
||||
}).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.
|
||||
// So when passing a range, we need to also specify the command type
|
||||
mapRangeToObjectId(
|
||||
|
@ -164,7 +164,8 @@ mySketch001 = startSketchOn('XY')
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
],
|
||||
})
|
||||
@ -188,7 +189,8 @@ mySketch001 = startSketchOn('XY')
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
],
|
||||
})
|
||||
|
@ -1701,7 +1701,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
|
||||
const { node: varDec } = nodeMeta2
|
||||
const varName = varDec.declaration.id.name
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const sketch = sketchFromKclValue(
|
||||
previousProgramMemory.get(varName),
|
||||
varName
|
||||
|
@ -111,10 +111,12 @@ export function isSketchVariablesLinked(
|
||||
let nextVarDec: VariableDeclarator | undefined
|
||||
for (const node of ast.body) {
|
||||
if (node.type !== 'VariableDeclaration') continue
|
||||
if (node.declaration.id.name === secondArg.name) {
|
||||
nextVarDec = node.declaration
|
||||
break
|
||||
}
|
||||
const found = node.declarations.find(
|
||||
({ id }) => id?.name === secondArg.name
|
||||
)
|
||||
if (!found) continue
|
||||
nextVarDec = found
|
||||
break
|
||||
}
|
||||
if (!nextVarDec) return false
|
||||
return isSketchVariablesLinked(nextVarDec, primaryVarDec, ast)
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { err } from 'lib/trap'
|
||||
import { initPromise, parse, ParseResult } from './wasm'
|
||||
import { parse, ParseResult } from './wasm'
|
||||
import { enginelessExecutor } from 'lib/testHelpers'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
|
||||
beforeEach(async () => {
|
||||
await initPromise
|
||||
})
|
||||
|
||||
it('can execute parsed AST', async () => {
|
||||
const code = `x = 1
|
||||
// A comment.`
|
||||
|
@ -1,13 +1,14 @@
|
||||
import init, {
|
||||
parse_wasm,
|
||||
recast_wasm,
|
||||
execute,
|
||||
execute_wasm,
|
||||
kcl_lint,
|
||||
modify_ast_for_sketch_wasm,
|
||||
is_points_ccw,
|
||||
get_tangential_arc_to_info,
|
||||
program_memory_init,
|
||||
make_default_planes,
|
||||
modify_grid,
|
||||
coredump,
|
||||
toml_stringify,
|
||||
default_app_settings,
|
||||
@ -15,7 +16,6 @@ import init, {
|
||||
parse_project_settings,
|
||||
default_project_settings,
|
||||
base64_decode,
|
||||
clear_scene_and_bust_cache,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
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 { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||
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 { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||
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 { 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]
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return [s[0], s[1], s[2] === 0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default SourceRange for testing or as a placeholder.
|
||||
*/
|
||||
export function defaultSourceRange(): SourceRange {
|
||||
return [0, 0, true]
|
||||
}
|
||||
@ -137,7 +121,7 @@ const initialise = async () => {
|
||||
const fullUrl = wasmUrl()
|
||||
const input = await fetch(fullUrl)
|
||||
const buffer = await input.arrayBuffer()
|
||||
return await init({ module_or_path: buffer })
|
||||
return await init(buffer)
|
||||
} catch (e) {
|
||||
console.log('Error initialising WASM', 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 {
|
||||
program: Node<Program>
|
||||
|
||||
@ -512,19 +492,18 @@ export const _executor = async (
|
||||
return Promise.reject(programMemoryOverride)
|
||||
|
||||
try {
|
||||
let jsAppSettings = default_app_settings()
|
||||
let baseUnit = 'mm'
|
||||
if (!TEST) {
|
||||
const lastSettingsSnapshot = await import(
|
||||
'components/SettingsAuthProvider'
|
||||
).then((module) => module.lastSettingsContextSnapshot)
|
||||
if (lastSettingsSnapshot) {
|
||||
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
|
||||
}
|
||||
const getSettingsState = import('components/SettingsAuthProvider').then(
|
||||
(module) => module.getSettingsState
|
||||
)
|
||||
baseUnit =
|
||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
||||
}
|
||||
const execState: RawExecState = await execute(
|
||||
const execState: RawExecState = await execute_wasm(
|
||||
JSON.stringify(node),
|
||||
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
||||
JSON.stringify({ settings: jsAppSettings }),
|
||||
baseUnit,
|
||||
engineCommandManager,
|
||||
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 (
|
||||
engineCommandManager: EngineCommandManager,
|
||||
ast: Node<Program>,
|
||||
@ -705,21 +698,6 @@ export function defaultAppSettings(): DeepPartial<Configuration> | Error {
|
||||
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(
|
||||
toml: string
|
||||
): DeepPartial<Configuration> | Error {
|
||||
|
12
src/lib/appVersion.ts
Normal 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'
|
@ -10,7 +10,7 @@ const noModifiersPressed = (e: MouseEvent) =>
|
||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||
|
||||
export type CameraSystem =
|
||||
| 'Zoo'
|
||||
| 'KittyCAD'
|
||||
| 'OnShape'
|
||||
| 'Trackpad Friendly'
|
||||
| 'Solidworks'
|
||||
@ -19,7 +19,7 @@ export type CameraSystem =
|
||||
| 'AutoCAD'
|
||||
|
||||
export const cameraSystems: CameraSystem[] = [
|
||||
'Zoo',
|
||||
'KittyCAD',
|
||||
'OnShape',
|
||||
'Trackpad Friendly',
|
||||
'Solidworks',
|
||||
@ -32,13 +32,8 @@ export function mouseControlsToCameraSystem(
|
||||
mouseControl: MouseControlType | undefined
|
||||
): CameraSystem | undefined {
|
||||
switch (mouseControl) {
|
||||
// TODO: understand why the values come back without underscores and fix the root cause
|
||||
// @ts-ignore: TS2678
|
||||
case 'zoo':
|
||||
return 'Zoo'
|
||||
// TODO: understand why the values come back without underscores and fix the root cause
|
||||
// @ts-ignore: TS2678
|
||||
case 'onshape':
|
||||
case 'kitty_cad':
|
||||
return 'KittyCAD'
|
||||
case 'on_shape':
|
||||
return 'OnShape'
|
||||
case 'trackpad_friendly':
|
||||
@ -49,9 +44,6 @@ export function mouseControlsToCameraSystem(
|
||||
return 'NX'
|
||||
case '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':
|
||||
return 'AutoCAD'
|
||||
default:
|
||||
@ -85,7 +77,7 @@ export const btnName = (e: MouseEvent) => ({
|
||||
})
|
||||
|
||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
Zoo: {
|
||||
KittyCAD: {
|
||||
pan: {
|
||||
description: 'Shift + Right click drag or middle click drag',
|
||||
callback: (e) =>
|
||||
|