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
|
directory: '/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
reviewers:
|
||||||
|
- franknoirot
|
||||||
|
- irev-dev
|
||||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||||
directory: '/' # Location of package manifests
|
directory: '/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
reviewers:
|
||||||
|
- adamchalmers
|
||||||
|
- jessfraz
|
||||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||||
directory: '/src/wasm-lib/' # Location of package manifests
|
directory: '/src/wasm-lib/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
reviewers:
|
||||||
|
- adamchalmers
|
||||||
|
- jessfraz
|
||||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||||
directory: '/src-tauri/' # Location of package manifests
|
directory: '/src-tauri/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
reviewers:
|
||||||
|
- adamchalmers
|
||||||
|
- jessfraz
|
||||||
|
15
.github/workflows/ci.yml
vendored
@ -104,7 +104,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||||
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
|
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
|
- uses: actions/upload-artifact@v3
|
||||||
if: github.event_name == 'schedule'
|
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) }}
|
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' }}
|
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' }}
|
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:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
@ -295,9 +300,9 @@ jobs:
|
|||||||
--arg pub_date "${PUB_DATE}" \
|
--arg pub_date "${PUB_DATE}" \
|
||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_sig "$DARWIN_SIG" \
|
--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_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,
|
"version": $version,
|
||||||
"pub_date": $pub_date,
|
"pub_date": $pub_date,
|
||||||
@ -326,8 +331,8 @@ jobs:
|
|||||||
--arg version "${VERSION}" \
|
--arg version "${VERSION}" \
|
||||||
--arg pub_date "${PUB_DATE}" \
|
--arg pub_date "${PUB_DATE}" \
|
||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
|
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
|
||||||
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
|
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
|
||||||
'{
|
'{
|
||||||
"version": $version,
|
"version": $version,
|
||||||
"pub_date": $pub_date,
|
"pub_date": $pub_date,
|
||||||
|
5
.github/workflows/generate-website-docs.yml
vendored
@ -15,7 +15,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
generate-website-docs:
|
generate-website-docs:
|
||||||
name: generate-website-docs
|
name: generate-website-docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -56,6 +56,9 @@ jobs:
|
|||||||
gh pr create --title "Update KCL docs" \
|
gh pr create --title "Update KCL docs" \
|
||||||
--body "Updating the generated kcl docs cc @jessfraz @franknoirot merge this" \
|
--body "Updating the generated kcl docs cc @jessfraz @franknoirot merge this" \
|
||||||
--head "$NEW_BRANCH" \
|
--head "$NEW_BRANCH" \
|
||||||
|
--reviewer jessfraz \
|
||||||
|
--reviewer irev-dev \
|
||||||
|
--reviewer franknoirot \
|
||||||
--base main || true
|
--base main || true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
@ -17,11 +17,11 @@ lineTo(to: [number], sketch_group: SketchGroup, tag?: String) -> SketchGroup
|
|||||||
```js
|
```js
|
||||||
fn rectShape = (pos, w, l) => {
|
fn rectShape = (pos, w, l) => {
|
||||||
const rr = startSketchOn('YZ')
|
const rr = startSketchOn('YZ')
|
||||||
|> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
|
|> 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)], %, "edge1")
|
||||||
|> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, "edge2")
|
|> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, "edge2")
|
||||||
|> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, "edge3")
|
|> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, "edge3")
|
||||||
|> close(%, "edge4")
|
|> close(%, "edge4")
|
||||||
return rr
|
return rr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const part = startSketchOn('XY')
|
|||||||
{
|
{
|
||||||
// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
||||||
arcDegrees: number,
|
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],
|
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.
|
// 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,
|
repetitions: number,
|
||||||
|
@ -42,7 +42,7 @@ const part = startSketchOn('XY')
|
|||||||
arcDegrees: number,
|
arcDegrees: number,
|
||||||
// The axis around which to make the pattern. This is a 3D vector.
|
// The axis around which to make the pattern. This is a 3D vector.
|
||||||
axis: [number, number, number],
|
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],
|
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.
|
// 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,
|
repetitions: number,
|
||||||
|
@ -121,9 +121,9 @@ const sketch001 = startSketchOn(box, "END")
|
|||||||
// Angle to revolve (in degrees). Default is 360.
|
// Angle to revolve (in degrees). Default is 360.
|
||||||
angle: number,
|
angle: number,
|
||||||
// Axis of revolution.
|
// Axis of revolution.
|
||||||
axis: "x" |
|
axis: "X" |
|
||||||
"y" |
|
"Y" |
|
||||||
"z" |
|
"Z" |
|
||||||
"-X" |
|
"-X" |
|
||||||
"-Y" |
|
"-Y" |
|
||||||
"-Z" |
|
"-Z" |
|
||||||
|
@ -27,12 +27,12 @@ startSketchOn('XY')
|
|||||||
```js
|
```js
|
||||||
fn cube = (pos, scale) => {
|
fn cube = (pos, scale) => {
|
||||||
const sg = startSketchOn('XY')
|
const sg = startSketchOn('XY')
|
||||||
|> startProfileAt(pos, %)
|
|> startProfileAt(pos, %)
|
||||||
|> line([0, scale], %)
|
|> line([0, scale], %)
|
||||||
|> line([scale, 0], %)
|
|> line([scale, 0], %)
|
||||||
|> line([0, -scale], %)
|
|> line([0, -scale], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(scale, %)
|
|> extrude(scale, %)
|
||||||
|
|
||||||
return sg
|
return sg
|
||||||
}
|
}
|
||||||
|
@ -26564,7 +26564,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"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,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"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"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"center": {
|
"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",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@ -44168,7 +44168,7 @@
|
|||||||
"minItems": 3
|
"minItems": 3
|
||||||
},
|
},
|
||||||
"center": {
|
"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",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@ -49363,21 +49363,21 @@
|
|||||||
"description": "X-axis.",
|
"description": "X-axis.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"x"
|
"X"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Y-axis.",
|
"description": "Y-axis.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"y"
|
"Y"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Z-axis.",
|
"description": "Z-axis.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"z"
|
"Z"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -58824,7 +58824,7 @@
|
|||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")",
|
"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 { getUtils } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { basicStorageState } from './storageStates'
|
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
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
|
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,
|
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
|
// kill animations, speeds up tests and reduced flakiness
|
||||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test.setTimeout(60000)
|
test.setTimeout(60000)
|
||||||
|
|
||||||
test('Basic sketch', async ({ page, context }) => {
|
test('Basic sketch', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
@ -128,6 +145,7 @@ test('Can moving camera', async ({ page, context }) => {
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
|
||||||
const camPos: [number, number, number] = [0, 85, 85]
|
const camPos: [number, number, number] = [0, 85, 85]
|
||||||
const bakeInRetries = async (
|
const bakeInRetries = async (
|
||||||
@ -161,6 +179,8 @@ test('Can moving camera', async ({ page, context }) => {
|
|||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByTestId('cam-x-position').isVisible()
|
||||||
|
|
||||||
const vals = await Promise.all([
|
const vals = await Promise.all([
|
||||||
page.getByTestId('cam-x-position').inputValue(),
|
page.getByTestId('cam-x-position').inputValue(),
|
||||||
page.getByTestId('cam-y-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()
|
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)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const part001 = startSketchOn('-XZ')
|
||||||
@ -325,7 +345,11 @@ test('executes on load', async ({ page, context }) => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// expand variables section
|
// 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)
|
// 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
|
// 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()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('re-executes', async ({ page, context }) => {
|
test('re-executes', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async (token) => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem('persistCode', `const myVar = 5`)
|
localStorage.setItem('persistCode', `const myVar = 5`)
|
||||||
})
|
})
|
||||||
await page.setViewportSize({ width: 1000, height: 500 })
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
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"
|
// expect to see "myVar:5"
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('.pretty-json-container >> text=myVar:5')
|
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
|
// expect there to be three auto complete options
|
||||||
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
||||||
await page.getByText('startSketchOn').click()
|
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.press('Enter')
|
||||||
await page.keyboard.type(' |> startProfi')
|
await page.keyboard.type(' |> startProfi')
|
||||||
// expect there be a single auto complete option that we can just hit enter on
|
// 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.waitForTimeout(100)
|
||||||
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
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.press('Enter')
|
||||||
await page.keyboard.type(' |> lin')
|
await page.keyboard.type(' |> lin')
|
||||||
|
|
||||||
@ -501,145 +533,149 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
// finish line with comment
|
// 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)
|
await page.waitForTimeout(100)
|
||||||
// there shouldn't be any auto complete options for 'lin' in the comment
|
// 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-completionLabel')).not.toBeVisible()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('XY')
|
.toHaveText(`const part001 = startSketchOn('XY')
|
||||||
|> startProfileAt([0,0], %)
|
|> startProfileAt([3.14, 3.14], %)
|
||||||
|> xLine(5, %) // lin`)
|
|> xLine(5, %) // lin`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Stored settings validation test
|
test('Stored settings are validated and fall back to defaults', async ({
|
||||||
test.describe('Settings persistence and validation tests', () => {
|
page,
|
||||||
// Override test setup
|
context,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
|
||||||
|
// Override beforeEach test setup
|
||||||
// with corrupted settings
|
// with corrupted settings
|
||||||
const storageState = structuredClone(basicStorageState)
|
await context.addInitScript(
|
||||||
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
|
async ({ settingsKey, settings }) => {
|
||||||
settings: SaveSettingsPayload
|
localStorage.setItem(settingsKey, settings)
|
||||||
}
|
},
|
||||||
s.settings.app.theme = Themes.Dark
|
{
|
||||||
s.settings.app.projectDirectory = 123 as any
|
settingsKey: TEST_SETTINGS_KEY,
|
||||||
s.settings.modeling.defaultUnit = 'invalid' as any
|
settings: TOML.stringify({ settings: TEST_SETTINGS_CORRUPTED }),
|
||||||
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)
|
|
||||||
|
|
||||||
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 ({
|
// Check the settings were reset
|
||||||
page,
|
const storedSettings = TOML.parse(
|
||||||
}) => {
|
await page.evaluate(
|
||||||
const u = getUtils(page)
|
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
{ settingsKey: TEST_SETTINGS_KEY }
|
||||||
await page.goto('/')
|
)
|
||||||
await u.waitForAuthSkipAppStart()
|
) as { settings: SaveSettingsPayload }
|
||||||
|
|
||||||
// Check the settings were reset
|
expect(storedSettings.settings.app?.theme).toBe('dark')
|
||||||
const storedSettings = TOML.parse(
|
|
||||||
await page.evaluate(() => localStorage.getItem('/user.toml') || '{}')
|
|
||||||
) as { settings: SaveSettingsPayload }
|
|
||||||
|
|
||||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
// Check that the invalid settings were removed
|
||||||
|
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
||||||
// Check that the invalid settings were removed
|
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
||||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
||||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
expect(storedSettings.settings.projects?.defaultProjectName).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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Onboarding tests
|
test('Project settings can be set and override user settings', async ({
|
||||||
test.describe('Onboarding tests', () => {
|
page,
|
||||||
// Override test setup
|
}) => {
|
||||||
const storageState = structuredClone(basicStorageState)
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
|
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||||
settings: SaveSettingsPayload
|
await page
|
||||||
}
|
.getByRole('button', { name: 'Start Sketch' })
|
||||||
s.settings.app.onboardingStatus = '/export'
|
.waitFor({ state: 'visible' })
|
||||||
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
|
|
||||||
test.use({ storageState })
|
|
||||||
|
|
||||||
test('Onboarding redirects and code updating', async ({ page, context }) => {
|
// Open the settings modal with the browser keyboard shortcut
|
||||||
const u = getUtils(page)
|
await page.keyboard.press('Meta+Shift+,')
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await expect(
|
||||||
await page.goto('/')
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||||
await u.waitForAuthSkipAppStart()
|
).toBeVisible()
|
||||||
|
await page
|
||||||
|
.locator('select[name="app-theme"]')
|
||||||
|
.selectOption({ value: 'light' })
|
||||||
|
|
||||||
// Test that the redirect happened
|
// Verify the toast appeared
|
||||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
await expect(
|
||||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
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
|
// Check that the project setting did not change
|
||||||
await page.reload()
|
await page.getByRole('radio', { name: 'Project' }).click()
|
||||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
|
||||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
})
|
||||||
)
|
|
||||||
|
|
||||||
// Test that the onboarding pane loaded
|
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||||
const title = page.locator('[data-testid="onboarding-content"]')
|
const u = getUtils(page)
|
||||||
await expect(title).toBeAttached()
|
|
||||||
|
|
||||||
// Test that the code changes when you advance to the next step
|
// Override beforeEach test setup
|
||||||
await page.locator('[data-testid="onboarding-next"]').click()
|
await page.addInitScript(
|
||||||
await expect(page.locator('.cm-content')).toHaveText('')
|
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.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.locator('[data-testid="onboarding-next"]').click()
|
await page.goto('/')
|
||||||
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
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 }) => {
|
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`)
|
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override test setup code
|
test('Can extrude from the command bar', async ({ page }) => {
|
||||||
const storageState = structuredClone(basicStorageState)
|
await page.addInitScript(async () => {
|
||||||
storageState.origins[0].localStorage[1].value = `const distance = sqrt(20)
|
localStorage.setItem(
|
||||||
const part001 = startSketchOn('-XZ')
|
'persistCode',
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
`const distance = sqrt(20)
|
||||||
|> line([25.1, 0.41], %)
|
const part001 = startSketchOn('-XZ')
|
||||||
|> line([0.73, -14.93], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> close(%)
|
|> line([0.73, -14.93], %)
|
||||||
`
|
|> line([-23.44, 0.52], %)
|
||||||
test.use({ storageState })
|
|> close(%)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('Can extrude from the command bar', async ({ page, context }) => {
|
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -872,15 +910,13 @@ test.describe('Command bar tests', () => {
|
|||||||
// Make sure the stream is up
|
// Make sure the stream is up
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
await u.clearCommandLogs()
|
||||||
await expect(
|
await page.getByText('|> line([0.73, -14.93], %)').click()
|
||||||
page.getByRole('button', { name: 'Extrude' })
|
await page.getByRole('button', { name: 'Extrude' }).isEnabled()
|
||||||
).not.toBeDisabled()
|
|
||||||
|
|
||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
await page.keyboard.press('Meta+K')
|
await page.keyboard.press('Meta+K')
|
||||||
@ -898,23 +934,25 @@ test.describe('Command bar tests', () => {
|
|||||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||||
'distance001'
|
'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
|
// Review step and argument hotkeys
|
||||||
await expect(
|
await expect(submitButton).toBeEnabled()
|
||||||
page.getByRole('button', { name: 'Submit command' })
|
|
||||||
).toBeEnabled()
|
|
||||||
await page.keyboard.press('Backspace')
|
await page.keyboard.press('Backspace')
|
||||||
|
|
||||||
|
// Assert we're back on the distance step
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Distance 12', exact: false })
|
page.getByRole('button', { name: 'Distance 12', exact: false })
|
||||||
).toBeDisabled()
|
).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
|
// Check that the code was updated
|
||||||
await page.keyboard.press('Enter')
|
await u.waitForCmdReceive('extrude')
|
||||||
// Unfortunately this indentation seems to matter for the test
|
// Unfortunately this indentation seems to matter for the test
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`const distance = sqrt(20)
|
`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)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part = startSketchOn('XY')
|
`const part = startSketchOn('XY')
|
||||||
@ -1067,7 +1105,7 @@ test('ProgramMemory can be serialised', async ({ page, context }) => {
|
|||||||
|> line([0, -1], %)
|
|> line([0, -1], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(1, %)
|
|> extrude(1, %)
|
||||||
|> patternLinear({
|
|> patternLinear3d({
|
||||||
axis: [1, 0, 1],
|
axis: [1, 0, 1],
|
||||||
repetitions: 3,
|
repetitions: 3,
|
||||||
distance: 6
|
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 ({
|
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||||
page,
|
page,
|
||||||
context,
|
|
||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
const selectionsSnippets = {
|
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], %)',
|
extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)',
|
||||||
editOnly: '|> startProfileAt([15.79, -14.6], %)',
|
editOnly: '|> startProfileAt([15.79, -14.6], %)',
|
||||||
}
|
}
|
||||||
await context.addInitScript(
|
await page.addInitScript(
|
||||||
async ({
|
async ({
|
||||||
extrudeAndEditBlocked,
|
extrudeAndEditBlocked,
|
||||||
extrudeAndEditBlockedInFunction,
|
extrudeAndEditBlockedInFunction,
|
||||||
@ -1138,11 +1175,11 @@ const part003 = startSketchOn('-XZ')
|
|||||||
|
|
||||||
fn yohey = (pos) => {
|
fn yohey = (pos) => {
|
||||||
const part004 = startSketchOn('-XZ')
|
const part004 = startSketchOn('-XZ')
|
||||||
${extrudeAndEditBlockedInFunction}
|
${extrudeAndEditBlockedInFunction}
|
||||||
|> line([27.55, -1.65], %)
|
|> line([27.55, -1.65], %)
|
||||||
|> line([4.95, -10.53], %)
|
|> line([4.95, -10.53], %)
|
||||||
|> line([-20.38, -8], %)
|
|> line([-20.38, -8], %)
|
||||||
|> line([-15.79, 17.08], %)
|
|> line([-15.79, 17.08], %)
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1152,7 +1189,7 @@ fn yohey = (pos) => {
|
|||||||
},
|
},
|
||||||
selectionsSnippets
|
selectionsSnippets
|
||||||
)
|
)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
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()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can edit segments by dragging their handles', async ({
|
test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||||
page,
|
|
||||||
context,
|
|
||||||
}) => {
|
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const part001 = startSketchOn('-XZ')
|
||||||
@ -1422,9 +1456,9 @@ test('Snap to close works (at any scale)', async ({ page }) => {
|
|||||||
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
|
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Sketch on face', async ({ page, context }) => {
|
test('Sketch on face', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
@ -7,16 +7,26 @@ import { spawn } from 'child_process'
|
|||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { basicSettings, basicStorageState } from './storageStates'
|
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// reducedMotion kills animations, which speeds up tests and reduces flakiness
|
// reducedMotion kills animations, which speeds up tests and reduces flakiness
|
||||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
})
|
|
||||||
|
|
||||||
test.use({
|
// set the default settings
|
||||||
storageState: structuredClone(basicStorageState),
|
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)
|
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
|
// FYI this test doesn't work with only engine running locally
|
||||||
// And you will need to have the KittyCAD CLI installed
|
// And you will need to have the KittyCAD CLI installed
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
;(window as any).playwrightSkipFilePicker = true
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -69,6 +79,7 @@ const part001 = startSketchOn('-XZ')
|
|||||||
|> extrude(4, %)`
|
|> extrude(4, %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -364,17 +375,19 @@ test('extrude on each default plane should be stable', async ({
|
|||||||
await u.removeCurrentCode()
|
await u.removeCurrentCode()
|
||||||
// add makeCode('XZ')
|
// add makeCode('XZ')
|
||||||
await u.openAndClearDebugPanel()
|
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
|
// wait for execution done
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
await page.getByText('Code').click()
|
await u.closeKclCodePanel()
|
||||||
await page.waitForTimeout(150)
|
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
await page.getByText('Code').click()
|
await u.openKclCodePanel()
|
||||||
}
|
}
|
||||||
await runSnapshotsForOtherPlanes('XY')
|
await runSnapshotsForOtherPlanes('XY')
|
||||||
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 ({
|
test.describe('Client side scene scale should match engine scale', () => {
|
||||||
page,
|
test('Inch scale', async ({ page }) => {
|
||||||
}) => {
|
const u = getUtils(page)
|
||||||
const u = getUtils(page)
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
await page.goto('/')
|
||||||
await page.goto('/')
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.openDebugPanel()
|
||||||
await u.openDebugPanel()
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await u.doAndWaitForImageDiff(
|
await u.doAndWaitForImageDiff(
|
||||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`const part001 = startSketchOn('-XZ')`
|
`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
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([9.06, -12.22], %)`)
|
|> startProfileAt([9.06, -12.22], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([9.06, -12.22], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([9.14, 0], %)`)
|
|> line([9.14, 0], %)`)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
await page.waitForTimeout(100)
|
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'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([9.06, -12.22], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([9.14, 0], %)
|
|> line([9.14, 0], %)
|
||||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||||
|
|
||||||
// click tangential arc tool again to unequip it
|
// click tangential arc tool again to unequip it
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// screen shot should show the sketch
|
// screen shot should show the sketch
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
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
|
test('Millimeter scale', async ({ page }) => {
|
||||||
await u.openAndClearDebugPanel()
|
await page.addInitScript(
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
async ({ settingsKey, settings }) => {
|
||||||
|
localStorage.setItem(settingsKey, settings)
|
||||||
// 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',
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
})
|
settingsKey: TEST_SETTINGS_KEY,
|
||||||
test.use({
|
settings: TOML.stringify({
|
||||||
storageState,
|
settings: {
|
||||||
})
|
...TEST_SETTINGS,
|
||||||
|
modeling: {
|
||||||
test('Millimeters', async ({ page }) => {
|
...TEST_SETTINGS.modeling,
|
||||||
|
defaultUnit: 'mm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
@ -638,7 +654,7 @@ test('Sketch on face with none z-up', async ({ page, context }) => {
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(5 + 7, %)
|
|> extrude(5 + 7, %)
|
||||||
const part002 = startSketchOn(part001, 'seg01')
|
const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([-2.89, 1.82], %)
|
|> startProfileAt([8, 8], %)
|
||||||
|> line([4.68, 3.05], %)
|
|> line([4.68, 3.05], %)
|
||||||
|> line([0, -7.79], %, 'seg02')
|
|> line([0, -7.79], %, 'seg02')
|
||||||
|> close(%)
|
|> close(%)
|
||||||
@ -650,6 +666,19 @@ const part002 = startSketchOn(part001, 'seg01')
|
|||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
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(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).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 { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { secrets } from './secrets'
|
|
||||||
import * as TOML from '@iarna/toml'
|
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
|
||||||
export const basicSettings = {
|
export const TEST_SETTINGS_KEY = '/user.toml'
|
||||||
|
export const TEST_SETTINGS = {
|
||||||
app: {
|
app: {
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
onboardingStatus: 'dismissed',
|
onboardingStatus: 'dismissed',
|
||||||
@ -22,19 +21,26 @@ export const basicSettings = {
|
|||||||
},
|
},
|
||||||
} satisfies Partial<SaveSettingsPayload>
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
|
|
||||||
export const basicStorageState = {
|
export const TEST_SETTINGS_ONBOARDING = {
|
||||||
cookies: [],
|
...TEST_SETTINGS,
|
||||||
origins: [
|
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
|
||||||
{
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
origin: 'http://localhost:3000',
|
|
||||||
localStorage: [
|
export const TEST_SETTINGS_CORRUPTED = {
|
||||||
{ name: 'TOKEN_PERSIST_KEY', value: secrets.token },
|
app: {
|
||||||
{ name: 'persistCode', value: '' },
|
theme: Themes.Dark,
|
||||||
{
|
onboardingStatus: 'dismissed',
|
||||||
name: '/user.toml',
|
projectDirectory: 123 as any,
|
||||||
value: TOML.stringify({ settings: basicSettings }),
|
},
|
||||||
},
|
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) {
|
async function openKclCodePanel(page: Page) {
|
||||||
const isOpen =
|
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||||
(await page
|
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||||
.locator('[data-testid="debug-panel"]')
|
|
||||||
?.getAttribute('open')) === ''
|
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
await page.getByText('Debug').click()
|
await paneLocator.click()
|
||||||
await page.getByTestId('debug-panel').and(page.locator('[open]')).waitFor()
|
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) {
|
async function closeDebugPanel(page: Page) {
|
||||||
const isOpen =
|
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||||
(await page.getByTestId('debug-panel')?.getAttribute('open')) === ''
|
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
await page.getByText('Debug').click()
|
await debugLocator.click()
|
||||||
await page
|
await debugLocator
|
||||||
.getByTestId('debug-panel')
|
.and(page.locator(':not([aria-selected="true"])'))
|
||||||
.and(page.locator(':not([open])'))
|
|
||||||
.waitFor()
|
.waitFor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,20 +99,19 @@ export function getUtils(page: Page) {
|
|||||||
removeCurrentCode: () => removeCurrentCode(page),
|
removeCurrentCode: () => removeCurrentCode(page),
|
||||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||||
updateCamPosition: async (xyz: [number, number, number]) => {
|
updateCamPosition: async (xyz: [number, number, number]) => {
|
||||||
const fillInput = async () => {
|
const fillInput = async (axis: 'x' | 'y' | 'z', value: number) => {
|
||||||
await page.fill('[data-testid="cam-x-position"]', String(xyz[0]))
|
await page.fill(`[data-testid="cam-${axis}-position"]`, String(value))
|
||||||
await page.fill('[data-testid="cam-y-position"]', String(xyz[1]))
|
await page.waitForTimeout(100)
|
||||||
await page.fill('[data-testid="cam-z-position"]', String(xyz[2]))
|
|
||||||
}
|
}
|
||||||
await fillInput()
|
|
||||||
await page.waitForTimeout(100)
|
await fillInput('x', xyz[0])
|
||||||
await fillInput()
|
await fillInput('y', xyz[1])
|
||||||
await page.waitForTimeout(100)
|
await fillInput('z', xyz[2])
|
||||||
await fillInput()
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
},
|
},
|
||||||
clearCommandLogs: () => clearCommandLogs(page),
|
clearCommandLogs: () => clearCommandLogs(page),
|
||||||
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
||||||
|
openKclCodePanel: () => openKclCodePanel(page),
|
||||||
|
closeKclCodePanel: () => closeKclCodePanel(page),
|
||||||
openDebugPanel: () => openDebugPanel(page),
|
openDebugPanel: () => openDebugPanel(page),
|
||||||
closeDebugPanel: () => closeDebugPanel(page),
|
closeDebugPanel: () => closeDebugPanel(page),
|
||||||
openAndClearDebugPanel: async () => {
|
openAndClearDebugPanel: async () => {
|
||||||
|
45
package.json
@ -4,8 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.15.0",
|
"@codemirror/autocomplete": "^6.15.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.18",
|
||||||
@ -15,29 +15,30 @@
|
|||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@replit/codemirror-interact": "^6.3.0",
|
"@replit/codemirror-interact": "^6.3.1",
|
||||||
"@tauri-apps/api": "^2.0.0-beta.7",
|
"@tauri-apps/api": "2.0.0-beta.7",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
||||||
"@tauri-apps/plugin-fs": "^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-http": "^2.0.0-beta.2",
|
||||||
"@tauri-apps/plugin-os": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-os": "^2.0.0-beta.2",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@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",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@tweenjs/tween.js": "^23.1.1",
|
"@tweenjs/tween.js": "^23.1.1",
|
||||||
"@types/node": "^18.19.26",
|
"@types/node": "^18.19.31",
|
||||||
"@types/react": "^18.2.73",
|
"@types/react": "^18.2.77",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@uiw/react-codemirror": "^4.21.25",
|
"@uiw/react-codemirror": "^4.21.25",
|
||||||
"@xstate/inspect": "^0.8.0",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"debounce-promise": "^3.1.2",
|
"debounce-promise": "^3.1.2",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.5",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"html2canvas-pro": "^1.4.3",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"json-rpc-2.0": "^1.6.0",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
@ -52,18 +53,19 @@
|
|||||||
"react-modal-promise": "^1.0.2",
|
"react-modal-promise": "^1.0.2",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.2.2",
|
"swr": "^2.2.5",
|
||||||
"three": "^0.160.0",
|
"three": "^0.163.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.5",
|
||||||
|
"ua-parser-js": "^1.0.37",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vitest": "^1.4.0",
|
"vitest": "^1.5.0",
|
||||||
"vscode-jsonrpc": "^8.1.0",
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.16.0",
|
||||||
"xstate": "^4.38.2",
|
"xstate": "^4.38.2",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
@ -115,23 +117,24 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.24.3",
|
"@babel/preset-env": "^7.24.3",
|
||||||
"@playwright/test": "^1.39.0",
|
"@playwright/test": "^1.43.0",
|
||||||
"@tauri-apps/cli": "^2.0.0-beta.12",
|
"@tauri-apps/cli": "^2.0.0-beta.13",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/debounce-promise": "^3.1.9",
|
"@types/debounce-promise": "^3.1.9",
|
||||||
"@types/pixelmatch": "^5.2.6",
|
"@types/pixelmatch": "^5.2.6",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/react-modal": "^3.16.3",
|
"@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/uuid": "^9.0.8",
|
||||||
"@types/wait-on": "^5.3.4",
|
"@types/wait-on": "^5.3.4",
|
||||||
"@types/wicg-file-system-access": "^2023.10.5",
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.10",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@wdio/cli": "^8.24.3",
|
"@wdio/cli": "^8.24.3",
|
||||||
"@wdio/globals": "^8.24.3",
|
"@wdio/globals": "^8.36.0",
|
||||||
"@wdio/local-runner": "^8.35.1",
|
"@wdio/local-runner": "^8.35.1",
|
||||||
"@wdio/mocha-framework": "^8.35.0",
|
"@wdio/mocha-framework": "^8.36.0",
|
||||||
"@wdio/spec-reporter": "^8.32.4",
|
"@wdio/spec-reporter": "^8.32.4",
|
||||||
"@xstate/cli": "^0.5.17",
|
"@xstate/cli": "^0.5.17",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
@ -153,6 +156,6 @@
|
|||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||||
"wait-on": "^7.2.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 { defineConfig, devices } from '@playwright/test'
|
||||||
import { basicStorageState } from './e2e/playwright/storageStates'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* 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 */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
/* Use a common shared localStorage */
|
|
||||||
storageState: basicStorageState,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
|
39
src-tauri/Cargo.lock
generated
@ -69,9 +69,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.81"
|
version = "1.0.82"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
@ -2231,9 +2231,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad"
|
name = "kittycad"
|
||||||
version = "0.2.63"
|
version = "0.2.67"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93a332250e08fd715ad3d5826e04d36da1c5bb42d0c1b1ff1f0598278b9ebf3c"
|
checksum = "fc460442c165c8e707b1154551cefd08938d10bb80c78940e10cd9869487c325"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -2333,7 +2333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.48.0",
|
"windows-targets 0.52.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4467,14 +4467,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.0.0-beta.14"
|
version = "2.0.0-beta.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a50fb0bdb687486415224f8be47c78993e9f3ea575ee0d5177c90d0c71842f4a"
|
checksum = "cd0aba659957a3f1f1666acbf17723e8d41dcc177539bf1adbe55305f5d7118a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
|
"dunce",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"getrandom 0.2.9",
|
"getrandom 0.2.9",
|
||||||
@ -4515,9 +4516,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "82a46303cc4bce0b17ad95965cbd8326e3511b9d2cb6fb13a4a4c98a11b0dcaf"
|
checksum = "33de24aabe2b9c340d67005800cb6dd40aac5283126a42896fc8eec0b87cbe45"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@ -4537,9 +4538,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1665f6a986842061a67cb9dcbe2fa27076c1a616f6525fc06de9d6d52838d63"
|
checksum = "9d1d211268a9590bbf75cc85b47208f59b447626c76396256e12479ac7df6c8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.0",
|
"base64 0.22.0",
|
||||||
"brotli",
|
"brotli",
|
||||||
@ -4564,9 +4565,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0c1558fc42cc2a1735cfd5edb2954c735d4516f8ba31c58b7180ba8a2bc18de"
|
checksum = "b096f63f2724a1280ae0f5a34d0731de18ca18305e2ef6e5e9a39bb2710e8a85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -4720,9 +4721,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "612e05de1382575b32b5220b546861256f630f37ac64c29cab252592861b9bd4"
|
checksum = "96c957749c40db7999959f379f799db095f2248a80bdbb13d8c078f6c299240e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dpi",
|
"dpi",
|
||||||
"gtk",
|
"gtk",
|
||||||
@ -4739,9 +4740,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f73672897b5396cb05c2f21b12b66ecfd4b51fae619dd35387467660d6c00fb"
|
checksum = "6b937adb1cf3fa0457928ace959ca3fc1a85ddd69f56b124682d40f3e5683e60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"gtk",
|
"gtk",
|
||||||
@ -4763,9 +4764,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a148adf8077e1891c8b7d1c2be90c1c8eb8c7a071c35bb8edbdfe7cd9d8e23c"
|
checksum = "760ac613d7f0de95067bcbcbcea175fe1df88fc4ab59c7f0b2cc2d01dc16a199"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"cargo_metadata",
|
"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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
tauri-build = { version = "2.0.0-beta.12", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
kittycad = "0.2.63"
|
kittycad = "0.2.67"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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-dialog = { version = "2.0.0-beta.0" }
|
||||||
tauri-plugin-fs = { version = "2.0.0-beta.0" }
|
tauri-plugin-fs = { version = "2.0.0-beta.0" }
|
||||||
tauri-plugin-http = { 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 { MouseEventHandler, useEffect, useRef } from 'react'
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { PaneType, useStore } from './useStore'
|
import { useStore } from './useStore'
|
||||||
import { Logs, KCLErrors } from './components/Logs'
|
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
|
||||||
import { MemoryPanel } from './components/MemoryPanel'
|
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
import { Stream } from './components/Stream'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
import { EngineCommand } from './lang/std/engineConnection'
|
import { EngineCommand } from './lang/std/engineConnection'
|
||||||
import { throttle } from './lib/utils'
|
import { throttle } from './lib/utils'
|
||||||
import { AppHeader } from './components/AppHeader'
|
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 { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||||
@ -24,9 +14,6 @@ import { type IndexLoaderData } from 'lib/types'
|
|||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
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 { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
@ -34,6 +21,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
|||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||||
|
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
useRefreshSettings(paths.FILE + 'SETTINGS')
|
useRefreshSettings(paths.FILE + 'SETTINGS')
|
||||||
@ -41,6 +29,9 @@ export function App() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
const { onProjectOpen } = useLspContext()
|
const { onProjectOpen } = useLspContext()
|
||||||
|
// We need the ref for the outermost div so we can screenshot the app for
|
||||||
|
// the coredump.
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const projectName = project?.name || null
|
const projectName = project?.name || null
|
||||||
const projectPath = project?.path || null
|
const projectPath = project?.path || null
|
||||||
@ -49,43 +40,24 @@ export function App() {
|
|||||||
}, [projectName, projectPath])
|
}, [projectName, projectPath])
|
||||||
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const { buttonDownInStream, didDragInStream, streamDimensions, setHtmlRef } =
|
||||||
buttonDownInStream,
|
useStore((s) => ({
|
||||||
openPanes,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
setOpenPanes,
|
didDragInStream: s.didDragInStream,
|
||||||
didDragInStream,
|
streamDimensions: s.streamDimensions,
|
||||||
streamDimensions,
|
setHtmlRef: s.setHtmlRef,
|
||||||
} = useStore((s) => ({
|
}))
|
||||||
buttonDownInStream: s.buttonDownInStream,
|
|
||||||
openPanes: s.openPanes,
|
useEffect(() => {
|
||||||
setOpenPanes: s.setOpenPanes,
|
setHtmlRef(ref)
|
||||||
didDragInStream: s.didDragInStream,
|
}, [ref])
|
||||||
streamDimensions: s.streamDimensions,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const {
|
const {
|
||||||
modeling: { showDebugPanel },
|
app: { onboardingStatus },
|
||||||
app: { theme, onboardingStatus },
|
|
||||||
} = settings.context
|
} = settings.context
|
||||||
const { state, send } = useModelingContext()
|
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('esc', () => send('Cancel'))
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -140,6 +112,7 @@ export function App() {
|
|||||||
<div
|
<div
|
||||||
className="relative h-full flex flex-col"
|
className="relative h-full flex flex-col"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
className={
|
className={
|
||||||
@ -151,74 +124,8 @@ export function App() {
|
|||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Resizable
|
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||||
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>
|
|
||||||
<Stream className="absolute inset-0 z-0" />
|
<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 /> */}
|
{/* <CamToggle /> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -35,15 +35,17 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
loader: settingsLoader,
|
loader: settingsLoader,
|
||||||
id: paths.INDEX,
|
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: (
|
element: (
|
||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
<KclContextProvider>
|
<SettingsAuthProvider>
|
||||||
<SettingsAuthProvider>
|
<LspProvider>
|
||||||
<LspProvider>
|
<KclContextProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</LspProvider>
|
</KclContextProvider>
|
||||||
</SettingsAuthProvider>
|
</LspProvider>
|
||||||
</KclContextProvider>
|
</SettingsAuthProvider>
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
|
@ -110,14 +110,6 @@ export class CameraControls {
|
|||||||
}, 400) as any as number
|
}, 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) => {
|
setCam = (camProps: ReactCameraProperties) => {
|
||||||
if (
|
if (
|
||||||
camProps.type === 'perspective' &&
|
camProps.type === 'perspective' &&
|
||||||
@ -910,6 +902,26 @@ export class CameraControls {
|
|||||||
.start()
|
.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 = () => {}
|
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
||||||
setReactCameraPropertiesCallback = (
|
setReactCameraPropertiesCallback = (
|
||||||
cb: (a: ReactCameraProperties) => void
|
cb: (a: ReactCameraProperties) => void
|
||||||
@ -937,24 +949,7 @@ export class CameraControls {
|
|||||||
isPerspective: this.isPerspective,
|
isPerspective: this.isPerspective,
|
||||||
target: this.target,
|
target: this.target,
|
||||||
})
|
})
|
||||||
this.deferReactUpdate({
|
this.deferReactUpdate(this.reactCameraProperties)
|
||||||
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),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||||
}
|
}
|
||||||
getInteractionType = (event: any) =>
|
getInteractionType = (event: any) =>
|
||||||
|
@ -126,12 +126,9 @@ const throttled = throttle((a: ReactCameraProperties) => {
|
|||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
|
|
||||||
export const CamDebugSettings = () => {
|
export const CamDebugSettings = () => {
|
||||||
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
|
const [camSettings, setCamSettings] = useState<ReactCameraProperties>(
|
||||||
type: 'perspective',
|
sceneInfra.camControls.reactCameraProperties
|
||||||
fov: 12,
|
)
|
||||||
position: [0, 0, 0],
|
|
||||||
quaternion: [0, 0, 0, 1],
|
|
||||||
})
|
|
||||||
const [fov, setFov] = useState(12)
|
const [fov, setFov] = useState(12)
|
||||||
|
|
||||||
useEffect(() => {
|
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
|
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 */
|
||||||
--chalkboard-10: oklch(99.9% 0.003766 102.8deg);
|
|
||||||
--chalkboard-20: oklch(91.34% 0.009353 109deg);
|
/* Chalkboard */
|
||||||
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
|
--chalkboard-10: oklch(99.11% 0 var(--primary-hue));
|
||||||
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);
|
--chalkboard-20: oklch(95.51% 0 var(--primary-hue));
|
||||||
--chalkboard-50: oklch(66.27% 0.01111 127.6deg);
|
--chalkboard-30: oklch(88.48% 0 var(--primary-hue));
|
||||||
--chalkboard-60: oklch(57.92% 0.0117 133.9deg);
|
--chalkboard-40: oklch(82.01% 0 var(--primary-hue));
|
||||||
--chalkboard-70: oklch(49.56% 0.01229 140.1deg);
|
--chalkboard-50: oklch(72.41% 0 var(--primary-hue));
|
||||||
--chalkboard-80: oklch(41.21% 0.01288 146.3deg);
|
--chalkboard-60: oklch(68.19% 0 var(--primary-hue));
|
||||||
--chalkboard-90: oklch(32.85% 0.01346 152.5deg);
|
--chalkboard-70: oklch(48.19% 0 var(--primary-hue));
|
||||||
--chalkboard-100: oklch(24.49% 0.01405 158.7deg);
|
--chalkboard-80: oklch(39.04% 0 var(--primary-hue));
|
||||||
--chalkboard-110: oklch(16.14% 0.01464 164.9deg);
|
--chalkboard-90: oklch(30.12% 0 var(--primary-hue));
|
||||||
--chalkboard-120: oklch(7.783% 0.01522 171.1deg);
|
--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 */
|
||||||
--energy-10: oklch(93.31% 0.227 122.3deg);
|
--energy-10: oklch(93.31% 0.227 122.3deg);
|
||||||
@ -144,18 +146,18 @@
|
|||||||
|
|
||||||
/* Base values for use with Tailwind. */
|
/* Base values for use with Tailwind. */
|
||||||
/* Chalkboard */
|
/* Chalkboard */
|
||||||
--_chalkboard-10: 99.7% 0.008766 102.8deg;
|
--_chalkboard-10: 99.11% 0 var(--primary-hue);
|
||||||
--_chalkboard-20: 91.34% 0.009353 109deg;
|
--_chalkboard-20: 95.51% 0 var(--primary-hue);
|
||||||
--_chalkboard-30: 82.99% 0.00994 115.2deg;
|
--_chalkboard-30: 88.48% 0 var(--primary-hue);
|
||||||
--_chalkboard-40: 74.63% 0.01053 121.4deg;
|
--_chalkboard-40: 82.01% 0 var(--primary-hue);
|
||||||
--_chalkboard-50: 66.27% 0.01111 127.6deg;
|
--_chalkboard-50: 72.41% 0 var(--primary-hue);
|
||||||
--_chalkboard-60: 57.92% 0.0117 133.9deg;
|
--_chalkboard-60: 68.19% 0 var(--primary-hue);
|
||||||
--_chalkboard-70: 49.56% 0.01229 140.1deg;
|
--_chalkboard-70: 48.19% 0 var(--primary-hue);
|
||||||
--_chalkboard-80: 41.21% 0.01288 146.3deg;
|
--_chalkboard-80: 39.04% 0 var(--primary-hue);
|
||||||
--_chalkboard-90: 32.85% 0.01346 152.5deg;
|
--_chalkboard-90: 30.12% 0 var(--primary-hue);
|
||||||
--_chalkboard-100: 24.49% 0.01405 158.7deg;
|
--_chalkboard-100: 22.64% 0 var(--primary-hue);
|
||||||
--_chalkboard-110: 16.14% 0.01464 164.9deg;
|
--_chalkboard-110: 18.22% 0 var(--primary-hue);
|
||||||
--_chalkboard-120: 7.783% 0.01522 171.1deg;
|
--_chalkboard-120: 0% 0 var(--primary-hue);
|
||||||
|
|
||||||
/* Energy */
|
/* Energy */
|
||||||
--_energy-10: 93.31% 0.227 122.3deg;
|
--_energy-10: 93.31% 0.227 122.3deg;
|
||||||
|
@ -16,7 +16,7 @@ export function AstExplorer() {
|
|||||||
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" style={{ width: '300px' }}>
|
<div id="ast-explorer" className="relative">
|
||||||
<div className="">
|
<div className="">
|
||||||
filter out keys:<div className="w-2 inline-block"></div>
|
filter out keys:<div className="w-2 inline-block"></div>
|
||||||
{['start', 'end', 'type'].map((key) => {
|
{['start', 'end', 'type'].map((key) => {
|
||||||
@ -45,7 +45,7 @@ export function AstExplorer() {
|
|||||||
setHighlightRange([0, 0])
|
setHighlightRange([0, 0])
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
<pre className="text-xs">
|
||||||
<DisplayObj
|
<DisplayObj
|
||||||
obj={kclManager.ast}
|
obj={kclManager.ast}
|
||||||
filterKeys={filterKeys}
|
filterKeys={filterKeys}
|
||||||
@ -109,7 +109,7 @@ function DisplayObj({
|
|||||||
<pre
|
<pre
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`ml-2 border-l border-violet-600 pl-1 ${
|
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) => {
|
onMouseEnter={(e) => {
|
||||||
setHighlightRange([obj?.start || 0, obj.end])
|
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'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
function useEngineCommands(): [CommandLog[], () => void] {
|
function useEngineCommands(): [CommandLog[], () => void] {
|
||||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>([])
|
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
|
||||||
|
engineCommandManager.commandLogs
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
engineCommandManager.registerCommandLogCallback((commands) =>
|
engineCommandManager.registerCommandLogCallback((commands) =>
|
||||||
|
@ -171,7 +171,7 @@ const FileTreeItem = ({
|
|||||||
|
|
||||||
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
||||||
// Import non-kcl files
|
// Import non-kcl files
|
||||||
kclManager.setCodeAndExecute(
|
kclManager.setCode(
|
||||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||||
kclManager.code
|
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 { LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
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 { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||||
import Server from '../editor/plugins/lsp/server'
|
import Server from '../editor/plugins/lsp/server'
|
||||||
import Client from '../editor/plugins/lsp/client'
|
import Client from '../editor/plugins/lsp/client'
|
||||||
@ -14,6 +14,7 @@ import { LanguageSupport } from '@codemirror/language'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { FileEntry } from 'lib/types'
|
import { FileEntry } from 'lib/types'
|
||||||
|
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||||
|
|
||||||
const DEFAULT_FILE_NAME: string = 'main.kcl'
|
const DEFAULT_FILE_NAME: string = 'main.kcl'
|
||||||
|
|
||||||
@ -60,16 +61,27 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
isCopilotLspServerReady,
|
isCopilotLspServerReady,
|
||||||
setIsKclLspServerReady,
|
setIsKclLspServerReady,
|
||||||
setIsCopilotLspServerReady,
|
setIsCopilotLspServerReady,
|
||||||
|
isStreamReady,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
isKclLspServerReady: s.isKclLspServerReady,
|
isKclLspServerReady: s.isKclLspServerReady,
|
||||||
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
||||||
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
||||||
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
||||||
|
isStreamReady: s.isStreamReady,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { auth } = useSettingsAuthContext()
|
const {
|
||||||
|
auth,
|
||||||
|
settings: {
|
||||||
|
context: {
|
||||||
|
modeling: { defaultUnit },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { overallState } = useNetworkStatus()
|
||||||
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||||
|
|
||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
// 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.
|
// 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' })
|
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
|
||||||
return { lspClient }
|
return { lspClient }
|
||||||
}, [setIsKclLspServerReady, token])
|
}, [
|
||||||
|
setIsKclLspServerReady,
|
||||||
|
// We need a token for authenticating the server.
|
||||||
|
token,
|
||||||
|
])
|
||||||
|
|
||||||
// Here we initialize the plugin which will start the client.
|
// 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
|
// 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
|
return plugin
|
||||||
}, [kclLspClient, isKclLspServerReady])
|
}, [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 { lspClient: copilotLspClient } = useMemo(() => {
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
const fromServer: FromServer = FromServer.create()
|
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'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||||
import { pathMapToSelections } from 'lang/util'
|
import { pathMapToSelections } from 'lang/util'
|
||||||
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import {
|
import {
|
||||||
Selections,
|
Selections,
|
||||||
@ -38,7 +39,7 @@ import {
|
|||||||
getSketchQuaternion,
|
getSketchQuaternion,
|
||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
|
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 { getNodePathFromSourceRange, isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||||
@ -47,6 +48,8 @@ import toast from 'react-hot-toast'
|
|||||||
import { EditorSelection } from '@uiw/react-codemirror'
|
import { EditorSelection } from '@uiw/react-codemirror'
|
||||||
import { Vector3 } from 'three'
|
import { Vector3 } from 'three'
|
||||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
||||||
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -76,6 +79,16 @@ export const ModelingMachineProvider = ({
|
|||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useSetupEngineManager(streamRef, token, theme.current)
|
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 {
|
const {
|
||||||
isShiftDown,
|
isShiftDown,
|
||||||
@ -105,6 +118,24 @@ export const ModelingMachineProvider = ({
|
|||||||
modelingMachine,
|
modelingMachine,
|
||||||
{
|
{
|
||||||
actions: {
|
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': () => {
|
'sketch exit execute': () => {
|
||||||
try {
|
try {
|
||||||
kclManager.executeAst(parse(kclManager.code))
|
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 { Menu } from '@headlessui/react'
|
||||||
import { PropsWithChildren } from 'react'
|
import { PropsWithChildren } from 'react'
|
||||||
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
|
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ActionIcon } from './ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import styles from './CodeMenu.module.css'
|
import styles from './KclEditorMenu.module.css'
|
||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
import { editorShortcutMeta } from './TextEditor'
|
import { editorShortcutMeta } from './KclEditorPane'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
|
|
||||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
|||||||
className="p-1"
|
className="p-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
bgClassName={
|
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'}
|
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
|
||||||
/>
|
/>
|
||||||
@ -65,7 +65,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
|||||||
>
|
>
|
||||||
<span>Read the KCL docs</span>
|
<span>Read the KCL docs</span>
|
||||||
<small>
|
<small>
|
||||||
On GitHub
|
zoo.dev
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
className="ml-1 align-text-top"
|
className="ml-1 align-text-top"
|
||||||
@ -83,7 +83,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
|||||||
>
|
>
|
||||||
<span>KCL samples</span>
|
<span>KCL samples</span>
|
||||||
<small>
|
<small>
|
||||||
On GitHub
|
zoo.dev
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
className="ml-1 align-text-top"
|
className="ml-1 align-text-top"
|
@ -1,35 +1,65 @@
|
|||||||
import { undo, redo } from '@codemirror/commands'
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import ReactCodeMirror, {
|
import ReactCodeMirror from '@uiw/react-codemirror'
|
||||||
Extension,
|
|
||||||
ViewUpdate,
|
|
||||||
keymap,
|
|
||||||
SelectionRange,
|
|
||||||
} from '@uiw/react-codemirror'
|
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { processCodeMirrorRanges } from 'lib/selections'
|
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 { roundOff } from 'lib/utils'
|
||||||
import { kclErrToDiagnostic } from 'lang/errors'
|
import {
|
||||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
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 { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import interact from '@replit/codemirror-interact'
|
import interact from '@replit/codemirror-interact'
|
||||||
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
|
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
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 = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
codeMirror: 'Alt-Shift-f',
|
|
||||||
display: 'Alt + Shift + F',
|
display: 'Alt + Shift + F',
|
||||||
},
|
},
|
||||||
convertToVariable: {
|
convertToVariable: {
|
||||||
@ -38,28 +68,23 @@ export const editorShortcutMeta = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextEditor = ({
|
export const KclEditorPane = () => {
|
||||||
theme,
|
const {
|
||||||
}: {
|
settings: { context },
|
||||||
theme: Themes.Light | Themes.Dark
|
} = useSettingsAuthContext()
|
||||||
}) => {
|
const theme =
|
||||||
|
context.app.theme.current === Themes.System
|
||||||
|
? getSystemTheme()
|
||||||
|
: context.app.theme.current
|
||||||
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
|
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
|
||||||
editorView: s.editorView,
|
editorView: s.editorView,
|
||||||
setEditorView: s.setEditorView,
|
setEditorView: s.setEditorView,
|
||||||
isShiftDown: s.isShiftDown,
|
isShiftDown: s.isShiftDown,
|
||||||
}))
|
}))
|
||||||
const { code, errors } = useKclContext()
|
const { code } = useKclContext()
|
||||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||||
const { overallState } = useNetworkStatus()
|
|
||||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
|
||||||
const { copilotLSP, kclLSP } = useLspContext()
|
const { copilotLSP, kclLSP } = useLspContext()
|
||||||
|
const navigate = useNavigate()
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === 'undefined') return
|
|
||||||
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
|
|
||||||
window.addEventListener('online', onlineCallback)
|
|
||||||
return () => window.removeEventListener('online', onlineCallback)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useHotkeys('mod+z', (e) => {
|
useHotkeys('mod+z', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -82,17 +107,33 @@ export const TextEditor = ({
|
|||||||
|
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const textWrapping = settings.context.textEditor.textWrapping
|
const textWrapping = settings.context.textEditor.textWrapping
|
||||||
|
const cursorBlinking = settings.context.textEditor.blinkingCursor
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
|
||||||
const onChange = async (newCode: string) => {
|
const onChange = async (newCode: string) => {
|
||||||
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
// If we are just fucking around in a snippet, return early and don't
|
||||||
else kclManager.setCode(newCode)
|
// 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 lastSelection = useRef('')
|
||||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
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) {
|
if (!editorView) {
|
||||||
setEditorView(viewUpdate.view)
|
setEditorView(viewUpdate.view)
|
||||||
}
|
}
|
||||||
@ -146,8 +187,22 @@ export const TextEditor = ({
|
|||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
const editorExtensions = useMemo(() => {
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
drawSelection({
|
||||||
|
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
||||||
|
}),
|
||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
|
history(),
|
||||||
|
closeBrackets(),
|
||||||
|
codeFolding(),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...searchKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...foldKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
...lintKeymap,
|
||||||
|
indentWithTab,
|
||||||
{
|
{
|
||||||
key: 'Meta-k',
|
key: 'Meta-k',
|
||||||
run: () => {
|
run: () => {
|
||||||
@ -156,10 +211,10 @@ export const TextEditor = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: editorShortcutMeta.formatCode.codeMirror,
|
key: isTauri() ? 'Meta-,' : 'Meta-Shift-,',
|
||||||
run: () => {
|
run: () => {
|
||||||
kclManager.format()
|
navigate(makeUrlPathRelative(paths.SETTINGS))
|
||||||
return true
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -175,16 +230,28 @@ export const TextEditor = ({
|
|||||||
]),
|
]),
|
||||||
] as Extension[]
|
] as Extension[]
|
||||||
|
|
||||||
if (kclLSP) extensions.push(kclLSP)
|
if (kclLSP) extensions.push(Prec.highest(kclLSP))
|
||||||
if (copilotLSP) extensions.push(copilotLSP)
|
if (copilotLSP) extensions.push(copilotLSP)
|
||||||
|
|
||||||
// These extensions have proven to mess with vitest
|
// These extensions have proven to mess with vitest
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
extensions.push(
|
extensions.push(
|
||||||
lintGutter(),
|
lintGutter(),
|
||||||
linter((_view) => {
|
lineNumbers(),
|
||||||
return kclErrToDiagnostic(errors)
|
highlightActiveLineGutter(),
|
||||||
}),
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
foldGutter(),
|
||||||
|
EditorState.allowMultipleSelections.of(true),
|
||||||
|
indentOnInput(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
highlightSelectionMatches(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
rectangularSelection(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
interact({
|
interact({
|
||||||
rules: [
|
rules: [
|
||||||
// a rule for a number dragger
|
// a rule for a number dragger
|
||||||
@ -222,22 +289,22 @@ export const TextEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}, [kclLSP, textWrapping.current, convertCallback])
|
}, [kclLSP, textWrapping.current, cursorBlinking.current, convertCallback])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="code-mirror-override"
|
id="code-mirror-override"
|
||||||
className="full-height-subtract"
|
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
||||||
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
|
||||||
>
|
>
|
||||||
<ReactCodeMirror
|
<ReactCodeMirror
|
||||||
className="h-full"
|
|
||||||
value={code}
|
value={code}
|
||||||
extensions={editorExtensions}
|
extensions={editorExtensions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||||
|
indentWithTab={false}
|
||||||
|
basicSetup={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { processMemory } from './MemoryPane'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../../../lib/testHelpers'
|
||||||
import { initPromise, parse } from '../lang/wasm'
|
import { initPromise, parse } from '../../../lang/wasm'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
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 { Popover, Transition } from '@headlessui/react'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
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 { uuidv4 } from 'lib/utils'
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { settings } from 'lib/settings/initialSettings'
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
@ -116,7 +116,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toastSuccess: (context, event) => {
|
toastSuccess: (_, event) => {
|
||||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||||
keyof typeof settings,
|
keyof typeof settings,
|
||||||
string
|
string
|
||||||
@ -138,7 +138,6 @@ export const SettingsAuthProviderBase = ({
|
|||||||
id: `${event.type}.success`,
|
id: `${event.type}.success`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'Execute AST': () => kclManager.executeAst(),
|
|
||||||
persistSettings: (context) =>
|
persistSettings: (context) =>
|
||||||
saveSettings(context, loadedProject?.project?.path),
|
saveSettings(context, loadedProject?.project?.path),
|
||||||
},
|
},
|
||||||
@ -211,6 +210,19 @@ export const SettingsAuthProviderBase = ({
|
|||||||
)
|
)
|
||||||
}, [settingsState.context.app.themeColor.current])
|
}, [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
|
// Auth machine setup
|
||||||
const [authState, authSend, authActor] = useMachine(authMachine, {
|
const [authState, authSend, authActor] = useMachine(authMachine, {
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -77,7 +77,4 @@ export function applyConstraintEqualLength({
|
|||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
})
|
})
|
||||||
return { modifiedAst, pathToNodeMap }
|
return { modifiedAst, pathToNodeMap }
|
||||||
// kclManager.updateAst(modifiedAst, true, {
|
|
||||||
// // callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
@ -94,11 +94,15 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(:hover, :focus-visible, :active) > .tooltip {
|
:is(:hover, :active) > .tooltip {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition-delay: var(--_delay);
|
transition-delay: var(--_delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(:focus-visible) > .tooltip.withFocus {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
:is(:focus, :focus-visible, :focus-within) > .tooltip {
|
:is(:focus, :focus-visible, :focus-within) > .tooltip {
|
||||||
--_delay: 0 !important;
|
--_delay: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ interface TooltipProps extends React.PropsWithChildren {
|
|||||||
| 'inlineEnd'
|
| 'inlineEnd'
|
||||||
className?: string
|
className?: string
|
||||||
delay?: number
|
delay?: number
|
||||||
|
hoverOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tooltip({
|
export default function Tooltip({
|
||||||
@ -22,13 +23,16 @@ export default function Tooltip({
|
|||||||
position = 'top',
|
position = 'top',
|
||||||
className,
|
className,
|
||||||
delay = 200,
|
delay = 200,
|
||||||
|
hoverOnly = false,
|
||||||
}: TooltipProps) {
|
}: TooltipProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
|
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
|
||||||
inert="true"
|
inert="true"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
className={styles.tooltip + ' ' + styles[position] + ' ' + className}
|
className={`${styles.tooltip} ${hoverOnly ? '' : styles.withFocus} ${
|
||||||
|
styles[position]
|
||||||
|
} ${className}`}
|
||||||
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
|
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -16,7 +16,7 @@ export const lineHighlightField = StateField.define({
|
|||||||
if (e.is(addLineHighlight)) {
|
if (e.is(addLineHighlight)) {
|
||||||
lines = Decoration.none
|
lines = Decoration.none
|
||||||
const [from, to] = e.value || [0, 0]
|
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)] })
|
lines = lines.update({ add: [matchDeco.range(from, to)] })
|
||||||
deco.push(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 { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
|
||||||
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
||||||
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
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/
|
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||||
|
|
||||||
@ -21,9 +25,17 @@ interface LSPRequestMap {
|
|||||||
LSP.SemanticTokensParams,
|
LSP.SemanticTokensParams,
|
||||||
LSP.SemanticTokens
|
LSP.SemanticTokens
|
||||||
]
|
]
|
||||||
getCompletions: [CopilotLspCompletionParams, CopilotCompletionResponse]
|
'textDocument/formatting': [
|
||||||
notifyAccepted: [CopilotAcceptCompletionParams, any]
|
LSP.DocumentFormattingParams,
|
||||||
notifyRejected: [CopilotRejectCompletionParams, any]
|
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
|
// Client to server
|
||||||
@ -36,6 +48,8 @@ interface LSPNotifyMap {
|
|||||||
'workspace/didCreateFiles': LSP.CreateFilesParams
|
'workspace/didCreateFiles': LSP.CreateFilesParams
|
||||||
'workspace/didRenameFiles': LSP.RenameFilesParams
|
'workspace/didRenameFiles': LSP.RenameFilesParams
|
||||||
'workspace/didDeleteFiles': LSP.DeleteFilesParams
|
'workspace/didDeleteFiles': LSP.DeleteFilesParams
|
||||||
|
'copilot/notifyAccepted': CopilotAcceptCompletionParams
|
||||||
|
'copilot/notifyRejected': CopilotRejectCompletionParams
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LanguageServerClientOptions {
|
export interface LanguageServerClientOptions {
|
||||||
@ -53,11 +67,11 @@ export interface LanguageServerOptions {
|
|||||||
|
|
||||||
export class LanguageServerClient {
|
export class LanguageServerClient {
|
||||||
private client: Client
|
private client: Client
|
||||||
private name: string
|
readonly name: string
|
||||||
|
|
||||||
public ready: boolean
|
public ready: boolean
|
||||||
|
|
||||||
private plugins: LanguageServerPlugin[]
|
readonly plugins: LanguageServerPlugin[]
|
||||||
|
|
||||||
public initializePromise: Promise<void>
|
public initializePromise: Promise<void>
|
||||||
|
|
||||||
@ -182,6 +196,22 @@ export class LanguageServerClient {
|
|||||||
return await this.request('textDocument/hover', params)
|
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) {
|
async textDocumentCompletion(params: LSP.CompletionParams) {
|
||||||
const serverCapabilities = this.getServerCapabilities()
|
const serverCapabilities = this.getServerCapabilities()
|
||||||
if (!serverCapabilities.completionProvider) {
|
if (!serverCapabilities.completionProvider) {
|
||||||
@ -215,7 +245,7 @@ export class LanguageServerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCompletion(params: CopilotLspCompletionParams) {
|
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)]
|
this.queuedUids = [...response.completions.map((c) => c.uuid)]
|
||||||
return response
|
return response
|
||||||
@ -224,22 +254,34 @@ export class LanguageServerClient {
|
|||||||
async accept(uuid: string) {
|
async accept(uuid: string) {
|
||||||
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
||||||
this.queuedUids = []
|
this.queuedUids = []
|
||||||
await this.acceptCompletion({ uuid })
|
this.acceptCompletion({ uuid })
|
||||||
await this.rejectCompletions({ uuids: badUids })
|
this.rejectCompletions({ uuids: badUids })
|
||||||
}
|
}
|
||||||
|
|
||||||
async reject() {
|
async reject() {
|
||||||
const badUids = this.queuedUids
|
const badUids = this.queuedUids
|
||||||
this.queuedUids = []
|
this.queuedUids = []
|
||||||
return await this.rejectCompletions({ uuids: badUids })
|
this.rejectCompletions({ uuids: badUids })
|
||||||
}
|
}
|
||||||
|
|
||||||
async acceptCompletion(params: CopilotAcceptCompletionParams) {
|
acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||||
return await this.request('notifyAccepted', params)
|
this.notify('copilot/notifyAccepted', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async rejectCompletions(params: CopilotRejectCompletionParams) {
|
rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||||
return await this.request('notifyRejected', params)
|
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) {
|
private processNotifications(notification: LSP.NotificationMessage) {
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { autocompletion } from '@codemirror/autocomplete'
|
import { autocompletion } from '@codemirror/autocomplete'
|
||||||
import { Extension } from '@codemirror/state'
|
import { Extension, EditorState, Prec } from '@codemirror/state'
|
||||||
import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
|
import {
|
||||||
|
ViewPlugin,
|
||||||
|
hoverTooltip,
|
||||||
|
EditorView,
|
||||||
|
keymap,
|
||||||
|
KeyBinding,
|
||||||
|
tooltips,
|
||||||
|
} from '@codemirror/view'
|
||||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||||
import { offsetToPos } from 'editor/plugins/lsp/util'
|
import { offsetToPos } from 'editor/plugins/lsp/util'
|
||||||
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
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 {
|
import {
|
||||||
LanguageServerPlugin,
|
LanguageServerPlugin,
|
||||||
documentUri,
|
documentUri,
|
||||||
@ -12,21 +20,71 @@ import {
|
|||||||
workspaceFolders,
|
workspaceFolders,
|
||||||
} from 'editor/plugins/lsp/plugin'
|
} 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 {
|
export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||||
let plugin: LanguageServerPlugin | null = null
|
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 [
|
return [
|
||||||
documentUri.of(options.documentUri),
|
documentUri.of(options.documentUri),
|
||||||
languageId.of('kcl'),
|
languageId.of('kcl'),
|
||||||
workspaceFolders.of(options.workspaceFolders),
|
workspaceFolders.of(options.workspaceFolders),
|
||||||
ViewPlugin.define(
|
viewPlugin,
|
||||||
(view) =>
|
kclKeymapExt,
|
||||||
(plugin = new LanguageServerPlugin(
|
kclIndentService(),
|
||||||
options.client,
|
|
||||||
view,
|
|
||||||
options.allowHTMLContent
|
|
||||||
))
|
|
||||||
),
|
|
||||||
hoverTooltip(
|
hoverTooltip(
|
||||||
(view, pos) =>
|
(view, pos) =>
|
||||||
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
||||||
@ -35,7 +93,19 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
|||||||
tooltips({
|
tooltips({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
}),
|
}),
|
||||||
|
linter((view) => {
|
||||||
|
let diagnostics: Diagnostic[] = []
|
||||||
|
forEachDiagnostic(
|
||||||
|
view.state,
|
||||||
|
(d: Diagnostic, from: number, to: number) => {
|
||||||
|
diagnostics.push(d)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return diagnostics
|
||||||
|
}),
|
||||||
|
folding,
|
||||||
autocompletion({
|
autocompletion({
|
||||||
|
defaultKeymap: true,
|
||||||
override: [
|
override: [
|
||||||
async (context) => {
|
async (context) => {
|
||||||
if (plugin == null) return null
|
if (plugin == null) return null
|
||||||
|
@ -19,37 +19,43 @@ export interface LanguageOptions {
|
|||||||
client: LanguageServerClient
|
client: LanguageServerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
class KclLanguage extends Language {
|
||||||
// For now let's use the javascript parser.
|
constructor(options: LanguageOptions) {
|
||||||
// It works really well and has good syntax highlighting.
|
const plugin = kclPlugin({
|
||||||
// We can use our lsp for the rest.
|
documentUri: options.documentUri,
|
||||||
const lang = new Language(
|
workspaceFolders: options.workspaceFolders,
|
||||||
data,
|
allowHTMLContent: true,
|
||||||
jsParser,
|
client: options.client,
|
||||||
[
|
})
|
||||||
EditorState.languageData.of(() => [
|
|
||||||
{
|
super(
|
||||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
data,
|
||||||
commentTokens: {
|
// For now let's use the javascript parser.
|
||||||
line: '//',
|
// It works really well and has good syntax highlighting.
|
||||||
block: {
|
// We can use our lsp for the rest.
|
||||||
open: '/*',
|
jsParser,
|
||||||
close: '*/',
|
[
|
||||||
|
plugin,
|
||||||
|
EditorState.languageData.of(() => [
|
||||||
|
{
|
||||||
|
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
||||||
|
commentTokens: {
|
||||||
|
line: '//',
|
||||||
|
block: {
|
||||||
|
open: '/*',
|
||||||
|
close: '*/',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
]),
|
||||||
]),
|
],
|
||||||
],
|
'kcl'
|
||||||
'kcl'
|
)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
// Create our supporting extension.
|
|
||||||
const kclLsp = kclPlugin({
|
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||||
documentUri: options.documentUri,
|
const lang = new KclLanguage(options)
|
||||||
workspaceFolders: options.workspaceFolders,
|
|
||||||
allowHTMLContent: true,
|
return new LanguageSupport(lang)
|
||||||
client: options.client,
|
|
||||||
})
|
|
||||||
|
|
||||||
return new LanguageSupport(lang, [kclLsp])
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { completeFromList } from '@codemirror/autocomplete'
|
import {
|
||||||
|
completeFromList,
|
||||||
|
hasNextSnippetField,
|
||||||
|
snippetCompletion,
|
||||||
|
} from '@codemirror/autocomplete'
|
||||||
import { setDiagnostics } from '@codemirror/lint'
|
import { setDiagnostics } from '@codemirror/lint'
|
||||||
import { Facet } from '@codemirror/state'
|
import { Facet } from '@codemirror/state'
|
||||||
import { EditorView, Tooltip } from '@codemirror/view'
|
import { EditorView, Tooltip } from '@codemirror/view'
|
||||||
@ -7,7 +11,6 @@ import {
|
|||||||
CompletionItemKind,
|
CompletionItemKind,
|
||||||
CompletionTriggerKind,
|
CompletionTriggerKind,
|
||||||
} from 'vscode-languageserver-protocol'
|
} from 'vscode-languageserver-protocol'
|
||||||
import debounce from 'debounce-promise'
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Completion,
|
Completion,
|
||||||
@ -20,6 +23,12 @@ import type * as LSP from 'vscode-languageserver-protocol'
|
|||||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
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, '')
|
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||||
export const documentUri = Facet.define<string, string>({ combine: useLast })
|
export const documentUri = Facet.define<string, string>({ combine: useLast })
|
||||||
@ -29,8 +38,6 @@ export const workspaceFolders = Facet.define<
|
|||||||
LSP.WorkspaceFolder[]
|
LSP.WorkspaceFolder[]
|
||||||
>({ combine: useLast })
|
>({ combine: useLast })
|
||||||
|
|
||||||
const changesDelay = 500
|
|
||||||
|
|
||||||
const CompletionItemKindMap = Object.fromEntries(
|
const CompletionItemKindMap = Object.fromEntries(
|
||||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||||
) as Record<CompletionItemKind, string>
|
) as Record<CompletionItemKind, string>
|
||||||
@ -41,6 +48,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
public languageId: string
|
public languageId: string
|
||||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
public workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
private documentVersion: number
|
private documentVersion: number
|
||||||
|
private foldingRanges: LSP.FoldingRange[] | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
client: LanguageServerClient,
|
client: LanguageServerClient,
|
||||||
@ -60,9 +68,19 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ docChanged }: ViewUpdate) {
|
update({ docChanged, state }: ViewUpdate) {
|
||||||
if (!docChanged) return
|
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({
|
this.sendChange({
|
||||||
documentText: this.view.state.doc.toString(),
|
documentText: this.view.state.doc.toString(),
|
||||||
})
|
})
|
||||||
@ -101,19 +119,13 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debounce(
|
this.client.textDocumentDidChange({
|
||||||
() => {
|
textDocument: {
|
||||||
return this.client.textDocumentDidChange({
|
uri: this.documentUri,
|
||||||
textDocument: {
|
version: this.documentVersion++,
|
||||||
uri: this.documentUri,
|
|
||||||
version: this.documentVersion++,
|
|
||||||
},
|
|
||||||
contentChanges: [{ text: documentText }],
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
changesDelay,
|
contentChanges: [{ text: documentText }],
|
||||||
{ leading: true }
|
})
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@ -154,6 +166,126 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
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(
|
async requestCompletion(
|
||||||
context: CompletionContext,
|
context: CompletionContext,
|
||||||
{ line, character }: { line: number; character: number },
|
{ 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
|
return completion
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -235,8 +371,12 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
try {
|
try {
|
||||||
switch (notification.method) {
|
switch (notification.method) {
|
||||||
case 'textDocument/publishDiagnostics':
|
case 'textDocument/publishDiagnostics':
|
||||||
this.processDiagnostics(
|
const params = notification.params as PublishDiagnosticsParams
|
||||||
notification.params as PublishDiagnosticsParams
|
this.processDiagnostics(params)
|
||||||
|
// Update the kcl errors pane.
|
||||||
|
kclManager.kclErrors = lspDiagnosticsToKclErrors(
|
||||||
|
this.view.state.doc,
|
||||||
|
params.diagnostics
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'window/logMessage':
|
case 'window/logMessage':
|
||||||
@ -253,6 +393,23 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
notification.params
|
notification.params
|
||||||
)
|
)
|
||||||
break
|
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) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -2,6 +2,7 @@ import { InitOutput, ServerConfig } from 'wasm-lib/pkg/wasm_lib'
|
|||||||
import { FromServer, IntoServer } from './codec'
|
import { FromServer, IntoServer } from './codec'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import { copilotLspRun, initPromise, kclLspRun } from 'lang/wasm'
|
import { copilotLspRun, initPromise, kclLspRun } from 'lang/wasm'
|
||||||
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
|
||||||
export default class Server {
|
export default class Server {
|
||||||
readonly initOutput: InitOutput
|
readonly initOutput: InitOutput
|
||||||
@ -41,7 +42,7 @@ export default class Server {
|
|||||||
if (type_ === 'copilot') {
|
if (type_ === 'copilot') {
|
||||||
await copilotLspRun(config, token)
|
await copilotLspRun(config, token)
|
||||||
} else if (type_ === 'kcl') {
|
} 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 { useLayoutEffect, useEffect, useRef } from 'react'
|
||||||
import { parse } from '../lang/wasm'
|
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { deferExecution } from 'lib/utils'
|
import { deferExecution } from 'lib/utils'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
import { makeDefaultPlanes } from 'lang/wasm'
|
||||||
|
|
||||||
export function useSetupEngineManager(
|
export function useSetupEngineManager(
|
||||||
streamRef: React.RefObject<HTMLDivElement>,
|
streamRef: React.RefObject<HTMLDivElement>,
|
||||||
@ -40,12 +40,11 @@ export function useSetupEngineManager(
|
|||||||
setIsStreamReady,
|
setIsStreamReady,
|
||||||
width: quadWidth,
|
width: quadWidth,
|
||||||
height: quadHeight,
|
height: quadHeight,
|
||||||
executeCode: (code?: string) => {
|
|
||||||
const _ast = parse(code || kclManager.code)
|
|
||||||
return kclManager.executeAst(_ast, true)
|
|
||||||
},
|
|
||||||
token,
|
token,
|
||||||
theme,
|
theme,
|
||||||
|
makeDefaultPlanes: () => {
|
||||||
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
setStreamDimensions({
|
setStreamDimensions({
|
||||||
streamWidth: quadWidth,
|
streamWidth: quadWidth,
|
||||||
|
@ -46,6 +46,15 @@ select {
|
|||||||
@apply bg-chalkboard-90;
|
@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 {
|
::-webkit-scrollbar {
|
||||||
@apply w-2 h-2 rounded-sm;
|
@apply w-2 h-2 rounded-sm;
|
||||||
@apply bg-chalkboard-20;
|
@apply bg-chalkboard-20;
|
||||||
@ -113,32 +122,32 @@ code {
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-height-subtract {
|
/*
|
||||||
--height-subtract: 2.25rem;
|
* The first descendent of the CodeMirror wrapper is the theme,
|
||||||
height: 100%;
|
* but its identifying class can change depending on the theme.
|
||||||
max-height: calc(100% - var(--height-subtract));
|
*/
|
||||||
}
|
#code-mirror-override > div,
|
||||||
|
|
||||||
#code-mirror-override .cm-editor {
|
#code-mirror-override .cm-editor {
|
||||||
@apply h-full bg-transparent;
|
@apply bg-transparent h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-scroller {
|
#code-mirror-override .cm-scroller {
|
||||||
@apply h-full;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-scroller::-webkit-scrollbar {
|
#code-mirror-override .cm-gutter {
|
||||||
@apply h-0;
|
@apply select-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-activeLine,
|
#code-mirror-override .cm-activeLine,
|
||||||
#code-mirror-override .cm-activeLineGutter {
|
#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-activeLine,
|
||||||
.dark #code-mirror-override .cm-activeLineGutter {
|
.dark #code-mirror-override .cm-activeLineGutter {
|
||||||
@apply bg-liquid-80/50;
|
@apply bg-primary/20;
|
||||||
|
mix-blend-mode: lighten;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-gutters {
|
#code-mirror-override .cm-gutters {
|
||||||
@ -149,19 +158,29 @@ code {
|
|||||||
@apply bg-chalkboard-110/50;
|
@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 {
|
#code-mirror-override .cm-focused .cm-cursor {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
#code-mirror-override .cm-cursor {
|
#code-mirror-override .cm-cursor {
|
||||||
display: block;
|
display: block;
|
||||||
width: 1ch;
|
width: 1ch;
|
||||||
@apply bg-liquid-40 mix-blend-multiply;
|
@apply mix-blend-multiply;
|
||||||
|
@apply border-l-primary;
|
||||||
animation: blink 2s ease-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark #code-mirror-override .cm-cursor {
|
.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 {
|
@keyframes blink {
|
||||||
@ -169,8 +188,8 @@ code {
|
|||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
15% {
|
10% {
|
||||||
opacity: 0.75;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { executeAst, executeCode } from 'useStore'
|
import { executeAst } from 'useStore'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EngineCommandManager } from './std/engineConnection'
|
import { EngineCommandManager } from './std/engineConnection'
|
||||||
|
|
||||||
import { deferExecution } from 'lib/utils'
|
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
initPromise,
|
initPromise,
|
||||||
@ -47,19 +46,6 @@ export class KclManager {
|
|||||||
private _params: Params<string> = {}
|
private _params: Params<string> = {}
|
||||||
|
|
||||||
engineCommandManager: EngineCommandManager
|
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 _isExecutingCallback: (arg: boolean) => void = () => {}
|
||||||
private _codeCallBack: (arg: string) => void = () => {}
|
private _codeCallBack: (arg: string) => void = () => {}
|
||||||
@ -81,6 +67,9 @@ export class KclManager {
|
|||||||
get code() {
|
get code() {
|
||||||
return this._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) {
|
set code(code) {
|
||||||
this._code = code
|
this._code = code
|
||||||
this._codeCallBack(code)
|
this._codeCallBack(code)
|
||||||
@ -106,6 +95,12 @@ export class KclManager {
|
|||||||
set programMemory(programMemory) {
|
set programMemory(programMemory) {
|
||||||
this._programMemory = programMemory
|
this._programMemory = programMemory
|
||||||
this._programMemoryCallBack(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() {
|
get logs() {
|
||||||
@ -208,7 +203,6 @@ export class KclManager {
|
|||||||
console.error('error parsing code', e)
|
console.error('error parsing code', e)
|
||||||
if (e instanceof KCLError) {
|
if (e instanceof KCLError) {
|
||||||
this.kclErrors = [e]
|
this.kclErrors = [e]
|
||||||
if (e.msg === 'file is empty') this.engineCommandManager?.endSession()
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -220,8 +214,9 @@ export class KclManager {
|
|||||||
if (this.wasmInitFailed) {
|
if (this.wasmInitFailed) {
|
||||||
this.wasmInitFailed = false
|
this.wasmInitFailed = false
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
this.wasmInitFailed = true
|
this.wasmInitFailed = true
|
||||||
|
throw new Error('error initializing wasm: ' + e.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +236,7 @@ export class KclManager {
|
|||||||
ast,
|
ast,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
})
|
})
|
||||||
enterEditMode(programMemory, this.engineCommandManager)
|
await enterEditMode(programMemory, this.engineCommandManager)
|
||||||
this.isExecuting = false
|
this.isExecuting = false
|
||||||
// Check the cancellation token for this execution before applying side effects
|
// Check the cancellation token for this execution before applying side effects
|
||||||
if (this._cancelTokens.get(currentExecutionId)) {
|
if (this._cancelTokens.get(currentExecutionId)) {
|
||||||
@ -276,7 +271,7 @@ export class KclManager {
|
|||||||
if (!newAst) return
|
if (!newAst) return
|
||||||
await this?.engineCommandManager?.waitForReady
|
await this?.engineCommandManager?.waitForReady
|
||||||
if (updates !== 'none') {
|
if (updates !== 'none') {
|
||||||
this.setCode(recast(ast))
|
this.setCode(newCode)
|
||||||
}
|
}
|
||||||
this._ast = { ...newAst }
|
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() {
|
cancelAllExecutions() {
|
||||||
this._cancelTokens.forEach((_, key) => {
|
this._cancelTokens.forEach((_, key) => {
|
||||||
this._cancelTokens.set(key, true)
|
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) {
|
setCode(code: string, shouldWriteFile = true) {
|
||||||
if (shouldWriteFile) {
|
if (shouldWriteFile) {
|
||||||
// use the normal code setter
|
// use the normal code setter
|
||||||
@ -352,27 +320,6 @@ export class KclManager {
|
|||||||
this._code = code
|
this._code = code
|
||||||
this._codeCallBack(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() {
|
format() {
|
||||||
const ast = this.safeParse(this.code)
|
const ast = this.safeParse(this.code)
|
||||||
if (!ast) return
|
if (!ast) return
|
||||||
@ -440,6 +387,10 @@ export class KclManager {
|
|||||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async enterEditMode() {
|
||||||
|
await enterEditMode(this.programMemory, this.engineCommandManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeLSGetItem(key: string) {
|
function safeLSGetItem(key: string) {
|
||||||
@ -452,7 +403,7 @@ function safteLSSetItem(key: string, value: string) {
|
|||||||
localStorage?.setItem(key, value)
|
localStorage?.setItem(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function enterEditMode(
|
async function enterEditMode(
|
||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
) {
|
) {
|
||||||
@ -460,7 +411,7 @@ function enterEditMode(
|
|||||||
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
|
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
|
||||||
) as SketchGroup | ExtrudeGroup
|
) as SketchGroup | ExtrudeGroup
|
||||||
firstSketchOrExtrudeGroup &&
|
firstSketchOrExtrudeGroup &&
|
||||||
engineCommandManager.sendSceneCommand({
|
(await engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_batch_req',
|
type: 'modeling_cmd_batch_req',
|
||||||
batch_id: uuidv4(),
|
batch_id: uuidv4(),
|
||||||
requests: [
|
requests: [
|
||||||
@ -479,5 +430,5 @@ function enterEditMode(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { kclErrToDiagnostic, KCLError } from './errors'
|
import { kclErrorsToDiagnostics, KCLError } from './errors'
|
||||||
|
|
||||||
describe('test kclErrToDiagnostic', () => {
|
describe('test kclErrToDiagnostic', () => {
|
||||||
it('converts KCL errors to CodeMirror diagnostics', () => {
|
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([
|
expect(diagnostics).toEqual([
|
||||||
{
|
{
|
||||||
from: 0,
|
from: 0,
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Diagnostic } from '@codemirror/lint'
|
|
||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
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
|
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||||
export class KCLError {
|
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.
|
* Maps the KCL errors to an array of CodeMirror diagnostics.
|
||||||
* Currently the diagnostics are all errors, but in the future they could include lints.
|
* 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 errors?.flatMap((err) => {
|
||||||
return err.sourceRanges.map(([from, to]) => {
|
return err.sourceRanges.map(([from, to]) => {
|
||||||
return { from, to, message: err.msg, severity: 'error' }
|
return { from, to, message: err.msg, severity: 'error' }
|
||||||
|
@ -241,7 +241,6 @@ export function extrudeSketch(
|
|||||||
pathToExtrudeArg: PathToNode
|
pathToExtrudeArg: PathToNode
|
||||||
} {
|
} {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const dumbyStartend = { start: 0, end: 0 }
|
|
||||||
const { node: sketchExpression } = getNodeFromPath(
|
const { node: sketchExpression } = getNodeFromPath(
|
||||||
_node,
|
_node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -256,18 +255,14 @@ export function extrudeSketch(
|
|||||||
)
|
)
|
||||||
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
||||||
|
|
||||||
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
const { node: variableDeclarator, shallowPath: pathToDecleration } =
|
||||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||||
|
|
||||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
distance,
|
distance,
|
||||||
shouldPipe
|
shouldPipe
|
||||||
? createPipeSubstitution()
|
? createPipeSubstitution()
|
||||||
: {
|
: createIdentifier(variableDeclarator.id.name),
|
||||||
type: 'Identifier',
|
|
||||||
...dumbyStartend,
|
|
||||||
name: variableDeclorator.id.name,
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
|
|
||||||
if (shouldPipe) {
|
if (shouldPipe) {
|
||||||
@ -277,7 +272,7 @@ export function extrudeSketch(
|
|||||||
: [sketchExpression as any, extrudeCall]
|
: [sketchExpression as any, extrudeCall]
|
||||||
)
|
)
|
||||||
|
|
||||||
variableDeclorator.init = pipeChain
|
variableDeclarator.init = pipeChain
|
||||||
const pathToExtrudeArg: PathToNode = [
|
const pathToExtrudeArg: PathToNode = [
|
||||||
...pathToDecleration,
|
...pathToDecleration,
|
||||||
['init', 'VariableDeclarator'],
|
['init', 'VariableDeclarator'],
|
||||||
|
@ -5,6 +5,7 @@ import { exportSave } from 'lib/exportSave'
|
|||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { Themes, getThemeColorForEngine } from 'lib/theme'
|
import { Themes, getThemeColorForEngine } from 'lib/theme'
|
||||||
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
|
|
||||||
let lastMessage = ''
|
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 {
|
interface ResultCommand extends CommandInfo {
|
||||||
type: 'result'
|
type: 'result'
|
||||||
@ -37,10 +39,19 @@ interface FailedCommand extends CommandInfo {
|
|||||||
type: 'failed'
|
type: 'failed'
|
||||||
errors: Models['FailureWebSocketResponse_type']['errors']
|
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 {
|
interface PendingCommand extends CommandInfo {
|
||||||
type: 'pending'
|
type: 'pending'
|
||||||
promise: Promise<any>
|
promise: Promise<any>
|
||||||
resolve: (val: any) => void
|
resolve: (val: ResolveCommand) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtifactMap {
|
export interface ArtifactMap {
|
||||||
@ -59,6 +70,15 @@ type Timeout = ReturnType<typeof setTimeout>
|
|||||||
|
|
||||||
type ClientMetrics = Models['ClientMetrics_type']
|
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 Value<T, U> = U extends undefined
|
||||||
? { type: T; value: U }
|
? { type: T; value: U }
|
||||||
: U extends void
|
: U extends void
|
||||||
@ -224,7 +244,7 @@ class EngineConnection {
|
|||||||
private onNewTrack: (track: NewTrackArgs) => void
|
private onNewTrack: (track: NewTrackArgs) => void
|
||||||
|
|
||||||
// TODO: actual type is ClientMetrics
|
// TODO: actual type is ClientMetrics
|
||||||
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
public webrtcStatsCollector?: () => Promise<WebRTCClientMetrics>
|
||||||
private engineCommandManager: EngineCommandManager
|
private engineCommandManager: EngineCommandManager
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@ -396,7 +416,7 @@ class EngineConnection {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
this.webrtcStatsCollector = (): Promise<WebRTCClientMetrics> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (mediaStream.getVideoTracks().length !== 1) {
|
if (mediaStream.getVideoTracks().length !== 1) {
|
||||||
reject(new Error('too many video tracks to report'))
|
reject(new Error('too many video tracks to report'))
|
||||||
@ -405,7 +425,7 @@ class EngineConnection {
|
|||||||
|
|
||||||
let videoTrack = mediaStream.getVideoTracks()[0]
|
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||||
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||||
let client_metrics: ClientMetrics = {
|
let client_metrics: WebRTCClientMetrics = {
|
||||||
rtc_frames_decoded: 0,
|
rtc_frames_decoded: 0,
|
||||||
rtc_frames_dropped: 0,
|
rtc_frames_dropped: 0,
|
||||||
rtc_frames_received: 0,
|
rtc_frames_received: 0,
|
||||||
@ -414,6 +434,12 @@ class EngineConnection {
|
|||||||
rtc_jitter_sec: 0.0,
|
rtc_jitter_sec: 0.0,
|
||||||
rtc_keyframes_decoded: 0,
|
rtc_keyframes_decoded: 0,
|
||||||
rtc_total_freezes_duration_sec: 0.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
|
// TODO(paultag): Since we can technically have multiple WebRTC
|
||||||
@ -439,6 +465,13 @@ class EngineConnection {
|
|||||||
videoTrackReport.keyFramesDecoded || 0
|
videoTrackReport.keyFramesDecoded || 0
|
||||||
client_metrics.rtc_total_freezes_duration_sec =
|
client_metrics.rtc_total_freezes_duration_sec =
|
||||||
videoTrackReport.totalFreezesDuration || 0
|
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') {
|
} else if (videoTrackReport.type === 'transport') {
|
||||||
// videoTrackReport.bytesReceived,
|
// videoTrackReport.bytesReceived,
|
||||||
// videoTrackReport.bytesSent,
|
// videoTrackReport.bytesSent,
|
||||||
@ -784,7 +817,6 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
this.webrtcStatsCollector = undefined
|
this.webrtcStatsCollector = undefined
|
||||||
}
|
}
|
||||||
finalizeIfAllConnectionsClosed() {
|
finalizeIfAllConnectionsClosed() {
|
||||||
console.log(this.websocket, this.pc, this.unreliableDataChannel)
|
|
||||||
const allClosed =
|
const allClosed =
|
||||||
this.websocket?.readyState === 3 &&
|
this.websocket?.readyState === 3 &&
|
||||||
this.pc?.connectionState === 'closed' &&
|
this.pc?.connectionState === 'closed' &&
|
||||||
@ -827,7 +859,7 @@ export type CommandLog =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'receive-reliable'
|
type: 'receive-reliable'
|
||||||
data: WebSocketResponse
|
data: OkWebSocketResponseData
|
||||||
id: string
|
id: string
|
||||||
cmd_type?: string
|
cmd_type?: string
|
||||||
}
|
}
|
||||||
@ -843,8 +875,8 @@ export class EngineCommandManager {
|
|||||||
outSequence = 1
|
outSequence = 1
|
||||||
inSequence = 1
|
inSequence = 1
|
||||||
engineConnection?: EngineConnection
|
engineConnection?: EngineConnection
|
||||||
defaultPlanes: { xy: string; yz: string; xz: string } | null = null
|
defaultPlanes: DefaultPlanes | null = null
|
||||||
_commandLogs: CommandLog[] = []
|
commandLogs: CommandLog[] = []
|
||||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||||
// Folks should realize that wait for ready does not get called _everytime_
|
// Folks should realize that wait for ready does not get called _everytime_
|
||||||
// the connection resets and restarts, it only gets called the first time.
|
// the connection resets and restarts, it only gets called the first time.
|
||||||
@ -882,24 +914,26 @@ export class EngineCommandManager {
|
|||||||
set getAstCb(cb: () => Program) {
|
set getAstCb(cb: () => Program) {
|
||||||
this.getAst = cb
|
this.getAst = cb
|
||||||
}
|
}
|
||||||
|
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||||
|
|
||||||
start({
|
start({
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setIsStreamReady,
|
setIsStreamReady,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
executeCode,
|
|
||||||
token,
|
token,
|
||||||
|
makeDefaultPlanes,
|
||||||
theme = Themes.Dark,
|
theme = Themes.Dark,
|
||||||
}: {
|
}: {
|
||||||
setMediaStream: (stream: MediaStream) => void
|
setMediaStream: (stream: MediaStream) => void
|
||||||
setIsStreamReady: (isStreamReady: boolean) => void
|
setIsStreamReady: (isStreamReady: boolean) => void
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
executeCode: (code?: string, force?: boolean) => void
|
|
||||||
token?: string
|
token?: string
|
||||||
|
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||||
theme?: Themes
|
theme?: Themes
|
||||||
}) {
|
}) {
|
||||||
|
this.makeDefaultPlanes = makeDefaultPlanes
|
||||||
if (width === 0 || height === 0) {
|
if (width === 0 || height === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -969,7 +1003,6 @@ export class EngineCommandManager {
|
|||||||
this.initPlanes().then(() => {
|
this.initPlanes().then(() => {
|
||||||
this.resolveReady()
|
this.resolveReady()
|
||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
executeCode(undefined, true)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
@ -1020,7 +1053,11 @@ export class EngineCommandManager {
|
|||||||
message.resp.type === 'modeling' &&
|
message.resp.type === 'modeling' &&
|
||||||
message.request_id
|
message.request_id
|
||||||
) {
|
) {
|
||||||
this.handleModelingCommand(message.resp, message.request_id)
|
this.handleModelingCommand(
|
||||||
|
message.resp,
|
||||||
|
message.request_id,
|
||||||
|
message
|
||||||
|
)
|
||||||
} else if (
|
} else if (
|
||||||
!message.success &&
|
!message.success &&
|
||||||
message.request_id &&
|
message.request_id &&
|
||||||
@ -1069,7 +1106,11 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
this.engineConnection?.send(resizeCmd)
|
this.engineConnection?.send(resizeCmd)
|
||||||
}
|
}
|
||||||
handleModelingCommand(message: WebSocketResponse, id: string) {
|
handleModelingCommand(
|
||||||
|
message: OkWebSocketResponseData,
|
||||||
|
id: string,
|
||||||
|
raw: WebSocketResponse
|
||||||
|
) {
|
||||||
if (message.type !== 'modeling') {
|
if (message.type !== 'modeling') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1081,7 +1122,7 @@ export class EngineCommandManager {
|
|||||||
command?.additionalData?.type === 'batch-ids'
|
command?.additionalData?.type === 'batch-ids'
|
||||||
) {
|
) {
|
||||||
command.additionalData.ids.forEach((id) => {
|
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
|
// batch artifact is just a container, we don't need to keep it
|
||||||
// once we process all the commands inside it
|
// once we process all the commands inside it
|
||||||
@ -1092,7 +1133,7 @@ export class EngineCommandManager {
|
|||||||
commandType: command.commandType,
|
commandType: command.commandType,
|
||||||
range: command.range,
|
range: command.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
raw: message,
|
raw,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1116,7 +1157,7 @@ export class EngineCommandManager {
|
|||||||
commandType: command.commandType,
|
commandType: command.commandType,
|
||||||
parentId: command.parentId ? command.parentId : undefined,
|
parentId: command.parentId ? command.parentId : undefined,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
raw: message,
|
raw,
|
||||||
} as const
|
} as const
|
||||||
this.artifactMap[id] = artifact
|
this.artifactMap[id] = artifact
|
||||||
if (
|
if (
|
||||||
@ -1161,7 +1202,7 @@ export class EngineCommandManager {
|
|||||||
commandType: command.commandType,
|
commandType: command.commandType,
|
||||||
range: command.range,
|
range: command.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
raw: message,
|
raw,
|
||||||
})
|
})
|
||||||
} else if (sceneCommand && sceneCommand.type === 'pending') {
|
} else if (sceneCommand && sceneCommand.type === 'pending') {
|
||||||
const resolve = sceneCommand.resolve
|
const resolve = sceneCommand.resolve
|
||||||
@ -1172,7 +1213,7 @@ export class EngineCommandManager {
|
|||||||
commandType: sceneCommand.commandType,
|
commandType: sceneCommand.commandType,
|
||||||
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
|
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
raw: message,
|
raw,
|
||||||
} as const
|
} as const
|
||||||
this.sceneCommandArtifacts[id] = artifact
|
this.sceneCommandArtifacts[id] = artifact
|
||||||
resolve({
|
resolve({
|
||||||
@ -1180,6 +1221,7 @@ export class EngineCommandManager {
|
|||||||
commandType: sceneCommand.commandType,
|
commandType: sceneCommand.commandType,
|
||||||
range: sceneCommand.range,
|
range: sceneCommand.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
|
raw,
|
||||||
})
|
})
|
||||||
} else if (command) {
|
} else if (command) {
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
@ -1188,7 +1230,7 @@ export class EngineCommandManager {
|
|||||||
range: command?.range,
|
range: command?.range,
|
||||||
pathToNode: command?.pathToNode,
|
pathToNode: command?.pathToNode,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
raw: message,
|
raw,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sceneCommandArtifacts[id] = {
|
this.sceneCommandArtifacts[id] = {
|
||||||
@ -1197,15 +1239,14 @@ export class EngineCommandManager {
|
|||||||
range: sceneCommand?.range,
|
range: sceneCommand?.range,
|
||||||
pathToNode: sceneCommand?.pathToNode,
|
pathToNode: sceneCommand?.pathToNode,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
raw: message,
|
raw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleFailedModelingCommand({
|
handleFailedModelingCommand(raw: WebSocketResponse) {
|
||||||
request_id,
|
const id = raw.request_id
|
||||||
errors,
|
const failed = raw as Models['FailureWebSocketResponse_type']
|
||||||
}: Models['FailureWebSocketResponse_type']) {
|
const errors = failed.errors
|
||||||
const id = request_id
|
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const command = this.artifactMap[id]
|
const command = this.artifactMap[id]
|
||||||
if (command && command.type === 'pending') {
|
if (command && command.type === 'pending') {
|
||||||
@ -1223,6 +1264,7 @@ export class EngineCommandManager {
|
|||||||
commandType: command.commandType,
|
commandType: command.commandType,
|
||||||
range: command.range,
|
range: command.range,
|
||||||
errors,
|
errors,
|
||||||
|
raw,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
@ -1238,10 +1280,10 @@ export class EngineCommandManager {
|
|||||||
tearDown() {
|
tearDown() {
|
||||||
this.engineConnection?.tearDown()
|
this.engineConnection?.tearDown()
|
||||||
}
|
}
|
||||||
startNewSession() {
|
async startNewSession() {
|
||||||
this.lastArtifactMap = this.artifactMap
|
this.lastArtifactMap = this.artifactMap
|
||||||
this.artifactMap = {}
|
this.artifactMap = {}
|
||||||
this.initPlanes()
|
await this.initPlanes()
|
||||||
}
|
}
|
||||||
subscribeTo<T extends ModelTypes>({
|
subscribeTo<T extends ModelTypes>({
|
||||||
event,
|
event,
|
||||||
@ -1285,6 +1327,16 @@ export class EngineCommandManager {
|
|||||||
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
|
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
|
||||||
this.callbacksEngineStateConnection.push(callback)
|
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() {
|
endSession() {
|
||||||
const deleteCmd: EngineCommand = {
|
const deleteCmd: EngineCommand = {
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -1293,20 +1345,20 @@ export class EngineCommandManager {
|
|||||||
type: 'scene_clear_all',
|
type: 'scene_clear_all',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
this.defaultPlanes = null
|
this.clearDefaultPlanes()
|
||||||
this.engineConnection?.send(deleteCmd)
|
this.engineConnection?.send(deleteCmd)
|
||||||
}
|
}
|
||||||
addCommandLog(message: CommandLog) {
|
addCommandLog(message: CommandLog) {
|
||||||
if (this._commandLogs.length > 500) {
|
if (this.commandLogs.length > 500) {
|
||||||
this._commandLogs.shift()
|
this.commandLogs.shift()
|
||||||
}
|
}
|
||||||
this._commandLogs.push(message)
|
this.commandLogs.push(message)
|
||||||
|
|
||||||
this._commandLogCallBack([...this._commandLogs])
|
this._commandLogCallBack([...this.commandLogs])
|
||||||
}
|
}
|
||||||
clearCommandLogs() {
|
clearCommandLogs() {
|
||||||
this._commandLogs = []
|
this.commandLogs = []
|
||||||
this._commandLogCallBack(this._commandLogs)
|
this._commandLogCallBack(this.commandLogs)
|
||||||
}
|
}
|
||||||
registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
|
registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
|
||||||
this._commandLogCallBack = callback
|
this._commandLogCallBack = callback
|
||||||
@ -1573,7 +1625,14 @@ export class EngineCommandManager {
|
|||||||
command: commandStr,
|
command: commandStr,
|
||||||
ast: this.getAst(),
|
ast: this.getAst(),
|
||||||
idToRangeMap,
|
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> {
|
commandResult(id: string): Promise<any> {
|
||||||
const command = this.artifactMap[id]
|
const command = this.artifactMap[id]
|
||||||
@ -1602,30 +1661,15 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
private async initPlanes() {
|
private async initPlanes() {
|
||||||
if (this.planesInitialized()) return
|
if (this.planesInitialized()) return
|
||||||
const [xy, yz, xz] = [
|
const planes = await this.makeDefaultPlanes()
|
||||||
await this.createPlane({
|
this.defaultPlanes = planes
|
||||||
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 }
|
|
||||||
|
|
||||||
this.subscribeTo({
|
this.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: ({ data }) => {
|
callback: ({ data }) => {
|
||||||
if (!data?.entity_id) return
|
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)
|
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,
|
ServerConfig,
|
||||||
copilot_lsp_run,
|
copilot_lsp_run,
|
||||||
kcl_lsp_run,
|
kcl_lsp_run,
|
||||||
|
make_default_planes,
|
||||||
|
coredump,
|
||||||
} from '../wasm-lib/pkg/wasm_lib'
|
} from '../wasm-lib/pkg/wasm_lib'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
@ -21,6 +23,10 @@ import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
|||||||
import { Coords2d } from './std/sketch'
|
import { Coords2d } from './std/sketch'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import { DEV } from 'env'
|
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 { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
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[] {
|
export function lexer(str: string): Token[] {
|
||||||
try {
|
try {
|
||||||
const tokens: Token[] = lexer_wasm(str)
|
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 {
|
try {
|
||||||
console.log('start kcl lsp')
|
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) {
|
} catch (e: any) {
|
||||||
console.log('kcl lsp failed', e)
|
console.log('kcl lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { buildCommandArgument } from 'lib/createMachineCommand'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
|
|
||||||
// An array of the paths to all of the settings that have commandConfigs
|
// An array of the paths to all of the settings that have commandConfigs
|
||||||
export const settingsWithCommandConfigs = (
|
export const settingsWithCommandConfigs = (
|
||||||
@ -87,11 +88,34 @@ export function createSettingsCommand({
|
|||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const valueArgConfig = {
|
let valueArgConfig = {
|
||||||
...valueArgPartialConfig,
|
...valueArgPartialConfig,
|
||||||
required: true,
|
required: true,
|
||||||
} as CommandArgumentConfig<S['default']>
|
} 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
|
// @ts-ignore - TODO figure out this typing for valueArgConfig
|
||||||
const valueArg = buildCommandArgument(valueArgConfig, context, actor)
|
const valueArg = buildCommandArgument(valueArgConfig, context, actor)
|
||||||
|
|
||||||
|
@ -151,7 +151,8 @@ export type CommandArgumentConfig<
|
|||||||
defaultValue?:
|
defaultValue?:
|
||||||
| OutputType
|
| OutputType
|
||||||
| ((
|
| ((
|
||||||
commandBarContext: ContextFrom<typeof commandBarMachine>
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: C
|
||||||
) => OutputType)
|
) => OutputType)
|
||||||
defaultValueFromContext?: (context: 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, %)
|
|> extrude(width, %)
|
||||||
|> fillet({
|
|> fillet({
|
||||||
radius: filletR,
|
radius: filletR,
|
||||||
tags: [getNextAdjacentEdge('innerEdge', %)]
|
tags: [getPreviousAdjacentEdge('innerEdge', %)]
|
||||||
}, %)
|
}, %)
|
||||||
|> fillet({
|
|> fillet({
|
||||||
radius: filletR + thickness,
|
radius: filletR + thickness,
|
||||||
tags: [getNextAdjacentEdge('outerEdge', %)]
|
tags: [getPreviousAdjacentEdge('outerEdge', %)]
|
||||||
}, %)`
|
}, %)`
|
||||||
|
|
||||||
function findLineInExampleCode({
|
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', {
|
const children = await invoke<FileEntry[]>('read_dir_recursive', {
|
||||||
path: projectPath,
|
path: projectPath,
|
||||||
})
|
})
|
||||||
kclManager.setCodeAndExecute(code, false)
|
kclManager.setCode(code, false)
|
||||||
|
|
||||||
// Set the file system manager to the project path
|
// Set the file system manager to the project path
|
||||||
// So that WASM gets an updated path for operations
|
// 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',
|
description: 'The hue of the primary theme color for the app',
|
||||||
validate: (v) => Number(v) >= 0 && Number(v) < 360,
|
validate: (v) => Number(v) >= 0 && Number(v) < 360,
|
||||||
Component: ({ value, updateValue }) => (
|
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
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
onChange={(e) => updateValue(e.currentTarget.value)}
|
onChange={(e) => updateValue(e.currentTarget.value)}
|
||||||
@ -147,13 +153,6 @@ export function createSettings() {
|
|||||||
step={1}
|
step={1}
|
||||||
className="block flex-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>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
@ -362,6 +361,17 @@ export function createSettings() {
|
|||||||
inputType: 'boolean',
|
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.
|
* Settings that affect the behavior of project management.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { type Models } from '@kittycad/lib'
|
import { type Models } from '@kittycad/lib'
|
||||||
import { Setting, settings } from './initialSettings'
|
import { Setting, settings } from './initialSettings'
|
||||||
import { AtLeast, PathValue, Paths } from 'lib/types'
|
import { AtLeast, PathValue, Paths } from 'lib/types'
|
||||||
import { ChangeEventHandler } from 'react'
|
|
||||||
import { CommandArgumentConfig } from 'lib/commandTypes'
|
import { CommandArgumentConfig } from 'lib/commandTypes'
|
||||||
|
|
||||||
export enum UnitSystem {
|
export enum UnitSystem {
|
||||||
|
@ -5,8 +5,19 @@ import {
|
|||||||
} from '../lang/std/engineConnection'
|
} from '../lang/std/engineConnection'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { Themes } from './theme'
|
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 {
|
class MockEngineCommandManager {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
@ -27,13 +38,19 @@ class MockEngineCommandManager {
|
|||||||
command: EngineCommand
|
command: EngineCommand
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
const response: WebSocketResponse = {
|
const response: WebSocketResponse = {
|
||||||
type: 'modeling',
|
success: true,
|
||||||
data: {
|
resp: {
|
||||||
modeling_response: { type: 'empty' },
|
type: 'modeling',
|
||||||
|
data: {
|
||||||
|
modeling_response: { type: 'empty' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return Promise.resolve(JSON.stringify(response))
|
return Promise.resolve(JSON.stringify(response))
|
||||||
}
|
}
|
||||||
|
async wasmGetDefaultPlanes(): Promise<string> {
|
||||||
|
return JSON.stringify(defaultPlanes)
|
||||||
|
}
|
||||||
sendModelingCommandFromWasm(
|
sendModelingCommandFromWasm(
|
||||||
id: string,
|
id: string,
|
||||||
rangeStr: string,
|
rangeStr: string,
|
||||||
@ -81,8 +98,10 @@ export async function executor(
|
|||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
executeCode: () => {},
|
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
|
makeDefaultPlanes: () => {
|
||||||
|
return new Promise((resolve) => resolve(defaultPlanes))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await engineCommandManager.waitForReady
|
await engineCommandManager.waitForReady
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
|
@ -26,7 +26,7 @@ export function setThemeClass(theme: Themes) {
|
|||||||
export function getThemeColorForEngine(theme: Themes) {
|
export function getThemeColorForEngine(theme: Themes) {
|
||||||
const resolvedTheme = theme === Themes.System ? getSystemTheme() : theme
|
const resolvedTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
const dark = 28 / 255
|
const dark = 28 / 255
|
||||||
const light = 242 / 255
|
const light = 249 / 255
|
||||||
return resolvedTheme === Themes.Dark
|
return resolvedTheme === Themes.Dark
|
||||||
? { r: dark, g: dark, b: dark, a: 1 }
|
? { r: dark, g: dark, b: dark, a: 1 }
|
||||||
: { r: light, g: light, b: light, a: 1 }
|
: { r: light, g: light, b: light, a: 1 }
|
||||||
|