Compare commits
66 Commits
derive-doc
...
migrate-to
Author | SHA1 | Date | |
---|---|---|---|
6717543305 | |||
db5abf0149 | |||
3634c96cf1 | |||
e9890aa22b | |||
36532c521e | |||
c75ecada03 | |||
21d64d7c29 | |||
2224c89909 | |||
d822593f35 | |||
9b0f9f321b | |||
f29573f3dc | |||
5a236577bb | |||
96346cfedf | |||
866dffbb46 | |||
e86c628118 | |||
28cd330fc2 | |||
9a9c2223de | |||
6d12aa48f8 | |||
3fdf7bd45e | |||
fdadd059d6 | |||
b646504cfb | |||
ff8a994cb8 | |||
4f9a0be343 | |||
e8240ee896 | |||
70bc0accad | |||
9dedc56b7e | |||
72144052c0 | |||
82bad2cab1 | |||
63be31971f | |||
ba6b3d9a8d | |||
0b5802a0d2 | |||
e2d24edfee | |||
cc06825ec9 | |||
51f7addf54 | |||
226e4d2932 | |||
e7397ec564 | |||
af69856633 | |||
bce058ab52 | |||
75545ddff1 | |||
203fa7e454 | |||
76de64780c | |||
2d804dee2b | |||
c094d0ced1 | |||
a90fe3c066 | |||
f3ea7fd0e2 | |||
704ff0df62 | |||
eba17e92b7 | |||
d9d700624e | |||
1e547aeef0 | |||
22899c9e69 | |||
25702f454c | |||
11faf86983 | |||
67d73382b1 | |||
15b9f43f2c | |||
d28555a070 | |||
7bf116629f | |||
fe45b5b54d | |||
bcbd3f5bfd | |||
959433e357 | |||
d18e35b7ea | |||
596c9a0ee6 | |||
9106a81c77 | |||
8b5ebe67b2 | |||
a7f539eca6 | |||
f4c87c994c | |||
3d4ae05145 |
12
.github/dependabot.yml
vendored
@ -9,15 +9,27 @@ updates:
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
reviewers:
|
||||
- franknoirot
|
||||
- irev-dev
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src-tauri/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
|
15
.github/workflows/ci.yml
vendored
@ -104,7 +104,11 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
|
||||
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||
echo "$(jq --arg id 'dev.zoo.modeling-app-nightly' \
|
||||
'.identifier=$id' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \
|
||||
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
@ -281,6 +285,7 @@ jobs:
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
|
||||
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
@ -295,9 +300,9 @@ jobs:
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
|
||||
--arg windows_sig "$WINDOWS_SIG" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
@ -326,8 +331,8 @@ jobs:
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
|
5
.github/workflows/generate-website-docs.yml
vendored
@ -15,7 +15,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
generate-website-docs:
|
||||
name: generate-website-docs
|
||||
name: generate-website-docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -56,6 +56,9 @@ jobs:
|
||||
gh pr create --title "Update KCL docs" \
|
||||
--body "Updating the generated kcl docs cc @jessfraz @franknoirot merge this" \
|
||||
--head "$NEW_BRANCH" \
|
||||
--reviewer jessfraz \
|
||||
--reviewer irev-dev \
|
||||
--reviewer franknoirot \
|
||||
--base main || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
@ -17,11 +17,11 @@ lineTo(to: [number], sketch_group: SketchGroup, tag?: String) -> SketchGroup
|
||||
```js
|
||||
fn rectShape = (pos, w, l) => {
|
||||
const rr = startSketchOn('YZ')
|
||||
|> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
|
||||
|> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, "edge1")
|
||||
|> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, "edge2")
|
||||
|> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, "edge3")
|
||||
|> close(%, "edge4")
|
||||
|> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
|
||||
|> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, "edge1")
|
||||
|> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, "edge2")
|
||||
|> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, "edge3")
|
||||
|> close(%, "edge4")
|
||||
return rr
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ const part = startSketchOn('XY')
|
||||
{
|
||||
// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
||||
arcDegrees: number,
|
||||
// The center about which to make th pattern. This is a 2D vector.
|
||||
// The center about which to make the pattern. This is a 2D vector.
|
||||
center: [number, number],
|
||||
// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.
|
||||
repetitions: number,
|
||||
|
@ -42,7 +42,7 @@ const part = startSketchOn('XY')
|
||||
arcDegrees: number,
|
||||
// The axis around which to make the pattern. This is a 3D vector.
|
||||
axis: [number, number, number],
|
||||
// The center about which to make th pattern. This is a 3D vector.
|
||||
// The center about which to make the pattern. This is a 3D vector.
|
||||
center: [number, number, number],
|
||||
// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.
|
||||
repetitions: number,
|
||||
|
@ -121,9 +121,9 @@ const sketch001 = startSketchOn(box, "END")
|
||||
// Angle to revolve (in degrees). Default is 360.
|
||||
angle: number,
|
||||
// Axis of revolution.
|
||||
axis: "x" |
|
||||
"y" |
|
||||
"z" |
|
||||
axis: "X" |
|
||||
"Y" |
|
||||
"Z" |
|
||||
"-X" |
|
||||
"-Y" |
|
||||
"-Z" |
|
||||
|
@ -27,12 +27,12 @@ startSketchOn('XY')
|
||||
```js
|
||||
fn cube = (pos, scale) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|> close(%)
|
||||
|> extrude(scale, %)
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|> close(%)
|
||||
|> extrude(scale, %)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
@ -26564,7 +26564,7 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false,
|
||||
"examples": [
|
||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 10], %)\n |> line([10, 0], %)\n |> line([0, -10], %, 'revolveAxis')\n |> close(%)\n |> extrude(10, %)\n\nconst sketch001 = startSketchOn(box, \"revolveAxis\")\n |> startProfileAt([5, 10], %)\n |> line([0, -10], %)\n |> line([2, 0], %)\n |> line([0, 10], %)\n |> close(%)\n |> revolve({\n axis: getEdge('revolveAxis', box),\n angle: 90\n }, %)"
|
||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 10], %)\n |> line([10, 0], %)\n |> line([0, -10], %, 'revolveAxis')\n |> close(%)\n |> extrude(10, %)\n\nconst sketch001 = startSketchOn('XY')\n |> startProfileAt([0, -10], %)\n |> line([0, -10], %)\n |> line([2, 0], %)\n |> line([0, 10], %)\n |> close(%)\n |> revolve({\n axis: getEdge('revolveAxis', box),\n angle: 90\n }, %)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -41924,7 +41924,7 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false,
|
||||
"examples": [
|
||||
"fn rectShape = (pos, w, l) => {\n const rr = startSketchOn('YZ')\n |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)\n |> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, \"edge1\")\n |> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, \"edge2\")\n |> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, \"edge3\")\n |> close(%, \"edge4\")\n return rr\n}\n\n// Create the mounting plate extrusion, holes, and fillets\nconst part = rectShape([0, 0], 20, 20)"
|
||||
"fn rectShape = (pos, w, l) => {\n const rr = startSketchOn('YZ')\n |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)\n |> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, \"edge1\")\n |> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, \"edge2\")\n |> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, \"edge3\")\n |> close(%, \"edge4\")\n return rr\n}\n\n// Create the mounting plate extrusion, holes, and fillets\nconst part = rectShape([0, 0], 20, 20)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -42165,7 +42165,7 @@
|
||||
"format": "double"
|
||||
},
|
||||
"center": {
|
||||
"description": "The center about which to make th pattern. This is a 2D vector.",
|
||||
"description": "The center about which to make the pattern. This is a 2D vector.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
@ -44168,7 +44168,7 @@
|
||||
"minItems": 3
|
||||
},
|
||||
"center": {
|
||||
"description": "The center about which to make th pattern. This is a 3D vector.",
|
||||
"description": "The center about which to make the pattern. This is a 3D vector.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
@ -49363,21 +49363,21 @@
|
||||
"description": "X-axis.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"x"
|
||||
"X"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Y-axis.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"y"
|
||||
"Y"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Z-axis.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"z"
|
||||
"Z"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -58824,7 +58824,7 @@
|
||||
"deprecated": false,
|
||||
"examples": [
|
||||
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")",
|
||||
"fn cube = (pos, scale) => {\n const sg = startSketchOn('XY')\n |> startProfileAt(pos, %)\n |> line([0, scale], %)\n |> line([scale, 0], %)\n |> line([0, -scale], %)\n |> close(%)\n |> extrude(scale, %)\n\n return sg\n}\n\nconst box = cube([0, 0], 20)\n\nconst part001 = startSketchOn(box, \"start\")\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%)\n |> extrude(20, %)"
|
||||
"fn cube = (pos, scale) => {\n const sg = startSketchOn('XY')\n |> startProfileAt(pos, %)\n |> line([0, scale], %)\n |> line([scale, 0], %)\n |> line([0, -scale], %)\n |> close(%)\n |> extrude(scale, %)\n\n return sg\n}\n\nconst box = cube([0, 0], 20)\n\nconst part001 = startSketchOn(box, \"start\")\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%)\n |> extrude(20, %)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -2,10 +2,15 @@ import { test, expect } from '@playwright/test'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { basicStorageState } from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { secrets } from './secrets'
|
||||
import {
|
||||
TEST_SETTINGS,
|
||||
TEST_SETTINGS_KEY,
|
||||
TEST_SETTINGS_CORRUPTED,
|
||||
TEST_SETTINGS_ONBOARDING,
|
||||
} from './storageStates'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -32,13 +37,25 @@ test.beforeEach(async ({ context, page }) => {
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
await context.addInitScript(
|
||||
async ({ token, settingsKey, settings }) => {
|
||||
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
token: secrets.token,
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS }),
|
||||
}
|
||||
)
|
||||
// kill animations, speeds up tests and reduced flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
})
|
||||
|
||||
test.setTimeout(60000)
|
||||
|
||||
test('Basic sketch', async ({ page, context }) => {
|
||||
test('Basic sketch', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -128,6 +145,7 @@ test('Can moving camera', async ({ page, context }) => {
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.closeKclCodePanel()
|
||||
|
||||
const camPos: [number, number, number] = [0, 85, 85]
|
||||
const bakeInRetries = async (
|
||||
@ -161,6 +179,8 @@ test('Can moving camera', async ({ page, context }) => {
|
||||
}, 300)
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByTestId('cam-x-position').isVisible()
|
||||
|
||||
const vals = await Promise.all([
|
||||
page.getByTestId('cam-x-position').inputValue(),
|
||||
page.getByTestId('cam-y-position').inputValue(),
|
||||
@ -308,9 +328,9 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('executes on load', async ({ page, context }) => {
|
||||
test('executes on load', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
@ -325,7 +345,11 @@ test('executes on load', async ({ page, context }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// expand variables section
|
||||
await page.getByText('Variables').click()
|
||||
const variablesTabButton = page.getByRole('tab', {
|
||||
name: 'Variables',
|
||||
exact: false,
|
||||
})
|
||||
await variablesTabButton.click()
|
||||
|
||||
// can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
|
||||
// part001 only shows up in the variables summary if it's been executed
|
||||
@ -340,16 +364,20 @@ test('executes on load', async ({ page, context }) => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('re-executes', async ({ page, context }) => {
|
||||
test('re-executes', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async (token) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem('persistCode', `const myVar = 5`)
|
||||
})
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await page.getByText('Variables').click()
|
||||
const variablesTabButton = page.getByRole('tab', {
|
||||
name: 'Variables',
|
||||
exact: false,
|
||||
})
|
||||
await variablesTabButton.click()
|
||||
// expect to see "myVar:5"
|
||||
await expect(
|
||||
page.locator('.pretty-json-container >> text=myVar:5')
|
||||
@ -482,7 +510,8 @@ test('Auto complete works', async ({ page }) => {
|
||||
// expect there to be three auto complete options
|
||||
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
||||
await page.getByText('startSketchOn').click()
|
||||
await page.keyboard.type("('XY')")
|
||||
await page.keyboard.type("'XY'")
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(' |> startProfi')
|
||||
// expect there be a single auto complete option that we can just hit enter on
|
||||
@ -490,7 +519,10 @@ test('Auto complete works', async ({ page }) => {
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
||||
|
||||
await page.keyboard.type('([0,0], %)')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(' |> lin')
|
||||
|
||||
@ -501,145 +533,149 @@ test('Auto complete works', async ({ page }) => {
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('Enter')
|
||||
// finish line with comment
|
||||
await page.keyboard.type('(5, %) // lin')
|
||||
await page.keyboard.type('5')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.type(' // lin')
|
||||
await page.waitForTimeout(100)
|
||||
// there shouldn't be any auto complete options for 'lin' in the comment
|
||||
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> startProfileAt([3.14, 3.14], %)
|
||||
|> xLine(5, %) // lin`)
|
||||
})
|
||||
|
||||
// Stored settings validation test
|
||||
test.describe('Settings persistence and validation tests', () => {
|
||||
// Override test setup
|
||||
test('Stored settings are validated and fall back to defaults', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
|
||||
// Override beforeEach test setup
|
||||
// with corrupted settings
|
||||
const storageState = structuredClone(basicStorageState)
|
||||
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
|
||||
settings: SaveSettingsPayload
|
||||
}
|
||||
s.settings.app.theme = Themes.Dark
|
||||
s.settings.app.projectDirectory = 123 as any
|
||||
s.settings.modeling.defaultUnit = 'invalid' as any
|
||||
s.settings.modeling.mouseControls = `() => alert('hack the planet')` as any
|
||||
s.settings.projects.defaultProjectName = false as any
|
||||
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
|
||||
await context.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_CORRUPTED }),
|
||||
}
|
||||
)
|
||||
|
||||
test.use({ storageState })
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
test('Stored settings are validated and fall back to defaults', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
// Check the settings were reset
|
||||
const storedSettings = TOML.parse(
|
||||
await page.evaluate(
|
||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
||||
{ settingsKey: TEST_SETTINGS_KEY }
|
||||
)
|
||||
) as { settings: SaveSettingsPayload }
|
||||
|
||||
// Check the settings were reset
|
||||
const storedSettings = TOML.parse(
|
||||
await page.evaluate(() => localStorage.getItem('/user.toml') || '{}')
|
||||
) as { settings: SaveSettingsPayload }
|
||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
||||
|
||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
||||
|
||||
// Check that the invalid settings were removed
|
||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
||||
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
||||
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
||||
})
|
||||
|
||||
test('Project settings can be set and override user settings', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// Open the settings modal with the browser keyboard shortcut
|
||||
await page.keyboard.press('Meta+Shift+,')
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await page
|
||||
.locator('select[name="app-theme"]')
|
||||
.selectOption({ value: 'light' })
|
||||
|
||||
// Verify the toast appeared
|
||||
await expect(
|
||||
page.getByText(`Set theme to "light" for this project`)
|
||||
).toBeVisible()
|
||||
// Check that the theme changed
|
||||
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||
|
||||
// Check that the user setting was not changed
|
||||
await page.getByRole('radio', { name: 'User' }).click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
|
||||
|
||||
// Roll back to default "system" theme
|
||||
await page
|
||||
.getByText(
|
||||
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
|
||||
)
|
||||
.hover()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Roll back theme ; Has tooltip: Roll back to match default',
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
|
||||
|
||||
// Check that the project setting did not change
|
||||
await page.getByRole('radio', { name: 'Project' }).click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
|
||||
})
|
||||
// Check that the invalid settings were removed
|
||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
||||
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
||||
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
||||
})
|
||||
|
||||
// Onboarding tests
|
||||
test.describe('Onboarding tests', () => {
|
||||
// Override test setup
|
||||
const storageState = structuredClone(basicStorageState)
|
||||
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
|
||||
settings: SaveSettingsPayload
|
||||
}
|
||||
s.settings.app.onboardingStatus = '/export'
|
||||
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
|
||||
test.use({ storageState })
|
||||
test('Project settings can be set and override user settings', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
|
||||
test('Onboarding redirects and code updating', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
// Open the settings modal with the browser keyboard shortcut
|
||||
await page.keyboard.press('Meta+Shift+,')
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await page
|
||||
.locator('select[name="app-theme"]')
|
||||
.selectOption({ value: 'light' })
|
||||
|
||||
// Test that the redirect happened
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
// Verify the toast appeared
|
||||
await expect(
|
||||
page.getByText(`Set theme to "light" for this project`)
|
||||
).toBeVisible()
|
||||
// Check that the theme changed
|
||||
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||
|
||||
// Check that the user setting was not changed
|
||||
await page.getByRole('radio', { name: 'User' }).click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
|
||||
|
||||
// Roll back to default "system" theme
|
||||
await page
|
||||
.getByText(
|
||||
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
|
||||
)
|
||||
.hover()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Roll back theme ; Has tooltip: Roll back to match default',
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
|
||||
|
||||
// Test that you come back to this page when you refresh
|
||||
await page.reload()
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
// Check that the project setting did not change
|
||||
await page.getByRole('radio', { name: 'Project' }).click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
|
||||
})
|
||||
|
||||
// Test that the onboarding pane loaded
|
||||
const title = page.locator('[data-testid="onboarding-content"]')
|
||||
await expect(title).toBeAttached()
|
||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
|
||||
// Test that the code changes when you advance to the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText('')
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
// Give some initial code, so we can test that it's cleared
|
||||
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING }),
|
||||
}
|
||||
)
|
||||
|
||||
// Test that the code is not empty when you click on the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// Test that the redirect happened
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
|
||||
// Test that you come back to this page when you refresh
|
||||
await page.reload()
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
|
||||
// Test that the onboarding pane loaded
|
||||
const title = page.locator('[data-testid="onboarding-content"]')
|
||||
await expect(title).toBeAttached()
|
||||
|
||||
// Test that the code changes when you advance to the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText('')
|
||||
|
||||
// Test that the code is not empty when you click on the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
||||
})
|
||||
|
||||
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
@ -851,19 +887,21 @@ test.describe('Command bar tests', () => {
|
||||
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||
})
|
||||
|
||||
// Override test setup code
|
||||
const storageState = structuredClone(basicStorageState)
|
||||
storageState.origins[0].localStorage[1].value = `const distance = sqrt(20)
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
`
|
||||
test.use({ storageState })
|
||||
test('Can extrude from the command bar', async ({ page }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const distance = sqrt(20)
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
@ -872,15 +910,13 @@ test.describe('Command bar tests', () => {
|
||||
// Make sure the stream is up
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Extrude' })
|
||||
).not.toBeDisabled()
|
||||
await u.clearCommandLogs()
|
||||
await page.getByText('|> line([0.73, -14.93], %)').click()
|
||||
await page.getByRole('button', { name: 'Extrude' }).isEnabled()
|
||||
|
||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await page.keyboard.press('Meta+K')
|
||||
@ -898,23 +934,25 @@ test.describe('Command bar tests', () => {
|
||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||
'distance001'
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
|
||||
const continueButton = page.getByRole('button', { name: 'Continue' })
|
||||
const submitButton = page.getByRole('button', { name: 'Submit command' })
|
||||
await continueButton.click()
|
||||
|
||||
// Review step and argument hotkeys
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Submit command' })
|
||||
).toBeEnabled()
|
||||
await expect(submitButton).toBeEnabled()
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// Assert we're back on the distance step
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Distance 12', exact: false })
|
||||
).toBeDisabled()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||
await continueButton.click()
|
||||
await submitButton.click()
|
||||
|
||||
// Check that the code was updated
|
||||
await page.keyboard.press('Enter')
|
||||
await u.waitForCmdReceive('extrude')
|
||||
// Unfortunately this indentation seems to matter for the test
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const distance = sqrt(20)
|
||||
@ -1055,9 +1093,9 @@ const part002 = startSketchOn('XY')
|
||||
)
|
||||
})
|
||||
|
||||
test('ProgramMemory can be serialised', async ({ page, context }) => {
|
||||
test('ProgramMemory can be serialised', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part = startSketchOn('XY')
|
||||
@ -1067,7 +1105,7 @@ test('ProgramMemory can be serialised', async ({ page, context }) => {
|
||||
|> line([0, -1], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|> patternLinear({
|
||||
|> patternLinear3d({
|
||||
axis: [1, 0, 1],
|
||||
repetitions: 3,
|
||||
distance: 6
|
||||
@ -1096,7 +1134,6 @@ test('ProgramMemory can be serialised', async ({ page, context }) => {
|
||||
|
||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
const selectionsSnippets = {
|
||||
@ -1105,7 +1142,7 @@ test("Various pipe expressions should and shouldn't allow edit and or extrude",
|
||||
extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)',
|
||||
editOnly: '|> startProfileAt([15.79, -14.6], %)',
|
||||
}
|
||||
await context.addInitScript(
|
||||
await page.addInitScript(
|
||||
async ({
|
||||
extrudeAndEditBlocked,
|
||||
extrudeAndEditBlockedInFunction,
|
||||
@ -1138,11 +1175,11 @@ const part003 = startSketchOn('-XZ')
|
||||
|
||||
fn yohey = (pos) => {
|
||||
const part004 = startSketchOn('-XZ')
|
||||
${extrudeAndEditBlockedInFunction}
|
||||
|> line([27.55, -1.65], %)
|
||||
|> line([4.95, -10.53], %)
|
||||
|> line([-20.38, -8], %)
|
||||
|> line([-15.79, 17.08], %)
|
||||
${extrudeAndEditBlockedInFunction}
|
||||
|> line([27.55, -1.65], %)
|
||||
|> line([4.95, -10.53], %)
|
||||
|> line([-20.38, -8], %)
|
||||
|> line([-15.79, 17.08], %)
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -1152,7 +1189,7 @@ fn yohey = (pos) => {
|
||||
},
|
||||
selectionsSnippets
|
||||
)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
@ -1265,12 +1302,9 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
})
|
||||
|
||||
test('Can edit segments by dragging their handles', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
@ -1422,9 +1456,9 @@ test('Snap to close works (at any scale)', async ({ page }) => {
|
||||
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
|
||||
})
|
||||
|
||||
test('Sketch on face', async ({ page, context }) => {
|
||||
test('Sketch on face', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
|
@ -7,16 +7,26 @@ import { spawn } from 'child_process'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import JSZip from 'jszip'
|
||||
import path from 'path'
|
||||
import { basicSettings, basicStorageState } from './storageStates'
|
||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// reducedMotion kills animations, which speeds up tests and reduces flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
})
|
||||
|
||||
test.use({
|
||||
storageState: structuredClone(basicStorageState),
|
||||
// set the default settings
|
||||
await page.addInitScript(
|
||||
async ({ token, settingsKey, settings }) => {
|
||||
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
token: secrets.token,
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS }),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.setTimeout(60_000)
|
||||
@ -25,7 +35,7 @@ test('exports of each format should work', async ({ page, context }) => {
|
||||
// FYI this test doesn't work with only engine running locally
|
||||
// And you will need to have the KittyCAD CLI installed
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
await page.addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
@ -69,6 +79,7 @@ const part001 = startSketchOn('-XZ')
|
||||
|> extrude(4, %)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -364,17 +375,19 @@ test('extrude on each default plane should be stable', async ({
|
||||
await u.removeCurrentCode()
|
||||
// add makeCode('XZ')
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.locator('.cm-content').fill(makeCode(plane))
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.locator('.cm-content').fill(makeCode(plane)),
|
||||
200
|
||||
)
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.getByText('Code').click()
|
||||
await page.waitForTimeout(150)
|
||||
await u.closeKclCodePanel()
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
await page.getByText('Code').click()
|
||||
await u.openKclCodePanel()
|
||||
}
|
||||
await runSnapshotsForOtherPlanes('XY')
|
||||
await runSnapshotsForOtherPlanes('-XY')
|
||||
@ -445,105 +458,108 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Client side scene scale should match engine scale - Inch', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
test.describe('Client side scene scale should match engine scale', () => {
|
||||
test('Inch scale', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)
|
||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)
|
||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||
|
||||
// click tangential arc tool again to unequip it
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
// click tangential arc tool again to unequip it
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Client side scene scale should match engine scale - Millimeters', () => {
|
||||
const storageState = structuredClone(basicStorageState)
|
||||
storageState.origins[0].localStorage[2].value = TOML.stringify({
|
||||
settings: {
|
||||
...basicSettings,
|
||||
modeling: {
|
||||
...basicSettings.modeling,
|
||||
defaultUnit: 'mm',
|
||||
test('Millimeter scale', async ({ page }) => {
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
},
|
||||
})
|
||||
test.use({
|
||||
storageState,
|
||||
})
|
||||
|
||||
test('Millimeters', async ({ page }) => {
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({
|
||||
settings: {
|
||||
...TEST_SETTINGS,
|
||||
modeling: {
|
||||
...TEST_SETTINGS.modeling,
|
||||
defaultUnit: 'mm',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -638,7 +654,7 @@ test('Sketch on face with none z-up', async ({ page, context }) => {
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
||||
const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([-2.89, 1.82], %)
|
||||
|> startProfileAt([8, 8], %)
|
||||
|> line([4.68, 3.05], %)
|
||||
|> line([0, -7.79], %, 'seg02')
|
||||
|> close(%)
|
||||
@ -650,6 +666,19 @@ const part002 = startSketchOn(part001, 'seg01')
|
||||
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(2)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Wait for the second extrusion to appear
|
||||
// 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)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
@ -1,9 +1,8 @@
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { secrets } from './secrets'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { Themes } from 'lib/theme'
|
||||
|
||||
export const basicSettings = {
|
||||
export const TEST_SETTINGS_KEY = '/user.toml'
|
||||
export const TEST_SETTINGS = {
|
||||
app: {
|
||||
theme: Themes.Dark,
|
||||
onboardingStatus: 'dismissed',
|
||||
@ -22,19 +21,26 @@ export const basicSettings = {
|
||||
},
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const basicStorageState = {
|
||||
cookies: [],
|
||||
origins: [
|
||||
{
|
||||
origin: 'http://localhost:3000',
|
||||
localStorage: [
|
||||
{ name: 'TOKEN_PERSIST_KEY', value: secrets.token },
|
||||
{ name: 'persistCode', value: '' },
|
||||
{
|
||||
name: '/user.toml',
|
||||
value: TOML.stringify({ settings: basicSettings }),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
export const TEST_SETTINGS_ONBOARDING = {
|
||||
...TEST_SETTINGS,
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_CORRUPTED = {
|
||||
app: {
|
||||
theme: Themes.Dark,
|
||||
onboardingStatus: 'dismissed',
|
||||
projectDirectory: 123 as any,
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: 'invalid' as any,
|
||||
mouseControls: `() => alert('hack the planet')` as any,
|
||||
showDebugPanel: true,
|
||||
},
|
||||
projects: {
|
||||
defaultProjectName: false as any,
|
||||
},
|
||||
textEditor: {
|
||||
textWrapping: true,
|
||||
},
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
@ -44,26 +44,44 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||
)
|
||||
}
|
||||
|
||||
async function openDebugPanel(page: Page) {
|
||||
const isOpen =
|
||||
(await page
|
||||
.locator('[data-testid="debug-panel"]')
|
||||
?.getAttribute('open')) === ''
|
||||
async function openKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await page.getByText('Debug').click()
|
||||
await page.getByTestId('debug-panel').and(page.locator('[open]')).waitFor()
|
||||
await paneLocator.click()
|
||||
await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
async function closeKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||
if (isOpen) {
|
||||
await paneLocator.click()
|
||||
await paneLocator
|
||||
.and(page.locator(':not([aria-selected="true"])'))
|
||||
.waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
async function openDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await debugLocator.click()
|
||||
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
async function closeDebugPanel(page: Page) {
|
||||
const isOpen =
|
||||
(await page.getByTestId('debug-panel')?.getAttribute('open')) === ''
|
||||
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||
if (isOpen) {
|
||||
await page.getByText('Debug').click()
|
||||
await page
|
||||
.getByTestId('debug-panel')
|
||||
.and(page.locator(':not([open])'))
|
||||
await debugLocator.click()
|
||||
await debugLocator
|
||||
.and(page.locator(':not([aria-selected="true"])'))
|
||||
.waitFor()
|
||||
}
|
||||
}
|
||||
@ -81,20 +99,19 @@ export function getUtils(page: Page) {
|
||||
removeCurrentCode: () => removeCurrentCode(page),
|
||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||
updateCamPosition: async (xyz: [number, number, number]) => {
|
||||
const fillInput = async () => {
|
||||
await page.fill('[data-testid="cam-x-position"]', String(xyz[0]))
|
||||
await page.fill('[data-testid="cam-y-position"]', String(xyz[1]))
|
||||
await page.fill('[data-testid="cam-z-position"]', String(xyz[2]))
|
||||
const fillInput = async (axis: 'x' | 'y' | 'z', value: number) => {
|
||||
await page.fill(`[data-testid="cam-${axis}-position"]`, String(value))
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
await fillInput()
|
||||
await page.waitForTimeout(100)
|
||||
await fillInput()
|
||||
await page.waitForTimeout(100)
|
||||
await fillInput()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await fillInput('x', xyz[0])
|
||||
await fillInput('y', xyz[1])
|
||||
await fillInput('z', xyz[2])
|
||||
},
|
||||
clearCommandLogs: () => clearCommandLogs(page),
|
||||
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
||||
openKclCodePanel: () => openKclCodePanel(page),
|
||||
closeKclCodePanel: () => closeKclCodePanel(page),
|
||||
openDebugPanel: () => openDebugPanel(page),
|
||||
closeDebugPanel: () => closeDebugPanel(page),
|
||||
openAndClearDebugPanel: async () => {
|
||||
|
45
package.json
@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.15.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
@ -15,29 +15,30 @@
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
"@tauri-apps/api": "^2.0.0-beta.7",
|
||||
"@replit/codemirror-interact": "^6.3.1",
|
||||
"@tauri-apps/api": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@tweenjs/tween.js": "^23.1.1",
|
||||
"@types/node": "^18.19.26",
|
||||
"@types/react": "^18.2.73",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/node": "^18.19.31",
|
||||
"@types/react": "^18.2.77",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"decamelize": "^6.0.0",
|
||||
"formik": "^2.4.3",
|
||||
"formik": "^2.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.4.3",
|
||||
"http-server": "^14.1.1",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
"jszip": "^3.10.1",
|
||||
@ -52,18 +53,19 @@
|
||||
"react-modal-promise": "^1.0.2",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"swr": "^2.2.2",
|
||||
"three": "^0.160.0",
|
||||
"swr": "^2.2.5",
|
||||
"three": "^0.163.0",
|
||||
"toml": "^3.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^9.0.1",
|
||||
"vitest": "^1.4.0",
|
||||
"vitest": "^1.5.0",
|
||||
"vscode-jsonrpc": "^8.1.0",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"wasm-pack": "^0.12.1",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.13.0",
|
||||
"ws": "^8.16.0",
|
||||
"xstate": "^4.38.2",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
@ -115,23 +117,24 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.24.3",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@tauri-apps/cli": "^2.0.0-beta.12",
|
||||
"@playwright/test": "^1.43.0",
|
||||
"@tauri-apps/cli": "^2.0.0-beta.13",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/debounce-promise": "^3.1.9",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.160.0",
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@wdio/cli": "^8.24.3",
|
||||
"@wdio/globals": "^8.24.3",
|
||||
"@wdio/globals": "^8.36.0",
|
||||
"@wdio/local-runner": "^8.35.1",
|
||||
"@wdio/mocha-framework": "^8.35.0",
|
||||
"@wdio/mocha-framework": "^8.36.0",
|
||||
"@wdio/spec-reporter": "^8.32.4",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@ -153,6 +156,6 @@
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"yarn": "^1.22.19"
|
||||
"yarn": "^1.22.22"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import { basicStorageState } from './e2e/playwright/storageStates'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
@ -29,9 +28,6 @@ export default defineConfig({
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
|
||||
/* Use a common shared localStorage */
|
||||
storageState: basicStorageState,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
39
src-tauri/Cargo.lock
generated
@ -69,9 +69,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.81"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
@ -2231,9 +2231,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.63"
|
||||
version = "0.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93a332250e08fd715ad3d5826e04d36da1c5bb42d0c1b1ff1f0598278b9ebf3c"
|
||||
checksum = "fc460442c165c8e707b1154551cefd08938d10bb80c78940e10cd9869487c325"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -2333,7 +2333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.0",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4467,14 +4467,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.0-beta.14"
|
||||
version = "2.0.0-beta.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50fb0bdb687486415224f8be47c78993e9f3ea575ee0d5177c90d0c71842f4a"
|
||||
checksum = "cd0aba659957a3f1f1666acbf17723e8d41dcc177539bf1adbe55305f5d7118a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cocoa",
|
||||
"dirs-next",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
"futures-util",
|
||||
"getrandom 0.2.9",
|
||||
@ -4515,9 +4516,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.0-beta.11"
|
||||
version = "2.0.0-beta.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82a46303cc4bce0b17ad95965cbd8326e3511b9d2cb6fb13a4a4c98a11b0dcaf"
|
||||
checksum = "33de24aabe2b9c340d67005800cb6dd40aac5283126a42896fc8eec0b87cbe45"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@ -4537,9 +4538,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.0-beta.11"
|
||||
version = "2.0.0-beta.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1665f6a986842061a67cb9dcbe2fa27076c1a616f6525fc06de9d6d52838d63"
|
||||
checksum = "9d1d211268a9590bbf75cc85b47208f59b447626c76396256e12479ac7df6c8b"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"brotli",
|
||||
@ -4564,9 +4565,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.0-beta.11"
|
||||
version = "2.0.0-beta.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c1558fc42cc2a1735cfd5edb2954c735d4516f8ba31c58b7180ba8a2bc18de"
|
||||
checksum = "b096f63f2724a1280ae0f5a34d0731de18ca18305e2ef6e5e9a39bb2710e8a85"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@ -4720,9 +4721,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.0.0-beta.11"
|
||||
version = "2.0.0-beta.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612e05de1382575b32b5220b546861256f630f37ac64c29cab252592861b9bd4"
|
||||
checksum = "96c957749c40db7999959f379f799db095f2248a80bdbb13d8c078f6c299240e"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
@ -4739,9 +4740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.0.0-beta.11"
|
||||
version = "2.0.0-beta.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f73672897b5396cb05c2f21b12b66ecfd4b51fae619dd35387467660d6c00fb"
|
||||
checksum = "6b937adb1cf3fa0457928ace959ca3fc1a85ddd69f56b124682d40f3e5683e60"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@ -4763,9 +4764,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.0.0-beta.11"
|
||||
version = "2.0.0-beta.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a148adf8077e1891c8b7d1c2be90c1c8eb8c7a071c35bb8edbdfe7cd9d8e23c"
|
||||
checksum = "760ac613d7f0de95067bcbcbcea175fe1df88fc4ab59c7f0b2cc2d01dc16a199"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
|
@ -12,15 +12,15 @@ rust-version = "1.70"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||
tauri-build = { version = "2.0.0-beta.12", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.63"
|
||||
kittycad = "0.2.67"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2.0.0-beta", features = [ "devtools", "unstable"] }
|
||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta.0" }
|
||||
tauri-plugin-fs = { version = "2.0.0-beta.0" }
|
||||
tauri-plugin-http = { version = "2.0.0-beta.0" }
|
||||
|
133
src/App.tsx
@ -1,22 +1,12 @@
|
||||
import { useCallback, MouseEventHandler, useEffect } from 'react'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { MouseEventHandler, useEffect, useRef } from 'react'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { PaneType, useStore } from './useStore'
|
||||
import { Logs, KCLErrors } from './components/Logs'
|
||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||
import { MemoryPanel } from './components/MemoryPanel'
|
||||
import { useStore } from './useStore'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
import { EngineCommand } from './lang/std/engineConnection'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { Resizable } from 're-resizable'
|
||||
import {
|
||||
faCode,
|
||||
faCodeCommit,
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||
@ -24,9 +14,6 @@ import { type IndexLoaderData } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { CodeMenu } from 'components/CodeMenu'
|
||||
import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
@ -34,6 +21,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
|
||||
|
||||
export function App() {
|
||||
useRefreshSettings(paths.FILE + 'SETTINGS')
|
||||
@ -41,6 +29,9 @@ export function App() {
|
||||
const navigate = useNavigate()
|
||||
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 projectName = project?.name || null
|
||||
const projectPath = project?.path || null
|
||||
@ -49,43 +40,24 @@ export function App() {
|
||||
}, [projectName, projectPath])
|
||||
|
||||
useHotKeyListener()
|
||||
const {
|
||||
buttonDownInStream,
|
||||
openPanes,
|
||||
setOpenPanes,
|
||||
didDragInStream,
|
||||
streamDimensions,
|
||||
} = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
didDragInStream: s.didDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
}))
|
||||
const { buttonDownInStream, didDragInStream, streamDimensions, setHtmlRef } =
|
||||
useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
didDragInStream: s.didDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
setHtmlRef: s.setHtmlRef,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
setHtmlRef(ref)
|
||||
}, [ref])
|
||||
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const {
|
||||
modeling: { showDebugPanel },
|
||||
app: { theme, onboardingStatus },
|
||||
app: { onboardingStatus },
|
||||
} = settings.context
|
||||
const { state, send } = useModelingContext()
|
||||
|
||||
const editorTheme =
|
||||
theme.current === Themes.System ? getSystemTheme() : theme.current
|
||||
|
||||
// Pane toggling keyboard shortcuts
|
||||
const togglePane = useCallback(
|
||||
(newPane: PaneType) =>
|
||||
openPanes.includes(newPane)
|
||||
? setOpenPanes(openPanes.filter((p) => p !== newPane))
|
||||
: setOpenPanes([...openPanes, newPane]),
|
||||
[openPanes, setOpenPanes]
|
||||
)
|
||||
useHotkeys('shift + c', () => togglePane('code'))
|
||||
useHotkeys('shift + v', () => togglePane('variables'))
|
||||
useHotkeys('shift + l', () => togglePane('logs'))
|
||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||
useHotkeys('shift + d', () => togglePane('debug'))
|
||||
useHotkeys('esc', () => send('Cancel'))
|
||||
useHotkeys('backspace', (e) => {
|
||||
e.preventDefault()
|
||||
@ -140,6 +112,7 @@ export function App() {
|
||||
<div
|
||||
className="relative h-full flex flex-col"
|
||||
onMouseMove={handleMouseMove}
|
||||
ref={ref}
|
||||
>
|
||||
<AppHeader
|
||||
className={
|
||||
@ -151,74 +124,8 @@ export function App() {
|
||||
enableMenu={true}
|
||||
/>
|
||||
<ModalContainer />
|
||||
<Resizable
|
||||
className={
|
||||
'pointer-events-none h-full flex flex-col flex-1 z-10 my-2 ml-2 pr-1 transition-opacity transition-duration-75 ' +
|
||||
+paneOpacity
|
||||
}
|
||||
defaultSize={{
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={800}
|
||||
minHeight={'auto'}
|
||||
maxHeight={'auto'}
|
||||
handleClasses={{
|
||||
right:
|
||||
'hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
|
||||
(buttonDownInStream || onboardingStatus.current === 'camera'
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto'),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="code-pane"
|
||||
className="h-full flex flex-col justify-between pointer-events-none"
|
||||
>
|
||||
<CollapsiblePanel
|
||||
title="Code"
|
||||
icon={faCode}
|
||||
className="open:!mb-2"
|
||||
open={openPanes.includes('code')}
|
||||
menu={<CodeMenu />}
|
||||
>
|
||||
<TextEditor theme={editorTheme} />
|
||||
</CollapsiblePanel>
|
||||
<section className="flex flex-col">
|
||||
<MemoryPanel
|
||||
theme={editorTheme}
|
||||
open={openPanes.includes('variables')}
|
||||
title="Variables"
|
||||
icon={faSquareRootVariable}
|
||||
/>
|
||||
<Logs
|
||||
theme={editorTheme}
|
||||
open={openPanes.includes('logs')}
|
||||
title="Logs"
|
||||
icon={faCodeCommit}
|
||||
/>
|
||||
<KCLErrors
|
||||
theme={editorTheme}
|
||||
open={openPanes.includes('kclErrors')}
|
||||
title="KCL Errors"
|
||||
iconClassNames={{ bg: 'group-open:bg-destroy-70' }}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</Resizable>
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
{showDebugPanel.current && (
|
||||
<DebugPanel
|
||||
title="Debug"
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
open={openPanes.includes('debug')}
|
||||
/>
|
||||
)}
|
||||
{/* <CamToggle /> */}
|
||||
</div>
|
||||
)
|
||||
|
@ -35,15 +35,17 @@ const router = createBrowserRouter([
|
||||
{
|
||||
loader: settingsLoader,
|
||||
id: paths.INDEX,
|
||||
/* Make sure auth is the outermost provider or else we will have
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<KclContextProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<KclContextProvider>
|
||||
<Outlet />
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</KclContextProvider>
|
||||
</KclContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</CommandBarProvider>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
|
@ -110,14 +110,6 @@ export class CameraControls {
|
||||
}, 400) as any as number
|
||||
}
|
||||
|
||||
// reacts hooks into some of this singleton's properties
|
||||
reactCameraProperties: ReactCameraProperties = {
|
||||
type: 'perspective',
|
||||
fov: 12,
|
||||
position: [0, 0, 0],
|
||||
quaternion: [0, 0, 0, 1],
|
||||
}
|
||||
|
||||
setCam = (camProps: ReactCameraProperties) => {
|
||||
if (
|
||||
camProps.type === 'perspective' &&
|
||||
@ -910,6 +902,26 @@ export class CameraControls {
|
||||
.start()
|
||||
})
|
||||
|
||||
get reactCameraProperties(): ReactCameraProperties {
|
||||
return {
|
||||
type: this.isPerspective ? 'perspective' : 'orthographic',
|
||||
[this.isPerspective ? 'fov' : 'zoom']:
|
||||
this.camera instanceof PerspectiveCamera
|
||||
? this.camera.fov
|
||||
: this.camera.zoom,
|
||||
position: [
|
||||
roundOff(this.camera.position.x, 2),
|
||||
roundOff(this.camera.position.y, 2),
|
||||
roundOff(this.camera.position.z, 2),
|
||||
],
|
||||
quaternion: [
|
||||
roundOff(this.camera.quaternion.x, 2),
|
||||
roundOff(this.camera.quaternion.y, 2),
|
||||
roundOff(this.camera.quaternion.z, 2),
|
||||
roundOff(this.camera.quaternion.w, 2),
|
||||
],
|
||||
}
|
||||
}
|
||||
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
||||
setReactCameraPropertiesCallback = (
|
||||
cb: (a: ReactCameraProperties) => void
|
||||
@ -937,24 +949,7 @@ export class CameraControls {
|
||||
isPerspective: this.isPerspective,
|
||||
target: this.target,
|
||||
})
|
||||
this.deferReactUpdate({
|
||||
type: this.isPerspective ? 'perspective' : 'orthographic',
|
||||
[this.isPerspective ? 'fov' : 'zoom']:
|
||||
this.camera instanceof PerspectiveCamera
|
||||
? this.camera.fov
|
||||
: this.camera.zoom,
|
||||
position: [
|
||||
roundOff(this.camera.position.x, 2),
|
||||
roundOff(this.camera.position.y, 2),
|
||||
roundOff(this.camera.position.z, 2),
|
||||
],
|
||||
quaternion: [
|
||||
roundOff(this.camera.quaternion.x, 2),
|
||||
roundOff(this.camera.quaternion.y, 2),
|
||||
roundOff(this.camera.quaternion.z, 2),
|
||||
roundOff(this.camera.quaternion.w, 2),
|
||||
],
|
||||
})
|
||||
this.deferReactUpdate(this.reactCameraProperties)
|
||||
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||
}
|
||||
getInteractionType = (event: any) =>
|
||||
|
@ -126,12 +126,9 @@ const throttled = throttle((a: ReactCameraProperties) => {
|
||||
}, 1000 / 15)
|
||||
|
||||
export const CamDebugSettings = () => {
|
||||
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
|
||||
type: 'perspective',
|
||||
fov: 12,
|
||||
position: [0, 0, 0],
|
||||
quaternion: [0, 0, 0, 1],
|
||||
})
|
||||
const [camSettings, setCamSettings] = useState<ReactCameraProperties>(
|
||||
sceneInfra.camControls.reactCameraProperties
|
||||
)
|
||||
const [fov, setFov] = useState(12)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -15,18 +15,20 @@
|
||||
https://catmosphere-theme-builder.vercel.app/?colors=%5B%7B%22from%22:%7B%22l%22:1,%22c%22:0.01,%22h%22:78%7D,%22to%22:%7B%22l%22:0.065,%22c%22:0.05,%22h%22:182.6%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.45,%22h%22:122.4%7D,%22to%22:%7B%22l%22:0.13,%22c%22:0.031,%22h%22:137.2%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.13,%22h%22:176%7D,%22to%22:%7B%22l%22:0.116,%22c%22:0.097,%22h%22:213.1%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.169,%22h%22:144.4%7D,%22to%22:%7B%22l%22:0.12,%22c%22:0.45,%22h%22:132.7%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.087,%22h%22:261.6%7D,%22to%22:%7B%22l%22:0.22,%22c%22:0.084,%22h%22:275.5%7D,%22steps%22:12,%22uuid%22:%227tpx9pf1zd6%22%7D,%7B%22from%22:%7B%22l%22:0.954,%22c%22:0.108,%22h%22:280.6%7D,%22to%22:%7B%22l%22:0.166,%22c%22:0.188,%22h%22:263.8%7D,%22steps%22:12,%22uuid%22:%22vu652mebd3%22%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.115,%22h%22:0%7D,%22to%22:%7B%22l%22:0.096,%22c%22:0.261,%22h%22:302%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.185,%22h%22:19.8%7D,%22to%22:%7B%22l%22:0.368,%22c%22:0.45,%22h%22:9.4%7D,%22steps%22:8,%22uuid%22:%22g05inkd34l%22%7D,%7B%22from%22:%7B%22l%22:0.912,%22c%22:0.139,%22h%22:87%7D,%22to%22:%7B%22l%22:0.502,%22c%22:0.45,%22h%22:97.7%7D,%22steps%22:8,%22uuid%22:%22l892hcw4ef%22%7D,%7B%22from%22:%7B%22l%22:0.89,%22c%22:0.16,%22h%22:143.4%7D,%22to%22:%7B%22l%22:0.466,%22c%22:0.208,%22h%22:147.7%7D,%22steps%22:8,%22uuid%22:%22hkd09y9ov4h%22%7D%5D
|
||||
*/
|
||||
/* Chalkboard */
|
||||
--chalkboard-10: oklch(99.9% 0.003766 102.8deg);
|
||||
--chalkboard-20: oklch(91.34% 0.009353 109deg);
|
||||
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
|
||||
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);
|
||||
--chalkboard-50: oklch(66.27% 0.01111 127.6deg);
|
||||
--chalkboard-60: oklch(57.92% 0.0117 133.9deg);
|
||||
--chalkboard-70: oklch(49.56% 0.01229 140.1deg);
|
||||
--chalkboard-80: oklch(41.21% 0.01288 146.3deg);
|
||||
--chalkboard-90: oklch(32.85% 0.01346 152.5deg);
|
||||
--chalkboard-100: oklch(24.49% 0.01405 158.7deg);
|
||||
--chalkboard-110: oklch(16.14% 0.01464 164.9deg);
|
||||
--chalkboard-120: oklch(7.783% 0.01522 171.1deg);
|
||||
|
||||
/* Chalkboard */
|
||||
--chalkboard-10: oklch(99.11% 0 var(--primary-hue));
|
||||
--chalkboard-20: oklch(95.51% 0 var(--primary-hue));
|
||||
--chalkboard-30: oklch(88.48% 0 var(--primary-hue));
|
||||
--chalkboard-40: oklch(82.01% 0 var(--primary-hue));
|
||||
--chalkboard-50: oklch(72.41% 0 var(--primary-hue));
|
||||
--chalkboard-60: oklch(68.19% 0 var(--primary-hue));
|
||||
--chalkboard-70: oklch(48.19% 0 var(--primary-hue));
|
||||
--chalkboard-80: oklch(39.04% 0 var(--primary-hue));
|
||||
--chalkboard-90: oklch(30.12% 0 var(--primary-hue));
|
||||
--chalkboard-100: oklch(22.64% 0 var(--primary-hue));
|
||||
--chalkboard-110: oklch(18.22% 0 var(--primary-hue));
|
||||
--chalkboard-120: oklch(0% 0 var(--primary-hue));
|
||||
|
||||
/* Energy */
|
||||
--energy-10: oklch(93.31% 0.227 122.3deg);
|
||||
@ -144,18 +146,18 @@
|
||||
|
||||
/* Base values for use with Tailwind. */
|
||||
/* Chalkboard */
|
||||
--_chalkboard-10: 99.7% 0.008766 102.8deg;
|
||||
--_chalkboard-20: 91.34% 0.009353 109deg;
|
||||
--_chalkboard-30: 82.99% 0.00994 115.2deg;
|
||||
--_chalkboard-40: 74.63% 0.01053 121.4deg;
|
||||
--_chalkboard-50: 66.27% 0.01111 127.6deg;
|
||||
--_chalkboard-60: 57.92% 0.0117 133.9deg;
|
||||
--_chalkboard-70: 49.56% 0.01229 140.1deg;
|
||||
--_chalkboard-80: 41.21% 0.01288 146.3deg;
|
||||
--_chalkboard-90: 32.85% 0.01346 152.5deg;
|
||||
--_chalkboard-100: 24.49% 0.01405 158.7deg;
|
||||
--_chalkboard-110: 16.14% 0.01464 164.9deg;
|
||||
--_chalkboard-120: 7.783% 0.01522 171.1deg;
|
||||
--_chalkboard-10: 99.11% 0 var(--primary-hue);
|
||||
--_chalkboard-20: 95.51% 0 var(--primary-hue);
|
||||
--_chalkboard-30: 88.48% 0 var(--primary-hue);
|
||||
--_chalkboard-40: 82.01% 0 var(--primary-hue);
|
||||
--_chalkboard-50: 72.41% 0 var(--primary-hue);
|
||||
--_chalkboard-60: 68.19% 0 var(--primary-hue);
|
||||
--_chalkboard-70: 48.19% 0 var(--primary-hue);
|
||||
--_chalkboard-80: 39.04% 0 var(--primary-hue);
|
||||
--_chalkboard-90: 30.12% 0 var(--primary-hue);
|
||||
--_chalkboard-100: 22.64% 0 var(--primary-hue);
|
||||
--_chalkboard-110: 18.22% 0 var(--primary-hue);
|
||||
--_chalkboard-120: 0% 0 var(--primary-hue);
|
||||
|
||||
/* Energy */
|
||||
--_energy-10: 93.31% 0.227 122.3deg;
|
||||
|
@ -16,7 +16,7 @@ export function AstExplorer() {
|
||||
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '300px' }}>
|
||||
<div id="ast-explorer" className="relative">
|
||||
<div className="">
|
||||
filter out keys:<div className="w-2 inline-block"></div>
|
||||
{['start', 'end', 'type'].map((key) => {
|
||||
@ -45,7 +45,7 @@ export function AstExplorer() {
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
>
|
||||
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||
<pre className="text-xs">
|
||||
<DisplayObj
|
||||
obj={kclManager.ast}
|
||||
filterKeys={filterKeys}
|
||||
@ -109,7 +109,7 @@ function DisplayObj({
|
||||
<pre
|
||||
ref={ref}
|
||||
className={`ml-2 border-l border-violet-600 pl-1 ${
|
||||
hasCursor ? 'bg-violet-100/25' : ''
|
||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
setHighlightRange([obj?.start || 0, obj.end])
|
||||
|
@ -1,57 +0,0 @@
|
||||
.panel {
|
||||
@apply relative z-0;
|
||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
.header::before,
|
||||
.header::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.dark) .panel {
|
||||
@apply bg-chalkboard-110/50 backdrop-blur-0;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 z-10 cursor-pointer;
|
||||
@apply flex items-center justify-between gap-2 w-full p-2;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply bg-chalkboard-10;
|
||||
}
|
||||
|
||||
.header:not(:last-of-type) {
|
||||
@apply border-b;
|
||||
}
|
||||
|
||||
:global(.dark) .header {
|
||||
@apply bg-chalkboard-110 border-b-chalkboard-90 text-chalkboard-30;
|
||||
}
|
||||
|
||||
:global(.dark) .header:not(:last-of-type) {
|
||||
@apply border-b-2;
|
||||
}
|
||||
|
||||
.panel:first-of-type .header {
|
||||
@apply rounded-t;
|
||||
}
|
||||
|
||||
.panel:last-of-type .header {
|
||||
@apply rounded-b;
|
||||
}
|
||||
|
||||
.panel[open] .header {
|
||||
@apply rounded-t rounded-b-none;
|
||||
}
|
||||
|
||||
.panel[open] {
|
||||
@apply flex-grow max-h-full h-48 my-1 rounded;
|
||||
}
|
||||
|
||||
.panel[open] + .panel[open],
|
||||
.panel[open]:first-of-type {
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
.panel[open]:last-of-type {
|
||||
@apply mb-0;
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import styles from './CollapsiblePanel.module.css'
|
||||
|
||||
export interface CollapsiblePanelProps
|
||||
extends React.PropsWithChildren,
|
||||
React.HTMLAttributes<HTMLDetailsElement> {
|
||||
title: string
|
||||
icon?: IconDefinition
|
||||
open?: boolean
|
||||
menu?: React.ReactNode
|
||||
detailsTestId?: string
|
||||
iconClassNames?: {
|
||||
bg?: string
|
||||
icon?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const PanelHeader = ({
|
||||
title,
|
||||
icon,
|
||||
iconClassNames,
|
||||
menu,
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<summary className={styles.header}>
|
||||
<div className="flex gap-2 items-center flex-1">
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
bgClassName={
|
||||
'dark:!bg-transparent group-open:bg-primary dark:group-open:!bg-primary rounded-sm ' +
|
||||
(iconClassNames?.bg || '')
|
||||
}
|
||||
iconClassName={
|
||||
'group-open:text-chalkboard-10 ' + (iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
</div>
|
||||
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||
{menu}
|
||||
</div>
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
|
||||
export const CollapsiblePanel = ({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
className,
|
||||
iconClassNames,
|
||||
menu,
|
||||
detailsTestId,
|
||||
...props
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<details
|
||||
{...props}
|
||||
data-testid={detailsTestId}
|
||||
className={
|
||||
styles.panel + ' pointer-events-auto group ' + (className || '')
|
||||
}
|
||||
>
|
||||
<PanelHeader
|
||||
title={title}
|
||||
icon={icon}
|
||||
iconClassNames={iconClassNames}
|
||||
menu={menu}
|
||||
/>
|
||||
{children}
|
||||
</details>
|
||||
)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { AstExplorer } from './AstExplorer'
|
||||
import { EngineCommands } from './EngineCommands'
|
||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||
|
||||
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
{...props}
|
||||
className={
|
||||
'!absolute overflow-auto !h-auto bottom-5 right-5 ' + className
|
||||
}
|
||||
// header height, top-5, and bottom-5
|
||||
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
|
||||
detailsTestId="debug-panel"
|
||||
>
|
||||
<section className="p-4 flex flex-col gap-4">
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||
<AstExplorer />
|
||||
</div>
|
||||
</section>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
@ -3,7 +3,9 @@ import { engineCommandManager } from 'lib/singletons'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
function useEngineCommands(): [CommandLog[], () => void] {
|
||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>([])
|
||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
|
||||
engineCommandManager.commandLogs
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
engineCommandManager.registerCommandLogCallback((commands) =>
|
||||
|
@ -171,7 +171,7 @@ const FileTreeItem = ({
|
||||
|
||||
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
||||
// Import non-kcl files
|
||||
kclManager.setCodeAndExecute(
|
||||
kclManager.setCode(
|
||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||
kclManager.code
|
||||
)
|
||||
|
@ -1,76 +0,0 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { useEffect } from 'react'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
|
||||
const ReactJsonTypeHack = ReactJson as any
|
||||
|
||||
interface LogPanelProps extends CollapsiblePanelProps {
|
||||
theme?: Exclude<Themes, Themes.System>
|
||||
}
|
||||
|
||||
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
||||
const { logs } = useKclContext()
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('.console-tile')
|
||||
if (element) {
|
||||
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||
}
|
||||
}, [logs])
|
||||
return (
|
||||
<CollapsiblePanel {...props}>
|
||||
<div className="relative w-full">
|
||||
<div className="absolute inset-0 flex flex-col">
|
||||
<ReactJsonTypeHack
|
||||
src={logs}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayArrayKey={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export const KCLErrors = ({
|
||||
theme = Themes.Light,
|
||||
...props
|
||||
}: LogPanelProps) => {
|
||||
const { errors } = useKclContext()
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('.console-tile')
|
||||
if (element) {
|
||||
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||
}
|
||||
}, [errors])
|
||||
return (
|
||||
<CollapsiblePanel {...props}>
|
||||
<div className="h-full relative">
|
||||
<div className="absolute inset-0 flex flex-col">
|
||||
<ReactJsonTypeHack
|
||||
src={errors}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayArrayKey={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import React, { createContext, useMemo, useContext } from 'react'
|
||||
import React, { createContext, useMemo, useEffect, useContext } from 'react'
|
||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||
import Server from '../editor/plugins/lsp/server'
|
||||
import Client from '../editor/plugins/lsp/client'
|
||||
@ -14,6 +14,7 @@ import { LanguageSupport } from '@codemirror/language'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from 'lib/paths'
|
||||
import { FileEntry } from 'lib/types'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
|
||||
const DEFAULT_FILE_NAME: string = 'main.kcl'
|
||||
|
||||
@ -60,16 +61,27 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
isCopilotLspServerReady,
|
||||
setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady,
|
||||
isStreamReady,
|
||||
} = useStore((s) => ({
|
||||
isKclLspServerReady: s.isKclLspServerReady,
|
||||
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
||||
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const {
|
||||
auth,
|
||||
settings: {
|
||||
context: {
|
||||
modeling: { defaultUnit },
|
||||
},
|
||||
},
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const navigate = useNavigate()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
// But the server happens async so we break this into two parts.
|
||||
@ -87,7 +99,11 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
|
||||
return { lspClient }
|
||||
}, [setIsKclLspServerReady, token])
|
||||
}, [
|
||||
setIsKclLspServerReady,
|
||||
// We need a token for authenticating the server.
|
||||
token,
|
||||
])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// Now that we have multi-file support the name of the file is a dep of
|
||||
@ -109,6 +125,25 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return plugin
|
||||
}, [kclLspClient, isKclLspServerReady])
|
||||
|
||||
// Re-execute the scene when the units change.
|
||||
useEffect(() => {
|
||||
let plugins = kclLspClient.plugins
|
||||
for (let plugin of plugins) {
|
||||
if (plugin.updateUnits && isStreamReady && isNetworkOkay) {
|
||||
plugin.updateUnits(defaultUnit.current)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
kclLspClient,
|
||||
defaultUnit.current,
|
||||
|
||||
// We want to re-execute the scene if the network comes back online.
|
||||
// The lsp server will only re-execute if there were previous errors or
|
||||
// changes, so it's fine to send it thru here.
|
||||
isStreamReady,
|
||||
isNetworkOkay,
|
||||
])
|
||||
|
||||
const { lspClient: copilotLspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
|
@ -1,70 +0,0 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
theme?: Exclude<Themes, Themes.System>
|
||||
}
|
||||
|
||||
export const MemoryPanel = ({
|
||||
theme = Themes.Light,
|
||||
...props
|
||||
}: MemoryPanelProps) => {
|
||||
const { programMemory } = useKclContext()
|
||||
const ProcessedMemory = useMemo(
|
||||
() => processMemory(programMemory),
|
||||
[programMemory]
|
||||
)
|
||||
return (
|
||||
<CollapsiblePanel {...props}>
|
||||
<div className="h-full relative">
|
||||
<div className="absolute inset-0 flex flex-col items-start">
|
||||
<div
|
||||
className="overflow-y-auto h-full console-tile w-full"
|
||||
style={{ marginBottom: 36 }}
|
||||
>
|
||||
{/* 36px is the height of PanelHeader */}
|
||||
<ReactJson
|
||||
src={ProcessedMemory}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export const processMemory = (programMemory: ProgramMemory) => {
|
||||
const processedMemory: any = {}
|
||||
Object.keys(programMemory?.root || {}).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
processedMemory[key] = val.value
|
||||
}
|
||||
} else if (key !== 'log') {
|
||||
processedMemory[key] = '__function__'
|
||||
}
|
||||
})
|
||||
return processedMemory
|
||||
}
|
@ -21,6 +21,7 @@ import {
|
||||
} from './Toolbar/SetAngleBetween'
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import { pathMapToSelections } from 'lang/util'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { useStore } from 'useStore'
|
||||
import {
|
||||
Selections,
|
||||
@ -38,7 +39,7 @@ import {
|
||||
getSketchQuaternion,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
|
||||
import { Program, parse } from 'lang/wasm'
|
||||
import { Program, coreDump, parse } from 'lang/wasm'
|
||||
import { getNodePathFromSourceRange, isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { TEST } from 'env'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
@ -47,6 +48,8 @@ import toast from 'react-hot-toast'
|
||||
import { EditorSelection } from '@uiw/react-codemirror'
|
||||
import { Vector3 } from 'three'
|
||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -76,6 +79,16 @@ export const ModelingMachineProvider = ({
|
||||
const token = auth?.context?.token
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
useSetupEngineManager(streamRef, token, theme.current)
|
||||
const { htmlRef } = useStore((s) => ({
|
||||
htmlRef: s.htmlRef,
|
||||
}))
|
||||
const coreDumpManager = new CoreDumpManager(
|
||||
engineCommandManager,
|
||||
htmlRef,
|
||||
token
|
||||
)
|
||||
const { lspClients } = useLspContext()
|
||||
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
|
||||
|
||||
const {
|
||||
isShiftDown,
|
||||
@ -105,6 +118,24 @@ export const ModelingMachineProvider = ({
|
||||
modelingMachine,
|
||||
{
|
||||
actions: {
|
||||
'disable lsp execution': async () => {
|
||||
// Update the lsp server that we are in sketch mode, so we can turn off lsp execution.
|
||||
for (const lspClient of lspClients) {
|
||||
for (const plugin of lspClient.plugins) {
|
||||
await plugin.updateCanExecute(false)
|
||||
}
|
||||
}
|
||||
await kclManager.enterEditMode()
|
||||
console.log('done with disabling lsp execution')
|
||||
},
|
||||
'enable lsp execution': async () => {
|
||||
// Update the lsp server that we are done with sketch mode, so we can turn back on lsp execution.
|
||||
for (const lspClient of lspClients) {
|
||||
for (const plugin of lspClient.plugins) {
|
||||
await plugin.updateCanExecute(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
'sketch exit execute': () => {
|
||||
try {
|
||||
kclManager.executeAst(parse(kclManager.code))
|
||||
|
27
src/components/ModelingSidebar/ModelingPane.module.css
Normal file
@ -0,0 +1,27 @@
|
||||
.panel {
|
||||
@apply relative z-0 rounded-r max-w-full h-full flex-1;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
@apply bg-chalkboard-10/50 backdrop-blur-sm border border-chalkboard-20;
|
||||
scroll-margin-block-start: 41px;
|
||||
}
|
||||
|
||||
.header::before,
|
||||
.header::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.dark) .panel {
|
||||
@apply bg-chalkboard-100/50 backdrop-blur-[3px] border-chalkboard-80;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply z-10 relative rounded-tr;
|
||||
@apply flex h-[41px] items-center justify-between gap-2 px-2;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply bg-chalkboard-10 border-b border-chalkboard-20;
|
||||
}
|
||||
|
||||
:global(.dark) .header {
|
||||
@apply bg-chalkboard-90 text-chalkboard-30 border-chalkboard-80;
|
||||
}
|
54
src/components/ModelingSidebar/ModelingPane.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { useStore } from 'useStore'
|
||||
import styles from './ModelingPane.module.css'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
|
||||
export interface ModelingPaneProps
|
||||
extends React.PropsWithChildren,
|
||||
React.HTMLAttributes<HTMLDivElement> {
|
||||
title: string
|
||||
Menu?: React.ReactNode | React.FC
|
||||
detailsTestId?: string
|
||||
}
|
||||
|
||||
export const ModelingPaneHeader = ({
|
||||
title,
|
||||
Menu,
|
||||
}: Pick<ModelingPaneProps, 'title' | 'Menu'>) => {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className="flex gap-2 items-center flex-1">{title}</div>
|
||||
{Menu instanceof Function ? <Menu /> : Menu}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ModelingPane = ({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
Menu,
|
||||
detailsTestId,
|
||||
...props
|
||||
}: ModelingPaneProps) => {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
const { buttonDownInStream } = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
}))
|
||||
const pointerEventsCssClass =
|
||||
buttonDownInStream || onboardingStatus.current === 'camera'
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto '
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
data-testid={detailsTestId}
|
||||
className={
|
||||
pointerEventsCssClass + styles.panel + ' group ' + (className || '')
|
||||
}
|
||||
>
|
||||
<ModelingPaneHeader title={title} Menu={Menu} />
|
||||
<div className="relative w-full">{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
18
src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { AstExplorer } from '../../AstExplorer'
|
||||
import { EngineCommands } from '../../EngineCommands'
|
||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||
|
||||
export const DebugPane = () => {
|
||||
return (
|
||||
<section
|
||||
data-testid="debug-panel"
|
||||
className="absolute inset-0 p-2 box-border overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<AstExplorer />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import styles from './CodeMenu.module.css'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import styles from './KclEditorMenu.module.css'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { editorShortcutMeta } from './TextEditor'
|
||||
import { editorShortcutMeta } from './KclEditorPane'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
|
||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
useConvertToVariable()
|
||||
|
||||
@ -30,7 +30,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
className="p-1"
|
||||
size="sm"
|
||||
bgClassName={
|
||||
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-active:!bg-primary/10 dark:ui-active:!bg-chalkboard-100 rounded-sm'
|
||||
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-open:!bg-primary/10 dark:ui-open:!bg-chalkboard-100 rounded-sm'
|
||||
}
|
||||
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
|
||||
/>
|
||||
@ -65,7 +65,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
>
|
||||
<span>Read the KCL docs</span>
|
||||
<small>
|
||||
On GitHub
|
||||
zoo.dev
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1 align-text-top"
|
||||
@ -83,7 +83,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
>
|
||||
<span>KCL samples</span>
|
||||
<small>
|
||||
On GitHub
|
||||
zoo.dev
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1 align-text-top"
|
@ -1,35 +1,65 @@
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import ReactCodeMirror, {
|
||||
Extension,
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
SelectionRange,
|
||||
} from '@uiw/react-codemirror'
|
||||
import ReactCodeMirror from '@uiw/react-codemirror'
|
||||
import { TEST } from 'env'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||
import { lineHighlightField } from 'editor/highlightextension'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
import {
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
EditorView,
|
||||
dropCursor,
|
||||
drawSelection,
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
indentWithTab,
|
||||
defaultKeymap,
|
||||
historyKeymap,
|
||||
history,
|
||||
} from '@codemirror/commands'
|
||||
import { lintGutter, lintKeymap } from '@codemirror/lint'
|
||||
import {
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
bracketMatching,
|
||||
indentOnInput,
|
||||
codeFolding,
|
||||
syntaxHighlighting,
|
||||
defaultHighlightStyle,
|
||||
} from '@codemirror/language'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from 'lib/paths'
|
||||
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { Prec, EditorState, Extension, SelectionRange } from '@codemirror/state'
|
||||
import {
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
hasNextSnippetField,
|
||||
} from '@codemirror/autocomplete'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
codeMirror: 'Alt-Shift-f',
|
||||
display: 'Alt + Shift + F',
|
||||
},
|
||||
convertToVariable: {
|
||||
@ -38,28 +68,23 @@ export const editorShortcutMeta = {
|
||||
},
|
||||
}
|
||||
|
||||
export const TextEditor = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
export const KclEditorPane = () => {
|
||||
const {
|
||||
settings: { context },
|
||||
} = useSettingsAuthContext()
|
||||
const theme =
|
||||
context.app.theme.current === Themes.System
|
||||
? getSystemTheme()
|
||||
: context.app.theme.current
|
||||
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
setEditorView: s.setEditorView,
|
||||
isShiftDown: s.isShiftDown,
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
const { code } = useKclContext()
|
||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
const { copilotLSP, kclLSP } = useLspContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
|
||||
window.addEventListener('online', onlineCallback)
|
||||
return () => window.removeEventListener('online', onlineCallback)
|
||||
}, [])
|
||||
const navigate = useNavigate()
|
||||
|
||||
useHotkeys('mod+z', (e) => {
|
||||
e.preventDefault()
|
||||
@ -82,17 +107,33 @@ export const TextEditor = ({
|
||||
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const textWrapping = settings.context.textEditor.textWrapping
|
||||
const cursorBlinking = settings.context.textEditor.blinkingCursor
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = async (newCode: string) => {
|
||||
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
||||
else kclManager.setCode(newCode)
|
||||
} //, []);
|
||||
// If we are just fucking around in a snippet, return early and don't
|
||||
// trigger stuff below that might cause the component to re-render.
|
||||
// Otherwise we will not be able to tab thru the snippet portions.
|
||||
// We explicitly dont check HasPrevSnippetField because we always add
|
||||
// a ${} to the end of the function so that's fine.
|
||||
if (editorView && hasNextSnippetField(editorView.state)) {
|
||||
return
|
||||
}
|
||||
|
||||
kclManager.setCode(newCode)
|
||||
}
|
||||
const lastSelection = useRef('')
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
// If we are just fucking around in a snippet, return early and don't
|
||||
// trigger stuff below that might cause the component to re-render.
|
||||
// Otherwise we will not be able to tab thru the snippet portions.
|
||||
// We explicitly dont check HasPrevSnippetField because we always add
|
||||
// a ${} to the end of the function so that's fine.
|
||||
if (hasNextSnippetField(viewUpdate.view.state)) {
|
||||
return
|
||||
}
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
@ -146,8 +187,22 @@ export const TextEditor = ({
|
||||
|
||||
const editorExtensions = useMemo(() => {
|
||||
const extensions = [
|
||||
drawSelection({
|
||||
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
||||
}),
|
||||
lineHighlightField,
|
||||
history(),
|
||||
closeBrackets(),
|
||||
codeFolding(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
indentWithTab,
|
||||
{
|
||||
key: 'Meta-k',
|
||||
run: () => {
|
||||
@ -156,10 +211,10 @@ export const TextEditor = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.formatCode.codeMirror,
|
||||
key: isTauri() ? 'Meta-,' : 'Meta-Shift-,',
|
||||
run: () => {
|
||||
kclManager.format()
|
||||
return true
|
||||
navigate(makeUrlPathRelative(paths.SETTINGS))
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -175,16 +230,28 @@ export const TextEditor = ({
|
||||
]),
|
||||
] as Extension[]
|
||||
|
||||
if (kclLSP) extensions.push(kclLSP)
|
||||
if (kclLSP) extensions.push(Prec.highest(kclLSP))
|
||||
if (copilotLSP) extensions.push(copilotLSP)
|
||||
|
||||
// These extensions have proven to mess with vitest
|
||||
if (!TEST) {
|
||||
extensions.push(
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(errors)
|
||||
}),
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
rectangularSelection(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
interact({
|
||||
rules: [
|
||||
// a rule for a number dragger
|
||||
@ -222,22 +289,22 @@ export const TextEditor = ({
|
||||
}
|
||||
|
||||
return extensions
|
||||
}, [kclLSP, textWrapping.current, convertCallback])
|
||||
}, [kclLSP, textWrapping.current, cursorBlinking.current, convertCallback])
|
||||
|
||||
return (
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className="full-height-subtract"
|
||||
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
||||
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
||||
>
|
||||
<ReactCodeMirror
|
||||
className="h-full"
|
||||
value={code}
|
||||
extensions={editorExtensions}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
indentWithTab={false}
|
||||
basicSetup={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
@ -0,0 +1,53 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { useResolvedTheme } from 'hooks/useResolvedTheme'
|
||||
|
||||
const ReactJsonTypeHack = ReactJson as any
|
||||
|
||||
export const LogsPane = () => {
|
||||
const theme = useResolvedTheme()
|
||||
const { logs } = useKclContext()
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
|
||||
<ReactJsonTypeHack
|
||||
src={logs}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayArrayKey={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const KclErrorsPane = () => {
|
||||
const theme = useResolvedTheme()
|
||||
const { errors } = useKclContext()
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
|
||||
<ReactJsonTypeHack
|
||||
src={errors}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayArrayKey={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { processMemory } from './MemoryPanel'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { initPromise, parse } from '../lang/wasm'
|
||||
import { processMemory } from './MemoryPane'
|
||||
import { enginelessExecutor } from '../../../lib/testHelpers'
|
||||
import { initPromise, parse } from '../../../lang/wasm'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
57
src/components/ModelingSidebar/ModelingPanes/MemoryPane.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from 'lang/wasm'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { useResolvedTheme } from 'hooks/useResolvedTheme'
|
||||
|
||||
export const MemoryPane = () => {
|
||||
const theme = useResolvedTheme()
|
||||
const { programMemory } = useKclContext()
|
||||
const ProcessedMemory = useMemo(
|
||||
() => processMemory(programMemory),
|
||||
[programMemory]
|
||||
)
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<div className="absolute inset-0 p-2 flex flex-col items-start">
|
||||
<div className="overflow-auto h-full w-full pb-12">
|
||||
<ReactJson
|
||||
src={ProcessedMemory}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const processMemory = (programMemory: ProgramMemory) => {
|
||||
const processedMemory: any = {}
|
||||
Object.keys(programMemory?.root || {}).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
processedMemory[key] = val.value
|
||||
}
|
||||
} else if (key !== 'log') {
|
||||
processedMemory[key] = '__function__'
|
||||
}
|
||||
})
|
||||
return processedMemory
|
||||
}
|
67
src/components/ModelingSidebar/ModelingPanes/index.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
IconDefinition,
|
||||
faBugSlash,
|
||||
faCode,
|
||||
faCodeCommit,
|
||||
faExclamationCircle,
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||
import { ReactNode } from 'react'
|
||||
import type { PaneType } from 'useStore'
|
||||
import { MemoryPane } from './MemoryPane'
|
||||
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
||||
import { DebugPane } from './DebugPane'
|
||||
|
||||
export type Pane = {
|
||||
id: PaneType
|
||||
title: string
|
||||
icon: CustomIconName | IconDefinition
|
||||
Content: ReactNode | React.FC
|
||||
Menu?: ReactNode | React.FC
|
||||
keybinding: string
|
||||
}
|
||||
|
||||
export const topPanes: Pane[] = [
|
||||
{
|
||||
id: 'code',
|
||||
title: 'KCL Code',
|
||||
icon: faCode,
|
||||
Content: KclEditorPane,
|
||||
keybinding: 'shift + c',
|
||||
Menu: KclEditorMenu,
|
||||
},
|
||||
]
|
||||
|
||||
export const bottomPanes: Pane[] = [
|
||||
{
|
||||
id: 'variables',
|
||||
title: 'Variables',
|
||||
icon: faSquareRootVariable,
|
||||
Content: MemoryPane,
|
||||
keybinding: 'shift + v',
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
title: 'Logs',
|
||||
icon: faCodeCommit,
|
||||
Content: LogsPane,
|
||||
keybinding: 'shift + l',
|
||||
},
|
||||
{
|
||||
id: 'kclErrors',
|
||||
title: 'KCL Errors',
|
||||
icon: faExclamationCircle,
|
||||
Content: KclErrorsPane,
|
||||
keybinding: 'shift + e',
|
||||
},
|
||||
{
|
||||
id: 'debug',
|
||||
title: 'Debug',
|
||||
icon: faBugSlash,
|
||||
Content: DebugPane,
|
||||
keybinding: 'shift + d',
|
||||
},
|
||||
]
|
11
src/components/ModelingSidebar/ModelingSidebar.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
row-gap: 0.25rem;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
padding-block: 1px;
|
||||
max-width: 100%;
|
||||
flex: 1 1 0;
|
||||
}
|
212
src/components/ModelingSidebar/ModelingSidebar.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Resizable } from 're-resizable'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { PaneType, useStore } from 'useStore'
|
||||
import { Tab } from '@headlessui/react'
|
||||
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import styles from './ModelingSidebar.module.css'
|
||||
import { ModelingPane } from './ModelingPane'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
}
|
||||
|
||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
const { openPanes, buttonDownInStream } = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
openPanes: s.openPanes,
|
||||
}))
|
||||
const pointerEventsCssClass =
|
||||
buttonDownInStream ||
|
||||
onboardingStatus.current === 'camera' ||
|
||||
openPanes.length === 0
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto '
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
className={`flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||
defaultSize={{
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={800}
|
||||
handleClasses={{
|
||||
right:
|
||||
(openPanes.length === 0 ? 'hidden ' : 'block ') +
|
||||
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ',
|
||||
}}
|
||||
>
|
||||
<div className={styles.grid + ' flex-1'}>
|
||||
<ModelingSidebarSection panes={topPanes} />
|
||||
<ModelingSidebarSection panes={bottomPanes} alignButtons="end" />
|
||||
</div>
|
||||
</Resizable>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelingSidebarSectionProps {
|
||||
panes: Pane[]
|
||||
alignButtons?: 'start' | 'end'
|
||||
}
|
||||
|
||||
function ModelingSidebarSection({
|
||||
panes,
|
||||
alignButtons = 'start',
|
||||
}: ModelingSidebarSectionProps) {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||
const paneIds = panes.map((pane) => pane.id)
|
||||
const { openPanes, setOpenPanes } = useStore((s) => ({
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
}))
|
||||
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
||||
const [currentPane, setCurrentPane] = useState(
|
||||
foundOpenPane || ('none' as PaneType | 'none')
|
||||
)
|
||||
|
||||
const togglePane = useCallback(
|
||||
(newPane: PaneType | 'none') => {
|
||||
if (newPane === 'none') {
|
||||
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
||||
setCurrentPane('none')
|
||||
} else if (newPane === currentPane) {
|
||||
setCurrentPane('none')
|
||||
setOpenPanes(openPanes.filter((p) => p !== newPane))
|
||||
} else {
|
||||
setOpenPanes([...openPanes.filter((p) => p !== currentPane), newPane])
|
||||
setCurrentPane(newPane)
|
||||
}
|
||||
},
|
||||
[openPanes, setOpenPanes, currentPane, setCurrentPane]
|
||||
)
|
||||
|
||||
// Filter out the debug panel if it's not supposed to be shown
|
||||
// TODO: abstract out for allowing user to configure which panes to show
|
||||
const filteredPanes = showDebugPanel.current
|
||||
? panes
|
||||
: panes.filter((pane) => pane.id !== 'debug')
|
||||
useEffect(() => {
|
||||
if (
|
||||
!showDebugPanel.current &&
|
||||
currentPane === 'debug' &&
|
||||
openPanes.includes('debug')
|
||||
) {
|
||||
togglePane('debug')
|
||||
}
|
||||
}, [showDebugPanel.current, togglePane, openPanes])
|
||||
|
||||
return (
|
||||
<Tab.Group
|
||||
vertical
|
||||
selectedIndex={
|
||||
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
|
||||
}
|
||||
onChange={(index) => {
|
||||
const newPane = index === 0 ? 'none' : paneIds[index - 1]
|
||||
togglePane(newPane)
|
||||
}}
|
||||
>
|
||||
<Tab.List
|
||||
className={
|
||||
'pointer-events-auto ' +
|
||||
(alignButtons === 'start'
|
||||
? 'justify-start self-start'
|
||||
: 'justify-end self-end') +
|
||||
(currentPane === 'none'
|
||||
? ' rounded-r focus-within:!border-primary/50'
|
||||
: ' border-r-0') +
|
||||
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 ' +
|
||||
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
|
||||
}
|
||||
>
|
||||
<Tab key="none" className="sr-only">
|
||||
No panes open
|
||||
</Tab>
|
||||
{filteredPanes.map((pane) => (
|
||||
<ModelingPaneButton
|
||||
key={pane.id}
|
||||
paneConfig={pane}
|
||||
currentPane={currentPane}
|
||||
togglePane={() => togglePane(pane.id)}
|
||||
/>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels
|
||||
as="article"
|
||||
className={
|
||||
'col-start-2 col-span-1 ' +
|
||||
(openPanes.length === 1
|
||||
? currentPane !== 'none'
|
||||
? `row-start-1 row-end-3`
|
||||
: `hidden`
|
||||
: ``)
|
||||
}
|
||||
>
|
||||
<Tab.Panel key="none" />
|
||||
{filteredPanes.map((pane) => (
|
||||
<Tab.Panel key={pane.id} className="h-full">
|
||||
<ModelingPane title={pane.title} Menu={pane.Menu}>
|
||||
{pane.Content instanceof Function ? (
|
||||
<pane.Content />
|
||||
) : (
|
||||
pane.Content
|
||||
)}
|
||||
</ModelingPane>
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelingPaneButtonProps {
|
||||
paneConfig: Pane
|
||||
currentPane: PaneType | 'none'
|
||||
togglePane: () => void
|
||||
}
|
||||
|
||||
function ModelingPaneButton({
|
||||
paneConfig,
|
||||
currentPane,
|
||||
togglePane,
|
||||
}: ModelingPaneButtonProps) {
|
||||
useHotkeys(paneConfig.keybinding, togglePane, {
|
||||
scopes: ['modeling'],
|
||||
})
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={paneConfig.id}
|
||||
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none"
|
||||
onClick={togglePane}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={paneConfig.icon}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
iconClassName={
|
||||
paneConfig.id === currentPane
|
||||
? ' !text-chalkboard-10'
|
||||
: '!text-chalkboard-80 dark:!text-chalkboard-30'
|
||||
}
|
||||
bgClassName={
|
||||
'rounded-sm ' +
|
||||
(paneConfig.id === currentPane ? '!bg-primary' : '!bg-transparent')
|
||||
}
|
||||
/>
|
||||
<Tooltip position="right" hoverOnly delay={800}>
|
||||
<span>{paneConfig.title}</span>
|
||||
<br />
|
||||
<span className="text-xs capitalize">{paneConfig.keybinding}</span>
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
)
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
} from 'xstate'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||
import { sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
@ -116,7 +116,7 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
})
|
||||
},
|
||||
toastSuccess: (context, event) => {
|
||||
toastSuccess: (_, event) => {
|
||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||
keyof typeof settings,
|
||||
string
|
||||
@ -138,7 +138,6 @@ export const SettingsAuthProviderBase = ({
|
||||
id: `${event.type}.success`,
|
||||
})
|
||||
},
|
||||
'Execute AST': () => kclManager.executeAst(),
|
||||
persistSettings: (context) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
},
|
||||
@ -211,6 +210,19 @@ export const SettingsAuthProviderBase = ({
|
||||
)
|
||||
}, [settingsState.context.app.themeColor.current])
|
||||
|
||||
/**
|
||||
* Update the --cursor-color CSS variable
|
||||
* based on the setting textEditor.blinkingCursor.current
|
||||
*/
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--cursor-color`,
|
||||
settingsState.context.textEditor.blinkingCursor.current
|
||||
? 'auto'
|
||||
: 'transparent'
|
||||
)
|
||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||
|
||||
// Auth machine setup
|
||||
const [authState, authSend, authActor] = useMachine(authMachine, {
|
||||
actions: {
|
||||
|
@ -77,7 +77,4 @@ export function applyConstraintEqualLength({
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
return { modifiedAst, pathToNodeMap }
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// // callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
@ -94,11 +94,15 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:is(:hover, :focus-visible, :active) > .tooltip {
|
||||
:is(:hover, :active) > .tooltip {
|
||||
opacity: 1;
|
||||
transition-delay: var(--_delay);
|
||||
}
|
||||
|
||||
:is(:focus-visible) > .tooltip.withFocus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:is(:focus, :focus-visible, :focus-within) > .tooltip {
|
||||
--_delay: 0 !important;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ interface TooltipProps extends React.PropsWithChildren {
|
||||
| 'inlineEnd'
|
||||
className?: string
|
||||
delay?: number
|
||||
hoverOnly?: boolean
|
||||
}
|
||||
|
||||
export default function Tooltip({
|
||||
@ -22,13 +23,16 @@ export default function Tooltip({
|
||||
position = 'top',
|
||||
className,
|
||||
delay = 200,
|
||||
hoverOnly = false,
|
||||
}: TooltipProps) {
|
||||
return (
|
||||
<div
|
||||
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
|
||||
inert="true"
|
||||
role="tooltip"
|
||||
className={styles.tooltip + ' ' + styles[position] + ' ' + className}
|
||||
className={`${styles.tooltip} ${hoverOnly ? '' : styles.withFocus} ${
|
||||
styles[position]
|
||||
} ${className}`}
|
||||
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
|
@ -16,7 +16,7 @@ export const lineHighlightField = StateField.define({
|
||||
if (e.is(addLineHighlight)) {
|
||||
lines = Decoration.none
|
||||
const [from, to] = e.value || [0, 0]
|
||||
if (!(from === to && from === 0)) {
|
||||
if (from && to && !(from === to && from === 0)) {
|
||||
lines = lines.update({ add: [matchDeco.range(from, to)] })
|
||||
deco.push(matchDeco.range(from, to))
|
||||
}
|
||||
|
@ -6,6 +6,10 @@ import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspComp
|
||||
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
|
||||
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
||||
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
||||
import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams'
|
||||
import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams'
|
||||
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
||||
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
|
||||
@ -21,9 +25,17 @@ interface LSPRequestMap {
|
||||
LSP.SemanticTokensParams,
|
||||
LSP.SemanticTokens
|
||||
]
|
||||
getCompletions: [CopilotLspCompletionParams, CopilotCompletionResponse]
|
||||
notifyAccepted: [CopilotAcceptCompletionParams, any]
|
||||
notifyRejected: [CopilotRejectCompletionParams, any]
|
||||
'textDocument/formatting': [
|
||||
LSP.DocumentFormattingParams,
|
||||
LSP.TextEdit[] | null
|
||||
]
|
||||
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
|
||||
'copilot/getCompletions': [
|
||||
CopilotLspCompletionParams,
|
||||
CopilotCompletionResponse
|
||||
]
|
||||
'kcl/updateUnits': [UpdateUnitsParams, UpdateUnitsResponse | null]
|
||||
'kcl/updateCanExecute': [UpdateCanExecuteParams, UpdateCanExecuteResponse]
|
||||
}
|
||||
|
||||
// Client to server
|
||||
@ -36,6 +48,8 @@ interface LSPNotifyMap {
|
||||
'workspace/didCreateFiles': LSP.CreateFilesParams
|
||||
'workspace/didRenameFiles': LSP.RenameFilesParams
|
||||
'workspace/didDeleteFiles': LSP.DeleteFilesParams
|
||||
'copilot/notifyAccepted': CopilotAcceptCompletionParams
|
||||
'copilot/notifyRejected': CopilotRejectCompletionParams
|
||||
}
|
||||
|
||||
export interface LanguageServerClientOptions {
|
||||
@ -53,11 +67,11 @@ export interface LanguageServerOptions {
|
||||
|
||||
export class LanguageServerClient {
|
||||
private client: Client
|
||||
private name: string
|
||||
readonly name: string
|
||||
|
||||
public ready: boolean
|
||||
|
||||
private plugins: LanguageServerPlugin[]
|
||||
readonly plugins: LanguageServerPlugin[]
|
||||
|
||||
public initializePromise: Promise<void>
|
||||
|
||||
@ -182,6 +196,22 @@ export class LanguageServerClient {
|
||||
return await this.request('textDocument/hover', params)
|
||||
}
|
||||
|
||||
async textDocumentFormatting(params: LSP.DocumentFormattingParams) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.documentFormattingProvider) {
|
||||
return
|
||||
}
|
||||
return await this.request('textDocument/formatting', params)
|
||||
}
|
||||
|
||||
async textDocumentFoldingRange(params: LSP.FoldingRangeParams) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.foldingRangeProvider) {
|
||||
return
|
||||
}
|
||||
return await this.request('textDocument/foldingRange', params)
|
||||
}
|
||||
|
||||
async textDocumentCompletion(params: LSP.CompletionParams) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.completionProvider) {
|
||||
@ -215,7 +245,7 @@ export class LanguageServerClient {
|
||||
}
|
||||
|
||||
async getCompletion(params: CopilotLspCompletionParams) {
|
||||
const response = await this.request('getCompletions', params)
|
||||
const response = await this.request('copilot/getCompletions', params)
|
||||
//
|
||||
this.queuedUids = [...response.completions.map((c) => c.uuid)]
|
||||
return response
|
||||
@ -224,22 +254,34 @@ export class LanguageServerClient {
|
||||
async accept(uuid: string) {
|
||||
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
||||
this.queuedUids = []
|
||||
await this.acceptCompletion({ uuid })
|
||||
await this.rejectCompletions({ uuids: badUids })
|
||||
this.acceptCompletion({ uuid })
|
||||
this.rejectCompletions({ uuids: badUids })
|
||||
}
|
||||
|
||||
async reject() {
|
||||
const badUids = this.queuedUids
|
||||
this.queuedUids = []
|
||||
return await this.rejectCompletions({ uuids: badUids })
|
||||
this.rejectCompletions({ uuids: badUids })
|
||||
}
|
||||
|
||||
async acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||
return await this.request('notifyAccepted', params)
|
||||
acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||
this.notify('copilot/notifyAccepted', params)
|
||||
}
|
||||
|
||||
async rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||
return await this.request('notifyRejected', params)
|
||||
rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||
this.notify('copilot/notifyRejected', params)
|
||||
}
|
||||
|
||||
async updateUnits(
|
||||
params: UpdateUnitsParams
|
||||
): Promise<UpdateUnitsResponse | null> {
|
||||
return await this.request('kcl/updateUnits', params)
|
||||
}
|
||||
|
||||
async updateCanExecute(
|
||||
params: UpdateCanExecuteParams
|
||||
): Promise<UpdateCanExecuteResponse> {
|
||||
return await this.request('kcl/updateCanExecute', params)
|
||||
}
|
||||
|
||||
private processNotifications(notification: LSP.NotificationMessage) {
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { autocompletion } from '@codemirror/autocomplete'
|
||||
import { Extension } from '@codemirror/state'
|
||||
import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
|
||||
import { Extension, EditorState, Prec } from '@codemirror/state'
|
||||
import {
|
||||
ViewPlugin,
|
||||
hoverTooltip,
|
||||
EditorView,
|
||||
keymap,
|
||||
KeyBinding,
|
||||
tooltips,
|
||||
} from '@codemirror/view'
|
||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||
import { offsetToPos } from 'editor/plugins/lsp/util'
|
||||
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import { syntaxTree, indentService, foldService } from '@codemirror/language'
|
||||
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
|
||||
import {
|
||||
LanguageServerPlugin,
|
||||
documentUri,
|
||||
@ -12,21 +20,71 @@ import {
|
||||
workspaceFolders,
|
||||
} from 'editor/plugins/lsp/plugin'
|
||||
|
||||
export const kclIndentService = () => {
|
||||
// Match the indentation of the previous line (if present).
|
||||
return indentService.of((context, pos) => {
|
||||
try {
|
||||
const previousLine = context.lineAt(pos, -1)
|
||||
const previousLineText = previousLine.text.replaceAll(
|
||||
'\t',
|
||||
' '.repeat(context.state.tabSize)
|
||||
)
|
||||
const match = previousLineText.match(/^(\s)*/)
|
||||
if (match === null || match.length <= 0) return null
|
||||
return match[0].length
|
||||
} catch (err) {
|
||||
console.error('Error in codemirror indentService', err)
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||
let plugin: LanguageServerPlugin | null = null
|
||||
const viewPlugin = ViewPlugin.define(
|
||||
(view) =>
|
||||
(plugin = new LanguageServerPlugin(
|
||||
options.client,
|
||||
view,
|
||||
options.allowHTMLContent
|
||||
))
|
||||
)
|
||||
|
||||
const kclKeymap: readonly KeyBinding[] = [
|
||||
{
|
||||
key: 'Alt-Shift-f',
|
||||
run: (view: EditorView) => {
|
||||
if (view.plugin === null) return false
|
||||
|
||||
// Get the current plugin from the map.
|
||||
const p = view.plugin(viewPlugin)
|
||||
|
||||
if (p === null) return false
|
||||
p.requestFormatting()
|
||||
return true
|
||||
},
|
||||
},
|
||||
]
|
||||
// Create an extension for the key mappings.
|
||||
const kclKeymapExt = Prec.highest(keymap.computeN([], () => [kclKeymap]))
|
||||
|
||||
const folding = foldService.of(
|
||||
(state: EditorState, lineStart: number, lineEnd: number) => {
|
||||
if (plugin == null) return null
|
||||
|
||||
// Get the folding ranges from the language server.
|
||||
// Since this is async we directly need to update the folding ranges after.
|
||||
return plugin?.foldingRange(lineStart, lineEnd)
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
documentUri.of(options.documentUri),
|
||||
languageId.of('kcl'),
|
||||
workspaceFolders.of(options.workspaceFolders),
|
||||
ViewPlugin.define(
|
||||
(view) =>
|
||||
(plugin = new LanguageServerPlugin(
|
||||
options.client,
|
||||
view,
|
||||
options.allowHTMLContent
|
||||
))
|
||||
),
|
||||
viewPlugin,
|
||||
kclKeymapExt,
|
||||
kclIndentService(),
|
||||
hoverTooltip(
|
||||
(view, pos) =>
|
||||
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
||||
@ -35,7 +93,19 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||
tooltips({
|
||||
position: 'absolute',
|
||||
}),
|
||||
linter((view) => {
|
||||
let diagnostics: Diagnostic[] = []
|
||||
forEachDiagnostic(
|
||||
view.state,
|
||||
(d: Diagnostic, from: number, to: number) => {
|
||||
diagnostics.push(d)
|
||||
}
|
||||
)
|
||||
return diagnostics
|
||||
}),
|
||||
folding,
|
||||
autocompletion({
|
||||
defaultKeymap: true,
|
||||
override: [
|
||||
async (context) => {
|
||||
if (plugin == null) return null
|
||||
|
@ -19,37 +19,43 @@ export interface LanguageOptions {
|
||||
client: LanguageServerClient
|
||||
}
|
||||
|
||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||
// For now let's use the javascript parser.
|
||||
// It works really well and has good syntax highlighting.
|
||||
// We can use our lsp for the rest.
|
||||
const lang = new Language(
|
||||
data,
|
||||
jsParser,
|
||||
[
|
||||
EditorState.languageData.of(() => [
|
||||
{
|
||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
||||
commentTokens: {
|
||||
line: '//',
|
||||
block: {
|
||||
open: '/*',
|
||||
close: '*/',
|
||||
class KclLanguage extends Language {
|
||||
constructor(options: LanguageOptions) {
|
||||
const plugin = kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
})
|
||||
|
||||
super(
|
||||
data,
|
||||
// For now let's use the javascript parser.
|
||||
// It works really well and has good syntax highlighting.
|
||||
// We can use our lsp for the rest.
|
||||
jsParser,
|
||||
[
|
||||
plugin,
|
||||
EditorState.languageData.of(() => [
|
||||
{
|
||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
||||
commentTokens: {
|
||||
line: '//',
|
||||
block: {
|
||||
open: '/*',
|
||||
close: '*/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
'kcl'
|
||||
)
|
||||
|
||||
// Create our supporting extension.
|
||||
const kclLsp = kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
})
|
||||
|
||||
return new LanguageSupport(lang, [kclLsp])
|
||||
]),
|
||||
],
|
||||
'kcl'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||
const lang = new KclLanguage(options)
|
||||
|
||||
return new LanguageSupport(lang)
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { completeFromList } from '@codemirror/autocomplete'
|
||||
import {
|
||||
completeFromList,
|
||||
hasNextSnippetField,
|
||||
snippetCompletion,
|
||||
} from '@codemirror/autocomplete'
|
||||
import { setDiagnostics } from '@codemirror/lint'
|
||||
import { Facet } from '@codemirror/state'
|
||||
import { EditorView, Tooltip } from '@codemirror/view'
|
||||
@ -7,7 +11,6 @@ import {
|
||||
CompletionItemKind,
|
||||
CompletionTriggerKind,
|
||||
} from 'vscode-languageserver-protocol'
|
||||
import debounce from 'debounce-promise'
|
||||
|
||||
import type {
|
||||
Completion,
|
||||
@ -20,6 +23,12 @@ import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
||||
import { Program, ProgramMemory } from 'lang/wasm'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
||||
import { lspDiagnosticsToKclErrors } from 'lang/errors'
|
||||
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
||||
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
||||
|
||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||
export const documentUri = Facet.define<string, string>({ combine: useLast })
|
||||
@ -29,8 +38,6 @@ export const workspaceFolders = Facet.define<
|
||||
LSP.WorkspaceFolder[]
|
||||
>({ combine: useLast })
|
||||
|
||||
const changesDelay = 500
|
||||
|
||||
const CompletionItemKindMap = Object.fromEntries(
|
||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||
) as Record<CompletionItemKind, string>
|
||||
@ -41,6 +48,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
public languageId: string
|
||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
||||
private documentVersion: number
|
||||
private foldingRanges: LSP.FoldingRange[] | null = null
|
||||
|
||||
constructor(
|
||||
client: LanguageServerClient,
|
||||
@ -60,9 +68,19 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
})
|
||||
}
|
||||
|
||||
update({ docChanged }: ViewUpdate) {
|
||||
update({ docChanged, state }: ViewUpdate) {
|
||||
if (!docChanged) return
|
||||
|
||||
// If we are just fucking around in a snippet, return early and don't
|
||||
// trigger stuff below that might cause the component to re-render.
|
||||
// Otherwise we will not be able to tab thru the snippet portions.
|
||||
// We explicitly dont check HasPrevSnippetField because we always add
|
||||
// a ${} to the end of the function so that's fine.
|
||||
// We only care about this for the 'kcl' plugin.
|
||||
if (this.client.name === 'kcl' && hasNextSnippetField(state)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sendChange({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
})
|
||||
@ -101,19 +119,13 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
|
||||
try {
|
||||
debounce(
|
||||
() => {
|
||||
return this.client.textDocumentDidChange({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
version: this.documentVersion++,
|
||||
},
|
||||
contentChanges: [{ text: documentText }],
|
||||
})
|
||||
this.client.textDocumentDidChange({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
version: this.documentVersion++,
|
||||
},
|
||||
changesDelay,
|
||||
{ leading: true }
|
||||
)
|
||||
contentChanges: [{ text: documentText }],
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@ -154,6 +166,126 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
||||
}
|
||||
|
||||
async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
!this.client.getServerCapabilities().foldingRangeProvider
|
||||
)
|
||||
return null
|
||||
const result = await this.client.textDocumentFoldingRange({
|
||||
textDocument: { uri: this.documentUri },
|
||||
})
|
||||
|
||||
return result || null
|
||||
}
|
||||
|
||||
async updateFoldingRanges() {
|
||||
const foldingRanges = await this.getFoldingRanges()
|
||||
if (foldingRanges === null) return
|
||||
// Update the folding ranges.
|
||||
this.foldingRanges = foldingRanges
|
||||
}
|
||||
|
||||
// In the future if codemirrors foldService accepts async folding ranges
|
||||
// then we will not have to store these and we can call getFoldingRanges
|
||||
// here.
|
||||
foldingRange(
|
||||
lineStart: number,
|
||||
lineEnd: number
|
||||
): { from: number; to: number } | null {
|
||||
if (this.foldingRanges === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.foldingRanges.length; i++) {
|
||||
const { startLine, endLine } = this.foldingRanges[i]
|
||||
if (startLine === lineEnd) {
|
||||
const range = {
|
||||
// Set the fold start to the end of the first line
|
||||
// With this, the fold will not include the first line
|
||||
from: startLine,
|
||||
to: endLine,
|
||||
}
|
||||
|
||||
return range
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async updateUnits(units: UnitLength): Promise<UpdateUnitsResponse | null> {
|
||||
if (this.client.name !== 'kcl') return null
|
||||
if (!this.client.ready) return null
|
||||
|
||||
return await this.client.updateUnits({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
},
|
||||
text: this.view.state.doc.toString(),
|
||||
units,
|
||||
})
|
||||
}
|
||||
async updateCanExecute(
|
||||
canExecute: boolean
|
||||
): Promise<UpdateCanExecuteResponse | null> {
|
||||
if (this.client.name !== 'kcl') return null
|
||||
if (!this.client.ready) return null
|
||||
|
||||
let response = await this.client.updateCanExecute({
|
||||
canExecute,
|
||||
})
|
||||
|
||||
if (!canExecute && response.isExecuting) {
|
||||
// We want to wait until the server is not busy before we reply to the
|
||||
// caller.
|
||||
while (response.isExecuting) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
response = await this.client.updateCanExecute({
|
||||
canExecute,
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log('[lsp] kcl: updated canExecute', canExecute, response)
|
||||
return response
|
||||
}
|
||||
|
||||
async requestFormatting() {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
!this.client.getServerCapabilities().documentFormattingProvider
|
||||
)
|
||||
return null
|
||||
|
||||
this.sendChange({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
})
|
||||
|
||||
const result = await this.client.textDocumentFormatting({
|
||||
textDocument: { uri: this.documentUri },
|
||||
options: {
|
||||
tabSize: 2,
|
||||
insertSpaces: true,
|
||||
insertFinalNewline: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const { range, newText } = result[i]
|
||||
this.view.dispatch({
|
||||
changes: [
|
||||
{
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
insert: newText,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async requestCompletion(
|
||||
context: CompletionContext,
|
||||
{ line, character }: { line: number; character: number },
|
||||
@ -224,6 +356,10 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
}
|
||||
|
||||
if (insertText && insertTextFormat === 2) {
|
||||
return snippetCompletion(insertText, completion)
|
||||
}
|
||||
|
||||
return completion
|
||||
}
|
||||
)
|
||||
@ -235,8 +371,12 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
case 'textDocument/publishDiagnostics':
|
||||
this.processDiagnostics(
|
||||
notification.params as PublishDiagnosticsParams
|
||||
const params = notification.params as PublishDiagnosticsParams
|
||||
this.processDiagnostics(params)
|
||||
// Update the kcl errors pane.
|
||||
kclManager.kclErrors = lspDiagnosticsToKclErrors(
|
||||
this.view.state.doc,
|
||||
params.diagnostics
|
||||
)
|
||||
break
|
||||
case 'window/logMessage':
|
||||
@ -253,6 +393,23 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
notification.params
|
||||
)
|
||||
break
|
||||
case 'kcl/astUpdated':
|
||||
// The server has updated the AST, we should update elsewhere.
|
||||
let updatedAst = notification.params as Program
|
||||
console.log('[lsp]: Updated AST', updatedAst)
|
||||
kclManager.ast = updatedAst
|
||||
|
||||
// Update the folding ranges, since the AST has changed.
|
||||
// This is a hack since codemirror does not support async foldService.
|
||||
// When they do we can delete this.
|
||||
this.updateFoldingRanges()
|
||||
break
|
||||
case 'kcl/memoryUpdated':
|
||||
// The server has updated the memory, we should update elsewhere.
|
||||
let updatedMemory = notification.params as ProgramMemory
|
||||
console.log('[lsp]: Updated Memory', updatedMemory)
|
||||
kclManager.programMemory = updatedMemory
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -2,6 +2,7 @@ import { InitOutput, ServerConfig } from 'wasm-lib/pkg/wasm_lib'
|
||||
import { FromServer, IntoServer } from './codec'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import { copilotLspRun, initPromise, kclLspRun } from 'lang/wasm'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
|
||||
export default class Server {
|
||||
readonly initOutput: InitOutput
|
||||
@ -41,7 +42,7 @@ export default class Server {
|
||||
if (type_ === 'copilot') {
|
||||
await copilotLspRun(config, token)
|
||||
} else if (type_ === 'kcl') {
|
||||
await kclLspRun(config, token || '')
|
||||
await kclLspRun(config, engineCommandManager, token || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
src/hooks/useResolvedTheme.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
|
||||
/**
|
||||
* Resolves the current theme based on the theme setting
|
||||
* and the system theme if needed.
|
||||
* @returns {Themes.Light | Themes.Dark}
|
||||
*/
|
||||
export function useResolvedTheme() {
|
||||
const {
|
||||
settings: { context },
|
||||
} = useSettingsAuthContext()
|
||||
return context.app.theme.current === Themes.System
|
||||
? getSystemTheme()
|
||||
: context.app.theme.current
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { useLayoutEffect, useEffect, useRef } from 'react'
|
||||
import { parse } from '../lang/wasm'
|
||||
import { useStore } from '../useStore'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { makeDefaultPlanes } from 'lang/wasm'
|
||||
|
||||
export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
@ -40,12 +40,11 @@ export function useSetupEngineManager(
|
||||
setIsStreamReady,
|
||||
width: quadWidth,
|
||||
height: quadHeight,
|
||||
executeCode: (code?: string) => {
|
||||
const _ast = parse(code || kclManager.code)
|
||||
return kclManager.executeAst(_ast, true)
|
||||
},
|
||||
token,
|
||||
theme,
|
||||
makeDefaultPlanes: () => {
|
||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||
},
|
||||
})
|
||||
setStreamDimensions({
|
||||
streamWidth: quadWidth,
|
||||
|
@ -46,6 +46,15 @@ select {
|
||||
@apply bg-chalkboard-90;
|
||||
}
|
||||
|
||||
/* We hide the cursor if the user has turned off the textEditor.blinkingCursor setting
|
||||
* any elements that could present a blinking cursor to the user
|
||||
*/
|
||||
input,
|
||||
textarea,
|
||||
*[contenteditable] {
|
||||
caret-color: var(--cursor-color, auto);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2 h-2 rounded-sm;
|
||||
@apply bg-chalkboard-20;
|
||||
@ -113,32 +122,32 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.full-height-subtract {
|
||||
--height-subtract: 2.25rem;
|
||||
height: 100%;
|
||||
max-height: calc(100% - var(--height-subtract));
|
||||
}
|
||||
|
||||
/*
|
||||
* The first descendent of the CodeMirror wrapper is the theme,
|
||||
* but its identifying class can change depending on the theme.
|
||||
*/
|
||||
#code-mirror-override > div,
|
||||
#code-mirror-override .cm-editor {
|
||||
@apply h-full bg-transparent;
|
||||
@apply bg-transparent h-full;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-scroller {
|
||||
@apply h-full;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-scroller::-webkit-scrollbar {
|
||||
@apply h-0;
|
||||
#code-mirror-override .cm-gutter {
|
||||
@apply select-none;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-activeLine,
|
||||
#code-mirror-override .cm-activeLineGutter {
|
||||
@apply bg-liquid-10/50;
|
||||
@apply bg-primary/10;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-activeLine,
|
||||
.dark #code-mirror-override .cm-activeLineGutter {
|
||||
@apply bg-liquid-80/50;
|
||||
@apply bg-primary/20;
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-gutters {
|
||||
@ -149,19 +158,29 @@ code {
|
||||
@apply bg-chalkboard-110/50;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-content {
|
||||
@apply caret-primary;
|
||||
}
|
||||
.dark #code-mirror-override .cm-content {
|
||||
@apply caret-chalkboard-10;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-focused .cm-cursor {
|
||||
width: 0px;
|
||||
}
|
||||
#code-mirror-override .cm-cursor {
|
||||
display: block;
|
||||
width: 1ch;
|
||||
@apply bg-liquid-40 mix-blend-multiply;
|
||||
|
||||
animation: blink 2s ease-out infinite;
|
||||
@apply mix-blend-multiply;
|
||||
@apply border-l-primary;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-cursor {
|
||||
@apply bg-liquid-50;
|
||||
@apply border-l-chalkboard-10;
|
||||
}
|
||||
|
||||
#code-mirror-override.blink .cm-cursor {
|
||||
animation: blink 1200ms ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
@ -169,8 +188,8 @@ code {
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
15% {
|
||||
opacity: 0.75;
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { executeAst, executeCode } from 'useStore'
|
||||
import { executeAst } from 'useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { KCLError } from './errors'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { EngineCommandManager } from './std/engineConnection'
|
||||
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import {
|
||||
CallExpression,
|
||||
initPromise,
|
||||
@ -47,19 +46,6 @@ export class KclManager {
|
||||
private _params: Params<string> = {}
|
||||
|
||||
engineCommandManager: EngineCommandManager
|
||||
private _defferer = deferExecution((code: string) => {
|
||||
const ast = this.safeParse(code)
|
||||
if (!ast) return
|
||||
try {
|
||||
const fmtAndStringify = (ast: Program) =>
|
||||
JSON.stringify(parse(recast(ast)))
|
||||
const isAstTheSame = fmtAndStringify(ast) === fmtAndStringify(this._ast)
|
||||
if (isAstTheSame) return
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this.executeAst(ast)
|
||||
}, 600)
|
||||
|
||||
private _isExecutingCallback: (arg: boolean) => void = () => {}
|
||||
private _codeCallBack: (arg: string) => void = () => {}
|
||||
@ -81,6 +67,9 @@ export class KclManager {
|
||||
get code() {
|
||||
return this._code
|
||||
}
|
||||
// Calling set code will update the code in the codemirror editor.
|
||||
// That will then trigger the lsp to update the ast and execute the code.
|
||||
// DO NOT ALSO CALL execute/parseCode here, as that will cause the code to be executed twice.
|
||||
set code(code) {
|
||||
this._code = code
|
||||
this._codeCallBack(code)
|
||||
@ -106,6 +95,12 @@ export class KclManager {
|
||||
set programMemory(programMemory) {
|
||||
this._programMemory = programMemory
|
||||
this._programMemoryCallBack(programMemory)
|
||||
// We only do this to help the playwright testes otherwise I would remove
|
||||
// it. (@jessfraz wrote this comment)
|
||||
this.engineCommandManager.addCommandLog({
|
||||
type: 'execution-done',
|
||||
data: null,
|
||||
})
|
||||
}
|
||||
|
||||
get logs() {
|
||||
@ -208,7 +203,6 @@ export class KclManager {
|
||||
console.error('error parsing code', e)
|
||||
if (e instanceof KCLError) {
|
||||
this.kclErrors = [e]
|
||||
if (e.msg === 'file is empty') this.engineCommandManager?.endSession()
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -220,8 +214,9 @@ export class KclManager {
|
||||
if (this.wasmInitFailed) {
|
||||
this.wasmInitFailed = false
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.wasmInitFailed = true
|
||||
throw new Error('error initializing wasm: ' + e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,7 +236,7 @@ export class KclManager {
|
||||
ast,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
})
|
||||
enterEditMode(programMemory, this.engineCommandManager)
|
||||
await enterEditMode(programMemory, this.engineCommandManager)
|
||||
this.isExecuting = false
|
||||
// Check the cancellation token for this execution before applying side effects
|
||||
if (this._cancelTokens.get(currentExecutionId)) {
|
||||
@ -276,7 +271,7 @@ export class KclManager {
|
||||
if (!newAst) return
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
if (updates !== 'none') {
|
||||
this.setCode(recast(ast))
|
||||
this.setCode(newCode)
|
||||
}
|
||||
this._ast = { ...newAst }
|
||||
|
||||
@ -308,41 +303,14 @@ export class KclManager {
|
||||
}
|
||||
)
|
||||
}
|
||||
executeCode = async (code?: string, executionId?: number) => {
|
||||
const currentExecutionId = executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
if (this._cancelTokens.get(currentExecutionId)) {
|
||||
this._cancelTokens.delete(currentExecutionId)
|
||||
return
|
||||
}
|
||||
await this.ensureWasmInit()
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
const result = await executeCode({
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
code: code || this._code,
|
||||
lastAst: this._ast,
|
||||
force: false,
|
||||
})
|
||||
// Check the cancellation token for this execution before applying side effects
|
||||
if (this._cancelTokens.get(currentExecutionId)) {
|
||||
this._cancelTokens.delete(currentExecutionId)
|
||||
return
|
||||
}
|
||||
if (!result.isChange) return
|
||||
const { logs, errors, programMemory, ast } = result
|
||||
enterEditMode(programMemory, this.engineCommandManager)
|
||||
this.logs = logs
|
||||
this.kclErrors = errors
|
||||
this.programMemory = programMemory
|
||||
this.ast = ast
|
||||
if (code) this.code = code
|
||||
this._cancelTokens.delete(currentExecutionId)
|
||||
}
|
||||
cancelAllExecutions() {
|
||||
this._cancelTokens.forEach((_, key) => {
|
||||
this._cancelTokens.set(key, true)
|
||||
})
|
||||
}
|
||||
// Calling set code will update the code in the codemirror editor.
|
||||
// That will then trigger the lsp to update the ast and execute the code.
|
||||
// DO NOT ALSO CALL execute/parseCode here, as that will cause the code to be executed twice.
|
||||
setCode(code: string, shouldWriteFile = true) {
|
||||
if (shouldWriteFile) {
|
||||
// use the normal code setter
|
||||
@ -352,27 +320,6 @@ export class KclManager {
|
||||
this._code = code
|
||||
this._codeCallBack(code)
|
||||
}
|
||||
setCodeAndExecute(code: string, shouldWriteFile = true) {
|
||||
this.setCode(code, shouldWriteFile)
|
||||
if (code.trim()) {
|
||||
this._defferer(code)
|
||||
return
|
||||
}
|
||||
this._ast = {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: [],
|
||||
},
|
||||
}
|
||||
this._programMemory = {
|
||||
root: {},
|
||||
return: null,
|
||||
}
|
||||
this.engineCommandManager.endSession()
|
||||
}
|
||||
format() {
|
||||
const ast = this.safeParse(this.code)
|
||||
if (!ast) return
|
||||
@ -440,6 +387,10 @@ export class KclManager {
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
}
|
||||
|
||||
async enterEditMode() {
|
||||
await enterEditMode(this.programMemory, this.engineCommandManager)
|
||||
}
|
||||
}
|
||||
|
||||
function safeLSGetItem(key: string) {
|
||||
@ -452,7 +403,7 @@ function safteLSSetItem(key: string, value: string) {
|
||||
localStorage?.setItem(key, value)
|
||||
}
|
||||
|
||||
function enterEditMode(
|
||||
async function enterEditMode(
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager
|
||||
) {
|
||||
@ -460,7 +411,7 @@ function enterEditMode(
|
||||
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
|
||||
) as SketchGroup | ExtrudeGroup
|
||||
firstSketchOrExtrudeGroup &&
|
||||
engineCommandManager.sendSceneCommand({
|
||||
(await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_batch_req',
|
||||
batch_id: uuidv4(),
|
||||
requests: [
|
||||
@ -479,5 +430,5 @@ function enterEditMode(
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { kclErrToDiagnostic, KCLError } from './errors'
|
||||
import { kclErrorsToDiagnostics, KCLError } from './errors'
|
||||
|
||||
describe('test kclErrToDiagnostic', () => {
|
||||
it('converts KCL errors to CodeMirror diagnostics', () => {
|
||||
@ -20,7 +20,7 @@ describe('test kclErrToDiagnostic', () => {
|
||||
],
|
||||
},
|
||||
]
|
||||
const diagnostics = kclErrToDiagnostic(errors)
|
||||
const diagnostics = kclErrorsToDiagnostics(errors)
|
||||
expect(diagnostics).toEqual([
|
||||
{
|
||||
from: 0,
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
||||
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
|
||||
import { Text } from '@codemirror/state'
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
export class KCLError {
|
||||
@ -81,11 +84,47 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the lsp diagnostic to an array of KclErrors.
|
||||
* Currently the diagnostics are all errors, but in the future they could include lints.
|
||||
* */
|
||||
export function lspDiagnosticsToKclErrors(
|
||||
doc: Text,
|
||||
diagnostics: LspDiagnostic[]
|
||||
): KCLError[] {
|
||||
return diagnostics
|
||||
.flatMap(
|
||||
({ range, message }) =>
|
||||
new KCLError('unexpected', message, [
|
||||
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!],
|
||||
])
|
||||
)
|
||||
.filter(({ sourceRanges }) => {
|
||||
const [from, to] = sourceRanges[0]
|
||||
return (
|
||||
from !== null && to !== null && from !== undefined && to !== undefined
|
||||
)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const c = a.sourceRanges[0][0]
|
||||
const d = b.sourceRanges[0][0]
|
||||
switch (true) {
|
||||
case c < d:
|
||||
return -1
|
||||
case c > d:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the KCL errors to an array of CodeMirror diagnostics.
|
||||
* Currently the diagnostics are all errors, but in the future they could include lints.
|
||||
* */
|
||||
export function kclErrToDiagnostic(errors: KCLError[]): Diagnostic[] {
|
||||
export function kclErrorsToDiagnostics(
|
||||
errors: KCLError[]
|
||||
): CodeMirrorDiagnostic[] {
|
||||
return errors?.flatMap((err) => {
|
||||
return err.sourceRanges.map(([from, to]) => {
|
||||
return { from, to, message: err.msg, severity: 'error' }
|
||||
|
@ -241,7 +241,6 @@ export function extrudeSketch(
|
||||
pathToExtrudeArg: PathToNode
|
||||
} {
|
||||
const _node = { ...node }
|
||||
const dumbyStartend = { start: 0, end: 0 }
|
||||
const { node: sketchExpression } = getNodeFromPath(
|
||||
_node,
|
||||
pathToNode,
|
||||
@ -256,18 +255,14 @@ export function extrudeSketch(
|
||||
)
|
||||
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
||||
|
||||
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
||||
const { node: variableDeclarator, shallowPath: pathToDecleration } =
|
||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||
|
||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||
distance,
|
||||
shouldPipe
|
||||
? createPipeSubstitution()
|
||||
: {
|
||||
type: 'Identifier',
|
||||
...dumbyStartend,
|
||||
name: variableDeclorator.id.name,
|
||||
},
|
||||
: createIdentifier(variableDeclarator.id.name),
|
||||
])
|
||||
|
||||
if (shouldPipe) {
|
||||
@ -277,7 +272,7 @@ export function extrudeSketch(
|
||||
: [sketchExpression as any, extrudeCall]
|
||||
)
|
||||
|
||||
variableDeclorator.init = pipeChain
|
||||
variableDeclarator.init = pipeChain
|
||||
const pathToExtrudeArg: PathToNode = [
|
||||
...pathToDecleration,
|
||||
['init', 'VariableDeclarator'],
|
||||
|
@ -5,6 +5,7 @@ import { exportSave } from 'lib/exportSave'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { Themes, getThemeColorForEngine } from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
let lastMessage = ''
|
||||
|
||||
@ -25,7 +26,8 @@ interface CommandInfo {
|
||||
}
|
||||
}
|
||||
|
||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
|
||||
interface ResultCommand extends CommandInfo {
|
||||
type: 'result'
|
||||
@ -37,10 +39,19 @@ interface FailedCommand extends CommandInfo {
|
||||
type: 'failed'
|
||||
errors: Models['FailureWebSocketResponse_type']['errors']
|
||||
}
|
||||
interface ResolveCommand {
|
||||
id: string
|
||||
commandType: CommandTypes
|
||||
range: SourceRange
|
||||
// We ALWAYS need the raw response because we pass it back to the rust side.
|
||||
raw: WebSocketResponse
|
||||
data?: Models['OkModelingCmdResponse_type']
|
||||
errors?: Models['FailureWebSocketResponse_type']['errors']
|
||||
}
|
||||
interface PendingCommand extends CommandInfo {
|
||||
type: 'pending'
|
||||
promise: Promise<any>
|
||||
resolve: (val: any) => void
|
||||
resolve: (val: ResolveCommand) => void
|
||||
}
|
||||
|
||||
export interface ArtifactMap {
|
||||
@ -59,6 +70,15 @@ type Timeout = ReturnType<typeof setTimeout>
|
||||
|
||||
type ClientMetrics = Models['ClientMetrics_type']
|
||||
|
||||
interface WebRTCClientMetrics extends ClientMetrics {
|
||||
rtc_frame_height: number
|
||||
rtc_frame_width: number
|
||||
rtc_packets_lost: number
|
||||
rtc_pli_count: number
|
||||
rtc_pause_count: number
|
||||
rtc_total_pauses_duration_sec: number
|
||||
}
|
||||
|
||||
type Value<T, U> = U extends undefined
|
||||
? { type: T; value: U }
|
||||
: U extends void
|
||||
@ -224,7 +244,7 @@ class EngineConnection {
|
||||
private onNewTrack: (track: NewTrackArgs) => void
|
||||
|
||||
// TODO: actual type is ClientMetrics
|
||||
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
||||
public webrtcStatsCollector?: () => Promise<WebRTCClientMetrics>
|
||||
private engineCommandManager: EngineCommandManager
|
||||
|
||||
constructor({
|
||||
@ -396,7 +416,7 @@ class EngineConnection {
|
||||
},
|
||||
}
|
||||
|
||||
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||
this.webrtcStatsCollector = (): Promise<WebRTCClientMetrics> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (mediaStream.getVideoTracks().length !== 1) {
|
||||
reject(new Error('too many video tracks to report'))
|
||||
@ -405,7 +425,7 @@ class EngineConnection {
|
||||
|
||||
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||
let client_metrics: ClientMetrics = {
|
||||
let client_metrics: WebRTCClientMetrics = {
|
||||
rtc_frames_decoded: 0,
|
||||
rtc_frames_dropped: 0,
|
||||
rtc_frames_received: 0,
|
||||
@ -414,6 +434,12 @@ class EngineConnection {
|
||||
rtc_jitter_sec: 0.0,
|
||||
rtc_keyframes_decoded: 0,
|
||||
rtc_total_freezes_duration_sec: 0.0,
|
||||
rtc_frame_height: 0,
|
||||
rtc_frame_width: 0,
|
||||
rtc_packets_lost: 0,
|
||||
rtc_pli_count: 0,
|
||||
rtc_pause_count: 0,
|
||||
rtc_total_pauses_duration_sec: 0.0,
|
||||
}
|
||||
|
||||
// TODO(paultag): Since we can technically have multiple WebRTC
|
||||
@ -439,6 +465,13 @@ class EngineConnection {
|
||||
videoTrackReport.keyFramesDecoded || 0
|
||||
client_metrics.rtc_total_freezes_duration_sec =
|
||||
videoTrackReport.totalFreezesDuration || 0
|
||||
client_metrics.rtc_frame_height =
|
||||
videoTrackReport.frameHeight || 0
|
||||
client_metrics.rtc_frame_width =
|
||||
videoTrackReport.frameWidth || 0
|
||||
client_metrics.rtc_packets_lost =
|
||||
videoTrackReport.packetsLost || 0
|
||||
client_metrics.rtc_pli_count = videoTrackReport.pliCount || 0
|
||||
} else if (videoTrackReport.type === 'transport') {
|
||||
// videoTrackReport.bytesReceived,
|
||||
// videoTrackReport.bytesSent,
|
||||
@ -784,7 +817,6 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
this.webrtcStatsCollector = undefined
|
||||
}
|
||||
finalizeIfAllConnectionsClosed() {
|
||||
console.log(this.websocket, this.pc, this.unreliableDataChannel)
|
||||
const allClosed =
|
||||
this.websocket?.readyState === 3 &&
|
||||
this.pc?.connectionState === 'closed' &&
|
||||
@ -827,7 +859,7 @@ export type CommandLog =
|
||||
}
|
||||
| {
|
||||
type: 'receive-reliable'
|
||||
data: WebSocketResponse
|
||||
data: OkWebSocketResponseData
|
||||
id: string
|
||||
cmd_type?: string
|
||||
}
|
||||
@ -843,8 +875,8 @@ export class EngineCommandManager {
|
||||
outSequence = 1
|
||||
inSequence = 1
|
||||
engineConnection?: EngineConnection
|
||||
defaultPlanes: { xy: string; yz: string; xz: string } | null = null
|
||||
_commandLogs: CommandLog[] = []
|
||||
defaultPlanes: DefaultPlanes | null = null
|
||||
commandLogs: CommandLog[] = []
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
// Folks should realize that wait for ready does not get called _everytime_
|
||||
// the connection resets and restarts, it only gets called the first time.
|
||||
@ -882,24 +914,26 @@ export class EngineCommandManager {
|
||||
set getAstCb(cb: () => Program) {
|
||||
this.getAst = cb
|
||||
}
|
||||
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||
|
||||
start({
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width,
|
||||
height,
|
||||
executeCode,
|
||||
token,
|
||||
makeDefaultPlanes,
|
||||
theme = Themes.Dark,
|
||||
}: {
|
||||
setMediaStream: (stream: MediaStream) => void
|
||||
setIsStreamReady: (isStreamReady: boolean) => void
|
||||
width: number
|
||||
height: number
|
||||
executeCode: (code?: string, force?: boolean) => void
|
||||
token?: string
|
||||
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||
theme?: Themes
|
||||
}) {
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
if (width === 0 || height === 0) {
|
||||
return
|
||||
}
|
||||
@ -969,7 +1003,6 @@ export class EngineCommandManager {
|
||||
this.initPlanes().then(() => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
executeCode(undefined, true)
|
||||
})
|
||||
},
|
||||
onClose: () => {
|
||||
@ -1020,7 +1053,11 @@ export class EngineCommandManager {
|
||||
message.resp.type === 'modeling' &&
|
||||
message.request_id
|
||||
) {
|
||||
this.handleModelingCommand(message.resp, message.request_id)
|
||||
this.handleModelingCommand(
|
||||
message.resp,
|
||||
message.request_id,
|
||||
message
|
||||
)
|
||||
} else if (
|
||||
!message.success &&
|
||||
message.request_id &&
|
||||
@ -1069,7 +1106,11 @@ export class EngineCommandManager {
|
||||
}
|
||||
this.engineConnection?.send(resizeCmd)
|
||||
}
|
||||
handleModelingCommand(message: WebSocketResponse, id: string) {
|
||||
handleModelingCommand(
|
||||
message: OkWebSocketResponseData,
|
||||
id: string,
|
||||
raw: WebSocketResponse
|
||||
) {
|
||||
if (message.type !== 'modeling') {
|
||||
return
|
||||
}
|
||||
@ -1081,7 +1122,7 @@ export class EngineCommandManager {
|
||||
command?.additionalData?.type === 'batch-ids'
|
||||
) {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleModelingCommand(message, id)
|
||||
this.handleModelingCommand(message, id, raw)
|
||||
})
|
||||
// batch artifact is just a container, we don't need to keep it
|
||||
// once we process all the commands inside it
|
||||
@ -1092,7 +1133,7 @@ export class EngineCommandManager {
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
raw,
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -1116,7 +1157,7 @@ export class EngineCommandManager {
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
raw,
|
||||
} as const
|
||||
this.artifactMap[id] = artifact
|
||||
if (
|
||||
@ -1161,7 +1202,7 @@ export class EngineCommandManager {
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
raw,
|
||||
})
|
||||
} else if (sceneCommand && sceneCommand.type === 'pending') {
|
||||
const resolve = sceneCommand.resolve
|
||||
@ -1172,7 +1213,7 @@ export class EngineCommandManager {
|
||||
commandType: sceneCommand.commandType,
|
||||
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
raw,
|
||||
} as const
|
||||
this.sceneCommandArtifacts[id] = artifact
|
||||
resolve({
|
||||
@ -1180,6 +1221,7 @@ export class EngineCommandManager {
|
||||
commandType: sceneCommand.commandType,
|
||||
range: sceneCommand.range,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
})
|
||||
} else if (command) {
|
||||
this.artifactMap[id] = {
|
||||
@ -1188,7 +1230,7 @@ export class EngineCommandManager {
|
||||
range: command?.range,
|
||||
pathToNode: command?.pathToNode,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
raw,
|
||||
}
|
||||
} else {
|
||||
this.sceneCommandArtifacts[id] = {
|
||||
@ -1197,15 +1239,14 @@ export class EngineCommandManager {
|
||||
range: sceneCommand?.range,
|
||||
pathToNode: sceneCommand?.pathToNode,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
}
|
||||
handleFailedModelingCommand({
|
||||
request_id,
|
||||
errors,
|
||||
}: Models['FailureWebSocketResponse_type']) {
|
||||
const id = request_id
|
||||
handleFailedModelingCommand(raw: WebSocketResponse) {
|
||||
const id = raw.request_id
|
||||
const failed = raw as Models['FailureWebSocketResponse_type']
|
||||
const errors = failed.errors
|
||||
if (!id) return
|
||||
const command = this.artifactMap[id]
|
||||
if (command && command.type === 'pending') {
|
||||
@ -1223,6 +1264,7 @@ export class EngineCommandManager {
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
errors,
|
||||
raw,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
@ -1238,10 +1280,10 @@ export class EngineCommandManager {
|
||||
tearDown() {
|
||||
this.engineConnection?.tearDown()
|
||||
}
|
||||
startNewSession() {
|
||||
async startNewSession() {
|
||||
this.lastArtifactMap = this.artifactMap
|
||||
this.artifactMap = {}
|
||||
this.initPlanes()
|
||||
await this.initPlanes()
|
||||
}
|
||||
subscribeTo<T extends ModelTypes>({
|
||||
event,
|
||||
@ -1285,6 +1327,16 @@ export class EngineCommandManager {
|
||||
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
|
||||
this.callbacksEngineStateConnection.push(callback)
|
||||
}
|
||||
// We make this a separate function so we can call it from wasm.
|
||||
clearDefaultPlanes() {
|
||||
this.defaultPlanes = null
|
||||
}
|
||||
async wasmGetDefaultPlanes(): Promise<string> {
|
||||
if (this.defaultPlanes === null) {
|
||||
await this.initPlanes()
|
||||
}
|
||||
return JSON.stringify(this.defaultPlanes)
|
||||
}
|
||||
endSession() {
|
||||
const deleteCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
@ -1293,20 +1345,20 @@ export class EngineCommandManager {
|
||||
type: 'scene_clear_all',
|
||||
},
|
||||
}
|
||||
this.defaultPlanes = null
|
||||
this.clearDefaultPlanes()
|
||||
this.engineConnection?.send(deleteCmd)
|
||||
}
|
||||
addCommandLog(message: CommandLog) {
|
||||
if (this._commandLogs.length > 500) {
|
||||
this._commandLogs.shift()
|
||||
if (this.commandLogs.length > 500) {
|
||||
this.commandLogs.shift()
|
||||
}
|
||||
this._commandLogs.push(message)
|
||||
this.commandLogs.push(message)
|
||||
|
||||
this._commandLogCallBack([...this._commandLogs])
|
||||
this._commandLogCallBack([...this.commandLogs])
|
||||
}
|
||||
clearCommandLogs() {
|
||||
this._commandLogs = []
|
||||
this._commandLogCallBack(this._commandLogs)
|
||||
this.commandLogs = []
|
||||
this._commandLogCallBack(this.commandLogs)
|
||||
}
|
||||
registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
|
||||
this._commandLogCallBack = callback
|
||||
@ -1573,7 +1625,14 @@ export class EngineCommandManager {
|
||||
command: commandStr,
|
||||
ast: this.getAst(),
|
||||
idToRangeMap,
|
||||
}).then(({ raw }) => JSON.stringify(raw))
|
||||
}).then(({ raw }: { raw: WebSocketResponse | undefined | null }) => {
|
||||
if (raw === undefined || raw === null) {
|
||||
throw new Error(
|
||||
'returning modeling cmd response to the rust side is undefined or null'
|
||||
)
|
||||
}
|
||||
return JSON.stringify(raw)
|
||||
})
|
||||
}
|
||||
commandResult(id: string): Promise<any> {
|
||||
const command = this.artifactMap[id]
|
||||
@ -1602,30 +1661,15 @@ export class EngineCommandManager {
|
||||
}
|
||||
private async initPlanes() {
|
||||
if (this.planesInitialized()) return
|
||||
const [xy, yz, xz] = [
|
||||
await this.createPlane({
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||
}),
|
||||
await this.createPlane({
|
||||
x_axis: { x: 0, y: 1, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||
}),
|
||||
await this.createPlane({
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||
}),
|
||||
]
|
||||
this.defaultPlanes = { xy, yz, xz }
|
||||
const planes = await this.makeDefaultPlanes()
|
||||
this.defaultPlanes = planes
|
||||
|
||||
this.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) return
|
||||
if (![xy, yz, xz].includes(data.entity_id)) return
|
||||
if (!planes) return
|
||||
if (![planes.xy, planes.yz, planes.xz].includes(data.entity_id)) return
|
||||
this.onPlaneSelectCallback(data.entity_id)
|
||||
},
|
||||
})
|
||||
@ -1655,40 +1699,4 @@ export class EngineCommandManager {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async createPlane({
|
||||
x_axis,
|
||||
y_axis,
|
||||
color,
|
||||
}: {
|
||||
x_axis: Models['Point3d_type']
|
||||
y_axis: Models['Point3d_type']
|
||||
color: Models['Color_type']
|
||||
}): Promise<string> {
|
||||
const planeId = uuidv4()
|
||||
await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 100,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
clobber: false,
|
||||
hide: true,
|
||||
},
|
||||
cmd_id: planeId,
|
||||
})
|
||||
await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
await this.setPlaneHidden(planeId, true)
|
||||
return planeId
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import init, {
|
||||
ServerConfig,
|
||||
copilot_lsp_run,
|
||||
kcl_lsp_run,
|
||||
make_default_planes,
|
||||
coredump,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
@ -21,6 +23,10 @@ import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
||||
import { Coords2d } from './std/sketch'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import { DEV } from 'env'
|
||||
import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||
@ -190,6 +196,21 @@ export const recast = (ast: Program): string => {
|
||||
}
|
||||
}
|
||||
|
||||
export const makeDefaultPlanes = async (
|
||||
engineCommandManager: EngineCommandManager
|
||||
): Promise<DefaultPlanes> => {
|
||||
try {
|
||||
const planes: DefaultPlanes = await make_default_planes(
|
||||
engineCommandManager
|
||||
)
|
||||
return planes
|
||||
} catch (e) {
|
||||
// TODO: do something real with the error.
|
||||
console.log('make default planes error', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function lexer(str: string): Token[] {
|
||||
try {
|
||||
const tokens: Token[] = lexer_wasm(str)
|
||||
@ -302,12 +323,33 @@ export async function copilotLspRun(config: ServerConfig, token: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function kclLspRun(config: ServerConfig, token: string) {
|
||||
export async function kclLspRun(
|
||||
config: ServerConfig,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
token: string
|
||||
) {
|
||||
try {
|
||||
console.log('start kcl lsp')
|
||||
await kcl_lsp_run(config, token, DEV)
|
||||
const baseUnit =
|
||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, DEV)
|
||||
} catch (e: any) {
|
||||
console.log('kcl lsp failed', e)
|
||||
// We can't restart here because a moved value, we should do this another way.
|
||||
}
|
||||
}
|
||||
|
||||
export async function coreDump(
|
||||
coreDumpManager: CoreDumpManager,
|
||||
openGithubIssue: boolean = false
|
||||
): Promise<AppInfo> {
|
||||
try {
|
||||
const dump: AppInfo = await coredump(coreDumpManager)
|
||||
if (openGithubIssue && dump.github_issue_url) {
|
||||
openWindow(dump.github_issue_url)
|
||||
}
|
||||
return dump
|
||||
} catch (e: any) {
|
||||
throw new Error(`Error getting core dump: ${e}`)
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
||||
import { buildCommandArgument } from 'lib/createMachineCommand'
|
||||
import decamelize from 'decamelize'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { Setting } from 'lib/settings/initialSettings'
|
||||
|
||||
// An array of the paths to all of the settings that have commandConfigs
|
||||
export const settingsWithCommandConfigs = (
|
||||
@ -87,11 +88,34 @@ export function createSettingsCommand({
|
||||
)
|
||||
return null
|
||||
|
||||
const valueArgConfig = {
|
||||
let valueArgConfig = {
|
||||
...valueArgPartialConfig,
|
||||
required: true,
|
||||
} as CommandArgumentConfig<S['default']>
|
||||
|
||||
// If the setting is a boolean, we coerce it into an options input type
|
||||
if (valueArgConfig.inputType === 'boolean') {
|
||||
valueArgConfig = {
|
||||
...valueArgConfig,
|
||||
inputType: 'options',
|
||||
options: (cmdBarContext, machineContext) => {
|
||||
const setting = getPropertyByPath(
|
||||
machineContext,
|
||||
type
|
||||
) as Setting<boolean>
|
||||
const level = cmdBarContext.argumentsToSubmit.level as SettingsLevel
|
||||
const isCurrent =
|
||||
setting[level] === undefined
|
||||
? setting.getFallback(level) === true
|
||||
: setting[level] === true
|
||||
return [
|
||||
{ name: 'On', value: true, isCurrent },
|
||||
{ name: 'Off', value: false, isCurrent: !isCurrent },
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore - TODO figure out this typing for valueArgConfig
|
||||
const valueArg = buildCommandArgument(valueArgConfig, context, actor)
|
||||
|
||||
|
@ -151,7 +151,8 @@ export type CommandArgumentConfig<
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
|
150
src/lib/coredump.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { WebrtcStats } from 'wasm-lib/kcl/bindings/WebrtcStats'
|
||||
import { OsInfo } from 'wasm-lib/kcl/bindings/OsInfo'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import {
|
||||
platform as tauriPlatform,
|
||||
arch as tauriArch,
|
||||
version as tauriKernelVersion,
|
||||
} from '@tauri-apps/plugin-os'
|
||||
import { APP_VERSION } from 'routes/Settings'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import screenshot from 'lib/screenshot'
|
||||
import React from 'react'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
|
||||
// This is a class for getting all the values from the JS world to pass to the Rust world
|
||||
// for a core dump.
|
||||
export class CoreDumpManager {
|
||||
engineCommandManager: EngineCommandManager
|
||||
htmlRef: React.RefObject<HTMLDivElement> | null
|
||||
token: string | undefined
|
||||
baseUrl: string = VITE_KC_API_BASE_URL
|
||||
|
||||
constructor(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
htmlRef: React.RefObject<HTMLDivElement> | null,
|
||||
token: string | undefined
|
||||
) {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
this.htmlRef = htmlRef
|
||||
this.token = token
|
||||
}
|
||||
|
||||
// Get the token.
|
||||
authToken(): string {
|
||||
if (!this.token) {
|
||||
throw new Error('Token not set')
|
||||
}
|
||||
return this.token
|
||||
}
|
||||
|
||||
// Get the base url.
|
||||
baseApiUrl(): string {
|
||||
return this.baseUrl
|
||||
}
|
||||
|
||||
// Get the version of the app from the package.json.
|
||||
version(): string {
|
||||
return APP_VERSION
|
||||
}
|
||||
|
||||
// Get the os information.
|
||||
getOsInfo(): Promise<string> {
|
||||
if (this.isTauri()) {
|
||||
return tauriArch()
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Error getting arch: ${error}`)
|
||||
})
|
||||
.then((arch: string) => {
|
||||
return tauriPlatform()
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Error getting platform: ${error}`)
|
||||
})
|
||||
.then((platform: string) => {
|
||||
return tauriKernelVersion()
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Error getting kernel version: ${error}`)
|
||||
})
|
||||
.then((kernelVersion: string) => {
|
||||
const osinfo: OsInfo = {
|
||||
platform,
|
||||
arch,
|
||||
version: kernelVersion,
|
||||
}
|
||||
return JSON.stringify(osinfo)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const userAgent = window.navigator.userAgent || 'unknown browser'
|
||||
if (userAgent === 'unknown browser') {
|
||||
const osinfo: OsInfo = {
|
||||
platform: userAgent,
|
||||
arch: userAgent,
|
||||
version: userAgent,
|
||||
}
|
||||
return new Promise((resolve) => resolve(JSON.stringify(osinfo)))
|
||||
}
|
||||
|
||||
const parser = new UAParser(userAgent)
|
||||
const parserResults = parser.getResult()
|
||||
const osinfo: OsInfo = {
|
||||
platform: parserResults.os.name,
|
||||
arch: parserResults.cpu.architecture,
|
||||
version: parserResults.os.version,
|
||||
browser: userAgent,
|
||||
}
|
||||
return new Promise((resolve) => resolve(JSON.stringify(osinfo)))
|
||||
}
|
||||
|
||||
isTauri(): boolean {
|
||||
return isTauri()
|
||||
}
|
||||
|
||||
getWebrtcStats(): Promise<string> {
|
||||
if (!this.engineCommandManager.engineConnection) {
|
||||
throw new Error('Engine connection not initialized')
|
||||
}
|
||||
|
||||
if (!this.engineCommandManager.engineConnection.webrtcStatsCollector) {
|
||||
throw new Error('Engine webrtcStatsCollector not initialized')
|
||||
}
|
||||
|
||||
return this.engineCommandManager.engineConnection
|
||||
.webrtcStatsCollector()
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Error getting webrtc stats: ${error}`)
|
||||
})
|
||||
.then((stats: any) => {
|
||||
const webrtcStats: WebrtcStats = {
|
||||
packets_lost: stats.rtc_packets_lost,
|
||||
frames_received: stats.rtc_frames_received,
|
||||
frame_width: stats.rtc_frame_width,
|
||||
frame_height: stats.rtc_frame_height,
|
||||
frame_rate: stats.rtc_frames_per_second,
|
||||
key_frames_decoded: stats.rtc_keyframes_decoded,
|
||||
frames_dropped: stats.rtc_frames_dropped,
|
||||
pause_count: stats.rtc_pause_count,
|
||||
total_pauses_duration: stats.rtc_total_pauses_duration_sec,
|
||||
freeze_count: stats.rtc_freeze_count,
|
||||
total_freezes_duration: stats.rtc_total_freezes_duration_sec,
|
||||
pli_count: stats.rtc_pli_count,
|
||||
jitter: stats.rtc_jitter_sec,
|
||||
}
|
||||
return JSON.stringify(webrtcStats)
|
||||
})
|
||||
}
|
||||
|
||||
// Return a data URL (png format) of the screenshot of the current page.
|
||||
screenshot(): Promise<string> {
|
||||
return screenshot(this.htmlRef)
|
||||
.then((screenshot: string) => {
|
||||
return screenshot
|
||||
})
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Error getting screenshot: ${error}`)
|
||||
})
|
||||
}
|
||||
}
|
@ -29,11 +29,11 @@ const bracket = startSketchOn('XY')
|
||||
|> extrude(width, %)
|
||||
|> fillet({
|
||||
radius: filletR,
|
||||
tags: [getNextAdjacentEdge('innerEdge', %)]
|
||||
tags: [getPreviousAdjacentEdge('innerEdge', %)]
|
||||
}, %)
|
||||
|> fillet({
|
||||
radius: filletR + thickness,
|
||||
tags: [getNextAdjacentEdge('outerEdge', %)]
|
||||
tags: [getPreviousAdjacentEdge('outerEdge', %)]
|
||||
}, %)`
|
||||
|
||||
function findLineInExampleCode({
|
||||
|
11
src/lib/openWindow.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { open as tauriOpen } from '@tauri-apps/plugin-shell'
|
||||
|
||||
// Open a new browser window tauri style or browser style.
|
||||
export default async function openWindow(url: string) {
|
||||
if (isTauri()) {
|
||||
await tauriOpen(url)
|
||||
} else {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
@ -100,7 +100,7 @@ export const fileLoader: LoaderFunction = async ({
|
||||
const children = await invoke<FileEntry[]>('read_dir_recursive', {
|
||||
path: projectPath,
|
||||
})
|
||||
kclManager.setCodeAndExecute(code, false)
|
||||
kclManager.setCode(code, false)
|
||||
|
||||
// Set the file system manager to the project path
|
||||
// So that WASM gets an updated path for operations
|
||||
|
21
src/lib/screenshot.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import html2canvas from 'html2canvas-pro'
|
||||
|
||||
// Return a data URL (png format) of the screenshot of the current page.
|
||||
export default async function screenshot(
|
||||
htmlRef: React.RefObject<HTMLDivElement> | null
|
||||
): Promise<string> {
|
||||
if (htmlRef === null) {
|
||||
throw new Error('htmlRef is null')
|
||||
}
|
||||
if (htmlRef.current === null) {
|
||||
throw new Error('htmlRef is null')
|
||||
}
|
||||
return html2canvas(htmlRef.current)
|
||||
.then((canvas) => {
|
||||
return canvas.toDataURL()
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error
|
||||
})
|
||||
}
|
@ -137,7 +137,13 @@ export function createSettings() {
|
||||
description: 'The hue of the primary theme color for the app',
|
||||
validate: (v) => Number(v) >= 0 && Number(v) < 360,
|
||||
Component: ({ value, updateValue }) => (
|
||||
<div className="flex item-center gap-2 px-2">
|
||||
<div className="flex item-center gap-4 px-2 m-0 py-0">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full bg-primary border border-solid border-chalkboard-100 dark:border-chalkboard-30"
|
||||
style={{
|
||||
backgroundColor: `oklch(var(--primary-lightness) var(--primary-chroma) ${value})`,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
onChange={(e) => updateValue(e.currentTarget.value)}
|
||||
@ -147,13 +153,6 @@ export function createSettings() {
|
||||
step={1}
|
||||
className="block flex-1"
|
||||
/>
|
||||
<span className="text-xs block w-[6ch] text-right">{value}º</span>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full bg-primary"
|
||||
style={{
|
||||
backgroundColor: `oklch(var(--primary-lightness) var(--primary-chroma) ${value})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
@ -362,6 +361,17 @@ export function createSettings() {
|
||||
inputType: 'boolean',
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* Whether to make the cursor blink in the editor
|
||||
*/
|
||||
blinkingCursor: new Setting<boolean>({
|
||||
defaultValue: true,
|
||||
description: 'Whether to make the cursor blink in the editor',
|
||||
validate: (v) => typeof v === 'boolean',
|
||||
commandConfig: {
|
||||
inputType: 'boolean',
|
||||
},
|
||||
}),
|
||||
},
|
||||
/**
|
||||
* Settings that affect the behavior of project management.
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { type Models } from '@kittycad/lib'
|
||||
import { Setting, settings } from './initialSettings'
|
||||
import { AtLeast, PathValue, Paths } from 'lib/types'
|
||||
import { ChangeEventHandler } from 'react'
|
||||
import { CommandArgumentConfig } from 'lib/commandTypes'
|
||||
|
||||
export enum UnitSystem {
|
||||
|
@ -5,8 +5,19 @@ import {
|
||||
} from '../lang/std/engineConnection'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { Themes } from './theme'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||
|
||||
const defaultPlanes: DefaultPlanes = {
|
||||
xy: uuidv4(),
|
||||
xz: uuidv4(),
|
||||
yz: uuidv4(),
|
||||
negXy: uuidv4(),
|
||||
negXz: uuidv4(),
|
||||
negYz: uuidv4(),
|
||||
}
|
||||
|
||||
class MockEngineCommandManager {
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
@ -27,13 +38,19 @@ class MockEngineCommandManager {
|
||||
command: EngineCommand
|
||||
}): Promise<any> {
|
||||
const response: WebSocketResponse = {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: { type: 'empty' },
|
||||
success: true,
|
||||
resp: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: { type: 'empty' },
|
||||
},
|
||||
},
|
||||
}
|
||||
return Promise.resolve(JSON.stringify(response))
|
||||
}
|
||||
async wasmGetDefaultPlanes(): Promise<string> {
|
||||
return JSON.stringify(defaultPlanes)
|
||||
}
|
||||
sendModelingCommandFromWasm(
|
||||
id: string,
|
||||
rangeStr: string,
|
||||
@ -81,8 +98,10 @@ export async function executor(
|
||||
setMediaStream: () => {},
|
||||
width: 0,
|
||||
height: 0,
|
||||
executeCode: () => {},
|
||||
theme: Themes.Dark,
|
||||
makeDefaultPlanes: () => {
|
||||
return new Promise((resolve) => resolve(defaultPlanes))
|
||||
},
|
||||
})
|
||||
await engineCommandManager.waitForReady
|
||||
engineCommandManager.startNewSession()
|
||||
|
@ -26,7 +26,7 @@ export function setThemeClass(theme: Themes) {
|
||||
export function getThemeColorForEngine(theme: Themes) {
|
||||
const resolvedTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
const dark = 28 / 255
|
||||
const light = 242 / 255
|
||||
const light = 249 / 255
|
||||
return resolvedTheme === Themes.Dark
|
||||
? { r: dark, g: dark, b: dark, a: 1 }
|
||||
: { r: light, g: light, b: light, a: 1 }
|
||||
|