Compare commits
116 Commits
kcl-0.2.10
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
c03357d897 | |||
292f89859f | |||
a00800bddc | |||
78ceba6d20 | |||
6776a350af | |||
dd75f06f77 | |||
394872d84e | |||
f9eef6397f | |||
900bac999c | |||
5b2738f826 | |||
dab96577a7 | |||
25443eba31 | |||
0a72d7a39a | |||
5f8d4f8294 | |||
7c2cfba0ac | |||
5ee43bda22 | |||
a1b6bbac7e | |||
e61516f3c3 | |||
e2eeec37ad | |||
d7fcc128aa | |||
cf266b17c1 | |||
b3a1796da9 | |||
39b9a6b2c4 | |||
6ba4fa305c | |||
1d043899c8 | |||
cb8a087d89 | |||
f2eb7b57b8 | |||
eba653930f | |||
3deb5c689a | |||
11ebe11111 | |||
9538ffb8ec | |||
55d1da226f | |||
2bfde64bf1 | |||
7cb9a2efd9 | |||
57e85d7fd0 | |||
ca4a442cce | |||
46eef39d53 | |||
dbc5f7b11f | |||
6797331c9d | |||
cc80a2da3d | |||
54fb9c903a | |||
e63597458a | |||
e15c38fa23 | |||
906ca65611 | |||
805b9f48e5 | |||
a762d741a5 | |||
4b8ca7f61f | |||
31b0a8af12 | |||
74b4cb9e08 | |||
e7c6dd3698 | |||
aa9abbe83f | |||
b19f3bbdb0 | |||
892e856471 | |||
84fae12cdd | |||
3d67781039 | |||
114c3a2580 | |||
02b4aa0476 | |||
57f4e1b79c | |||
35f9b82a65 | |||
cbddb3553d | |||
dd754c78ab | |||
150f56b47a | |||
0eef6ab7d3 | |||
91d3ba3fce | |||
7165aa1b41 | |||
3cbda10eab | |||
0f3432b5a0 | |||
f11dc07f0b | |||
e49beb6609 | |||
b8f27b77a8 | |||
fa7e31223d | |||
f04c4588df | |||
c95812efa6 | |||
96385cd5ee | |||
64707edaad | |||
27baf135e7 | |||
a4cf68c661 | |||
403e074249 | |||
50259aa052 | |||
1739f3dafe | |||
7ceb518446 | |||
36a6b8c0ea | |||
bbdca7421e | |||
03c6f6d60e | |||
18c7e7934a | |||
bf650fd129 | |||
81ccb65f15 | |||
335b5100ae | |||
1162ff3b03 | |||
5e8227ead8 | |||
ed339a6b9a | |||
1d19fc6b7e | |||
5b5355376f | |||
5c90f72c91 | |||
026a8d19cb | |||
6dd0981709 | |||
b231a26115 | |||
3f47486fb5 | |||
57e97d16d0 | |||
dbdc7e5c8b | |||
f6bb10170d | |||
972dca8743 | |||
e9e933eecd | |||
2b1315423f | |||
bd4c24bc04 | |||
50cc88977c | |||
bea9a1c3ec | |||
f43411fdb4 | |||
c2e9d18f92 | |||
199722c505 | |||
f9699d174c | |||
590a6479e0 | |||
fbf0d3d953 | |||
3dd66bc8d2 | |||
a928b8fbd0 | |||
d2349bec2b |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas,.yarn.lock,**/yarn.lock
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock
|
||||
|
@ -2,7 +2,9 @@ NODE_ENV=development
|
||||
DEV=true
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||
BASE_URL=https://api.dev.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!
|
||||
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||
|
@ -13,6 +13,8 @@
|
||||
"plugin:css-modules/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
@ -24,7 +26,6 @@
|
||||
{
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"suggest-no-throw/suggest-no-throw": "off",
|
||||
"testing-library/prefer-screen-queries": "off",
|
||||
"jest/valid-expect": "off"
|
||||
|
357
.github/workflows/build-test-publish-apps.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: build-test-publish-apps
|
||||
name: build-publish-apps
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -21,7 +21,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-json-files:
|
||||
prepare-files:
|
||||
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
@ -33,6 +33,19 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm"
|
||||
|
||||
- name: Set nightly version
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
@ -42,36 +55,48 @@ jobs:
|
||||
# TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
name: prepared-files
|
||||
path: |
|
||||
package.json
|
||||
src/wasm-lib/pkg/wasm_lib*
|
||||
|
||||
- id: export_version
|
||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
build-test-app-macos:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: macos-14
|
||||
build-apps:
|
||||
needs: [prepare-files]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-14, windows-2022, ubuntu-22.04]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
name: prepared-files
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
- name: Copy prepared files
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
ls -R prepared-files
|
||||
cp prepared-files/package.json package.json
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
mkdir src/wasm-lib/pkg
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
@ -81,79 +106,10 @@ jobs:
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
|
||||
|
||||
# TODO: sign the app (and updater bundle potentially)
|
||||
- name: Add signing certificate
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make"
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*/*"
|
||||
|
||||
|
||||
build-test-app-windows:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm manually
|
||||
shell: bash
|
||||
env:
|
||||
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
|
||||
run: |
|
||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||
echo "building with ${{ env.MODE }}"
|
||||
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
- run: yarn tronb:vite
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
@ -168,7 +124,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Setup certicate with SSM KSP (Windows only)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||
@ -178,83 +134,22 @@ jobs:
|
||||
smksp_cert_sync.exe
|
||||
shell: cmd
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
- name: Build the app
|
||||
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make --arch arm64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
|
||||
- name: Sign using Signtool
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
env:
|
||||
THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D"
|
||||
X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe"
|
||||
ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe"
|
||||
run: |
|
||||
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}"
|
||||
signtool.exe verify /v /pa "${{ env.X64_FILE }}"
|
||||
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}"
|
||||
signtool.exe verify /v /pa "${{ env.ARM64_FILE }}"
|
||||
- name: List artifacts in out/
|
||||
run: ls -R out
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*"
|
||||
|
||||
# TODO: Run e2e tests
|
||||
|
||||
|
||||
build-test-app-ubuntu:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make --arch arm64"
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
name: out-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*.*
|
||||
out/latest*.yml
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
# TODO: sign the app (and updater bundle potentially)
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*"
|
||||
# TODO: add the updater tests back
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
@ -262,88 +157,76 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu]
|
||||
needs: [prepare-files, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate the update static endpoint
|
||||
run: |
|
||||
ls -l artifact/*/*oo*
|
||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
|
||||
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
jq --null-input \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
|
||||
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
|
||||
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
|
||||
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"darwin-x86_64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"darwin-aarch64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": $windows_x86_64_sig,
|
||||
"url": $windows_x86_64_url
|
||||
},
|
||||
"windows-aarch64": {
|
||||
"signature": $windows_aarch64_sig,
|
||||
"url": $windows_aarch64_url
|
||||
}
|
||||
}
|
||||
}' > last_update.json
|
||||
cat last_update.json
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-windows-2022
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-macos-14
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-ubuntu-22.04
|
||||
path: out
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}
|
||||
jq --null-input \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
|
||||
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
|
||||
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
|
||||
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.exe" \
|
||||
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.exe" \
|
||||
--arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \
|
||||
--arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
"dmg-arm64": {
|
||||
"url": $mac_arm64_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_x86_64_url
|
||||
"dmg-x64": {
|
||||
"url": $mac_x64_url
|
||||
},
|
||||
"msi-aarch64": {
|
||||
"url": $windows_aarch64_url
|
||||
"exe-arm64": {
|
||||
"url": $windows_arm64_url
|
||||
},
|
||||
"exe-x64": {
|
||||
"url": $windows_x64_url
|
||||
},
|
||||
"appimage-arm64": {
|
||||
"url": $linux_arm64_url
|
||||
},
|
||||
"appimage-x64": {
|
||||
"url": $linux_x64_url
|
||||
}
|
||||
}
|
||||
}' > last_download.json
|
||||
cat last_download.json
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out"
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.5'
|
||||
with:
|
||||
@ -352,24 +235,44 @@ jobs:
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2.1.0
|
||||
with:
|
||||
project_id: kittycadapi
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/Zoo*'
|
||||
path: out
|
||||
glob: 'Zoo*'
|
||||
parent: false
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
with:
|
||||
path: last_update.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
|
||||
- name: Upload release files to public bucket (test/electron-builder workaround)
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: out
|
||||
glob: 'Zoo*'
|
||||
parent: false
|
||||
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: out
|
||||
glob: 'latest*'
|
||||
parent: false
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
|
||||
- name: Upload update endpoint to public bucket (test/electron-builder workaround)
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: out
|
||||
glob: 'latest*'
|
||||
parent: false
|
||||
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
@ -378,7 +281,9 @@ jobs:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'artifact/*/Zoo*'
|
||||
files: 'out/Zoo*'
|
||||
|
||||
# TODO: Add GitHub publisher
|
||||
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
|
2
.github/workflows/build-test-web.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
- run: yarn xstate:typegen
|
||||
- run: yarn tsc
|
||||
- name: Lint
|
||||
run: yarn eslint --max-warnings 0 src e2e
|
||||
run: yarn eslint --max-warnings 0 src e2e packages/codemirror-lsp-client
|
||||
|
||||
|
||||
check-typos:
|
||||
|
2
.github/workflows/cargo-check.yml
vendored
@ -37,4 +37,4 @@ jobs:
|
||||
# We specifically want to test the disable-println feature
|
||||
# Since it is not enabled by default, we need to specify it
|
||||
# This is used in kcl-lsp
|
||||
cargo check --all --features disable-println --features pyo3
|
||||
cargo check --all --features disable-println --features pyo3 --features cli
|
||||
|
3
.github/workflows/cargo-clippy.yml
vendored
@ -28,6 +28,7 @@ jobs:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@just
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@ -41,7 +42,7 @@ jobs:
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo clippy --all --tests --benches -- -D warnings
|
||||
just lint
|
||||
# If this fails, run "cargo check" to update Cargo.lock,
|
||||
# then add Cargo.lock to the PR.
|
||||
- name: Check Cargo.lock doesn't need updating
|
||||
|
5
.github/workflows/cargo-test.yml
vendored
@ -38,11 +38,6 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: install dependencies
|
||||
if: matrix.dir == 'src-tauri'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: Install vector
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
|
||||
|
20
.github/workflows/playwright.yml
vendored
@ -139,7 +139,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
@ -174,14 +174,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run playwright/chrome flow (with retries)
|
||||
id: retry
|
||||
@ -244,14 +244,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
@ -262,7 +262,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-14]
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
@ -351,7 +351,7 @@ jobs:
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run electron tests (with retries)
|
||||
id: retry
|
||||
@ -381,7 +381,7 @@ jobs:
|
||||
echo "retried=true" >>$GITHUB_OUTPUT
|
||||
echo "run playwright with last failed tests and retry $retry"
|
||||
if [[ "$IS_UBUNTU" == "true" ]]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
|
||||
else
|
||||
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
|
||||
fi
|
||||
@ -423,14 +423,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: test-results-electron-${{ github.sha }}
|
||||
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-electron-${{ github.sha }}
|
||||
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
6
.gitignore
vendored
@ -54,19 +54,15 @@ e2e/playwright/export-snapshots/*
|
||||
|
||||
## generated files
|
||||
src/**/*.typegen.ts
|
||||
src-tauri/gen
|
||||
|
||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
Mac_App_Distribution.provisionprofile
|
||||
|
||||
*.tsbuildinfo
|
||||
src/wasm-lib/pkg
|
||||
|
||||
venv
|
||||
.vite/
|
||||
|
||||
# electron
|
||||
out/
|
||||
|
||||
src-tauri/target
|
||||
electron-test-projects-dir
|
||||
electron-test-projects-dir-2
|
||||
|
344
Info.plist
Normal file
@ -0,0 +1,344 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.kcl</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>KCL</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.toml</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>TOML</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.gltf</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>glTF</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.glb</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>glb</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.step</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>STEP</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.fbx</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>FBX</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>dev.zoo.sldprt</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Solidworks Part</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.geometry-definition-format</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>OBJ</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.polygon-file-format</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>PLY</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.standard-tesselated-geometry-format</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>STL</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.folder</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Folders</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.kcl</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://zoo.dev/docs/kcl</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.source-code</string>
|
||||
<string>public.data</string>
|
||||
<string>public.text</string>
|
||||
<string>public.plain-text</string>
|
||||
<string>public.3d-content</string>
|
||||
<string>public.script</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>KCL (KittyCAD Language) document</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>kcl</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>text/vnd.zoo.kcl</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.gltf</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.text</string>
|
||||
<string>public.plain-text</string>
|
||||
<string>public.3d-content</string>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Graphics Library Transmission Format (glTF)</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>gltf</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>model/gltf+json</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.glb</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.3d-content</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Graphics Library Transmission Format (glTF) binary</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>glb</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>model/gltf-binary</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.step</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://www.loc.gov/preservation/digital/formats/fdd/fdd000448.shtml</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.3d-content</string>
|
||||
<string>public.text</string>
|
||||
<string>public.plain-text</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>STEP-file, ISO 10303-21</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>step</string>
|
||||
<string>stp</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>model/step</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.sldprt</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://docs.fileformat.com/cad/sldprt/</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.3d-content</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Solidworks Part</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>sldprt</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>model/vnd.solidworks.sldprt</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.fbx</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://en.wikipedia.org/wiki/FBX</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.3d-content</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Autodesk Filmbox (FBX) format</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>fbx</string>
|
||||
<string>fbxb</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>model/vnd.autodesk.fbx</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>dev.zoo.toml</string>
|
||||
<key>UTTypeReferenceURL</key>
|
||||
<string>https://toml.io/en/</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.text</string>
|
||||
<string>public.plain-text</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Tom's Obvious Minimal Language</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>kcl</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>text/toml</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
8
Makefile
@ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
|
||||
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
|
||||
yarn start
|
||||
|
||||
# I'm sorry this is so specific to my setup you may as well ignore this.
|
||||
# This is so you don't have to deal with electron windows popping up constantly.
|
||||
# It should work for you other Linux users.
|
||||
lee-electron-test:
|
||||
Xephyr -br -ac -noreset -screen 1200x500 :2 &
|
||||
DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree"
|
||||
killall Xephyr
|
||||
|
||||
$(XSTATE_TYPEGENS): $(TS_SRC)
|
||||
yarn xstate typegen 'src/**/*.ts?(x)'
|
||||
|
||||
|
50
README.md
@ -110,7 +110,6 @@ Which commands from setup are one off vs need to be run every time?
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
```bash
|
||||
yarn install
|
||||
yarn wasm-prep
|
||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||
```
|
||||
@ -189,12 +188,22 @@ For more information on fuzzing you can check out
|
||||
|
||||
### Playwright tests
|
||||
|
||||
You will need a `./e2e/playwright/playwright-secrets.env` file:
|
||||
|
||||
```bash
|
||||
$ touch ./e2e/playwright/playwright-secrets.env
|
||||
$ cat ./e2e/playwright/playwright-secrets.env
|
||||
token=<dev.zoo.dev/account/api-tokens>
|
||||
snapshottoken=<your-snapshot-token>
|
||||
```
|
||||
|
||||
For a portable way to run Playwright you'll need Docker.
|
||||
|
||||
#### Generic example
|
||||
After that, open a terminal and run:
|
||||
|
||||
```bash
|
||||
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
|
||||
docker run --network host --rm --init -it playwright/chrome:playwright-x.xx.x
|
||||
```
|
||||
|
||||
and in another terminal, run:
|
||||
@ -203,21 +212,27 @@ and in another terminal, run:
|
||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
|
||||
```
|
||||
|
||||
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
|
||||
|
||||
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE:
|
||||
#### Specific example
|
||||
|
||||
open a terminal and run:
|
||||
|
||||
```bash
|
||||
# ./e2e/playwright/playwright-secrets.env
|
||||
token=<your-token>
|
||||
snapshottoken=<your-snapshot-token>
|
||||
docker run --network host --rm --init -it playwright/chrome:playwright-1.46.0
|
||||
```
|
||||
|
||||
and in another terminal, run:
|
||||
|
||||
```bash
|
||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" e2e/playwright/command-bar-tests.spec.ts
|
||||
```
|
||||
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
|
||||
|
||||
run a specific test change the test from `test('...` to `test.only('...`
|
||||
(note if you commit this, the tests will instantly fail without running any of the tests)
|
||||
|
||||
|
||||
**Gotcha**: running the docker container with a mismatched image against your `./node_modules/playwright` will cause a failure. Make sure the versions are matched and up to date.
|
||||
|
||||
run headed
|
||||
|
||||
```
|
||||
@ -336,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
|
||||
</details>
|
||||
|
||||
### Tauri e2e tests
|
||||
|
||||
#### Windows (local only until the CI edge version mismatch is fixed)
|
||||
|
||||
```
|
||||
yarn install
|
||||
yarn build:wasm-dev
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
yarn vite build --mode development
|
||||
yarn tauri build --debug -b
|
||||
$env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
|
||||
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
|
||||
$env:E2E_TAURI_ENABLED="true"
|
||||
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
|
||||
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
|
||||
Stop-Process -Name msedgedriver
|
||||
yarn wdio run wdio.conf.ts
|
||||
```
|
||||
|
||||
## KCL
|
||||
|
||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
||||
|
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
|
||||
|
||||
KEY_CHAIN=build.keychain
|
||||
CERTIFICATE_P12=certificate.p12
|
||||
|
||||
# Recreate the certificate from the secure environment variable
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12
|
||||
|
||||
#create a keychain
|
||||
security create-keychain -p actions $KEY_CHAIN
|
||||
|
||||
# Make the keychain the default so identities are found
|
||||
security default-keychain -s $KEY_CHAIN
|
||||
|
||||
# Unlock the keychain
|
||||
security unlock-keychain -p actions $KEY_CHAIN
|
||||
|
||||
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
|
||||
|
||||
# remove certs
|
||||
rm -fr *.p12
|
@ -24,6 +24,3 @@ once fixed in engine will just start working here with no language changes.
|
||||
chamfer cases work currently.
|
||||
|
||||
Sketching on the chamfered face does not currently work.
|
||||
|
||||
- **Shell**: Shell sometimes does not work when arcs or fillets are involved.
|
||||
We are tracking the engine side bug on this.
|
||||
|
@ -56,6 +56,7 @@ layout: manual
|
||||
* [`line`](kcl/line)
|
||||
* [`lineTo`](kcl/lineTo)
|
||||
* [`ln`](kcl/ln)
|
||||
* [`loft`](kcl/loft)
|
||||
* [`log`](kcl/log)
|
||||
* [`log10`](kcl/log10)
|
||||
* [`log2`](kcl/log2)
|
||||
@ -63,6 +64,7 @@ layout: manual
|
||||
* [`max`](kcl/max)
|
||||
* [`min`](kcl/min)
|
||||
* [`mm`](kcl/mm)
|
||||
* [`offsetPlane`](kcl/offsetPlane)
|
||||
* [`patternCircular2d`](kcl/patternCircular2d)
|
||||
* [`patternCircular3d`](kcl/patternCircular3d)
|
||||
* [`patternLinear2d`](kcl/patternLinear2d)
|
||||
@ -88,6 +90,7 @@ layout: manual
|
||||
* [`tan`](kcl/tan)
|
||||
* [`tangentialArc`](kcl/tangentialArc)
|
||||
* [`tangentialArcTo`](kcl/tangentialArcTo)
|
||||
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
|
||||
* [`tau`](kcl/tau)
|
||||
* [`toDegrees`](kcl/toDegrees)
|
||||
* [`toRadians`](kcl/toRadians)
|
||||
|
516
docs/kcl/loft.md
Normal file
138
docs/kcl/offsetPlane.md
Normal file
11661
docs/kcl/std.json
@ -37,8 +37,7 @@ const example = extrude(10, exampleSketch)
|
||||
offset: number,
|
||||
// Radius of the arc. Not to be confused with Raiders of the Lost Ark.
|
||||
radius: number,
|
||||
} |
|
||||
[number, number]
|
||||
}
|
||||
```
|
||||
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
|
||||
```js
|
||||
|
863
docs/kcl/tangentialArcToRelative.md
Normal file
@ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
}
|
||||
|
||||
// deselect line tool
|
||||
await page.getByTestId('line').click()
|
||||
const btnLine = page.getByTestId('line')
|
||||
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
|
||||
if (btnLineAriaPressed === 'true') {
|
||||
await btnLine.click()
|
||||
}
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect
|
||||
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
|
||||
.toBeLessThan(3)
|
||||
await page.waitForTimeout(100)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(line1, [249, 249, 249]))
|
||||
.poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249]))
|
||||
.toBeLessThan(3)
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
// click between first two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
if (openPanes.includes('code')) {
|
||||
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)
|
||||
).toBeLessThan(3)
|
||||
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
|
||||
}
|
||||
|
||||
// hold down shift
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click between the latest two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// selected two lines therefore there should be two cursors
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Length: open menu' }).click()
|
||||
|
@ -27,9 +27,19 @@ test.describe('Code pane and errors', () => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Extruded Triangle
|
||||
const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([-5, 10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(5, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -261,10 +271,7 @@ test(
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
// If they're open by default, we're not actually testing anything.
|
||||
@ -292,16 +299,7 @@ test(
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
await test.step('All panes opened before should be visible', async () => {
|
||||
|
@ -43,12 +43,6 @@ test(
|
||||
// open the project
|
||||
await page.getByText(`bracket`).click()
|
||||
|
||||
// wait for the project to load
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
@ -56,6 +50,12 @@ test(
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeVisible()
|
||||
|
||||
// Wait for the model to finish loading
|
||||
const modelStateIndicator = page.getByTestId(
|
||||
'model-state-indicator-execution-done'
|
||||
)
|
||||
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
|
||||
|
||||
const gltfOption = page.getByText('glTF')
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
@ -104,7 +104,7 @@ test(
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
.toBe(477481)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
|
@ -84,6 +84,63 @@ test.describe('Editor tests', () => {
|
||||
|> close(%)`)
|
||||
})
|
||||
|
||||
test('if you click the format button it formats your code and executes so lints are still there', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type(`const sketch_001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)`)
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase').first()
|
||||
).toBeVisible()
|
||||
|
||||
await page.locator('#code-pane button:first-child').click()
|
||||
await page.locator('button:has-text("Format code")').click()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch_001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)`)
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase').first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('fold gutters work', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -241,6 +298,67 @@ test.describe('Editor tests', () => {
|
||||
|> close(%)`)
|
||||
})
|
||||
|
||||
test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch_001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)`
|
||||
)
|
||||
localStorage.setItem('disableAxis', 'true')
|
||||
})
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase').first()
|
||||
).toBeVisible()
|
||||
|
||||
// focus the editor
|
||||
await u.codeLocator.click()
|
||||
|
||||
// Hit alt+shift+f to format the code
|
||||
await page.keyboard.press('Alt+Shift+KeyF')
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch_001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)`)
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase').first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('if you write kcl with lint errors you get lints', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
@ -399,7 +517,7 @@ test.describe('Editor tests', () => {
|
||||
const width = 0.500
|
||||
const height = 0.500
|
||||
const dia = 4
|
||||
|
||||
|
||||
fn squareHole = (l, w) => {
|
||||
const squareHoleSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-width / 2, -length / 2], %)
|
||||
|
279
e2e/playwright/file-tree.spec.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test.describe('when using the file tree to', () => {
|
||||
const fromFile = 'main.kcl'
|
||||
const toFile = 'hello.kcl'
|
||||
|
||||
test(
|
||||
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
renameFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files', 'code'])
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
// File the main.kcl with contents
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
await renameFile(fromFile, toFile)
|
||||
await page.reload()
|
||||
|
||||
await test.step('Postcondition: editor has same content as before the rename', async () => {
|
||||
await editorTextMatches(kclCube)
|
||||
})
|
||||
|
||||
await test.step('Postcondition: opening and closing settings works', async () => {
|
||||
const settingsOpenButton = page.getByRole('link', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
const settingsCloseButton = page.getByTestId('settings-close-button')
|
||||
await settingsOpenButton.click()
|
||||
await settingsCloseButton.click()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`create many new untitled files they increment their names`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const { panesOpen, createAndSelectProject, createNewFile } =
|
||||
await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files'])
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
|
||||
await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: /Untitled[-]?[0-5]?/ })
|
||||
).toHaveCount(5)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'create a new file with the same name as an existing file cancels the operation',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
openKclCodePanel,
|
||||
openFilePanel,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
createNewFileAndSelect,
|
||||
renameFile,
|
||||
selectFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
await openKclCodePanel()
|
||||
await openFilePanel()
|
||||
// File the main.kcl with contents
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
const kcl1 = 'main.kcl'
|
||||
const kcl2 = '2.kcl'
|
||||
|
||||
await createNewFileAndSelect(kcl2)
|
||||
const kclCylinder = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCylinder)
|
||||
|
||||
await renameFile(kcl2, kcl1)
|
||||
|
||||
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {
|
||||
await selectFile(kcl1)
|
||||
await editorTextMatches(kclCube)
|
||||
})
|
||||
|
||||
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
|
||||
await selectFile(kcl2)
|
||||
await editorTextMatches(kclCylinder)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'deleting all files recreates a default main.kcl with no code',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
deleteFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files', 'code'])
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
// File the main.kcl with contents
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
const kcl1 = 'main.kcl'
|
||||
|
||||
await deleteFile(kcl1)
|
||||
|
||||
await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => {
|
||||
await editorTextMatches('')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'loading small file, then large, then back to small',
|
||||
{
|
||||
tag: '@electron',
|
||||
},
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { page } = await setupElectron({
|
||||
testInfo,
|
||||
})
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
createNewFile,
|
||||
openDebugPanel,
|
||||
closeDebugPanel,
|
||||
expectCmdLog,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files', 'code'])
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
// Create a small file
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
// pasted into main.kcl
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
// Create a large lego file
|
||||
await createNewFile('lego')
|
||||
const legoFile = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'lego.kcl' }),
|
||||
})
|
||||
await expect(legoFile).toBeVisible({ timeout: 60_000 })
|
||||
await legoFile.click()
|
||||
const kclLego = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/lego.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclLego)
|
||||
const mainFile = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'main.kcl' }),
|
||||
})
|
||||
|
||||
// Open settings and enable the debug panel
|
||||
await page
|
||||
.getByRole('link', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
.click()
|
||||
await page.locator('#showDebugPanel').getByText('OffOn').click()
|
||||
await page.getByTestId('settings-close-button').click()
|
||||
|
||||
await test.step('swap between small and large files', async () => {
|
||||
await openDebugPanel()
|
||||
// Previously created a file so we need to start back at main.kcl
|
||||
await mainFile.click()
|
||||
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
|
||||
// Click the large file
|
||||
await legoFile.click()
|
||||
// Once it is building, click back to the smaller file
|
||||
await mainFile.click()
|
||||
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
|
||||
await closeDebugPanel()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
@ -147,9 +147,6 @@ test.describe('Can export from electron app', () => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
await electronApp.context().addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
})
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
@ -173,10 +170,10 @@ test.describe('Can export from electron app', () => {
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
const exportLocations: Array<Paths> = []
|
||||
@ -207,7 +204,7 @@ test.describe('Can export from electron app', () => {
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
.toBe(477481)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
@ -454,6 +451,7 @@ test(
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'File in the file pane should open with a single click',
|
||||
{ tag: '@electron' },
|
||||
@ -494,10 +492,6 @@ test(
|
||||
|
||||
await file.click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await expect(u.codeLocator).toContainText(
|
||||
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
|
||||
)
|
||||
@ -506,6 +500,69 @@ test(
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Nested directories in project without main.kcl do not create main.kcl',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
let testDir: string | undefined
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'router-template-slate', 'nested'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
join(dir, 'router-template-slate', 'nested', 'slate.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(dir, 'router-template-slate', 'nested', 'bracket.kcl')
|
||||
)
|
||||
testDir = dir
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
await test.step('Open the project', async () => {
|
||||
await page.getByText('router-template-slate').click()
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// It actually loads.
|
||||
await expect(u.codeLocator).toContainText('mounting bracket')
|
||||
await expect(u.codeLocator).toContainText('const radius =')
|
||||
})
|
||||
|
||||
await u.openFilePanel()
|
||||
|
||||
// Find the current file.
|
||||
const filesPane = page.locator('#files-pane')
|
||||
await expect(filesPane.getByText('bracket.kcl')).toBeVisible()
|
||||
// But there's no main.kcl in the file tree browser.
|
||||
await expect(filesPane.getByText('main.kcl')).not.toBeVisible()
|
||||
// No main.kcl file is created on the filesystem.
|
||||
expect(testDir).toBeDefined()
|
||||
if (testDir !== undefined) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
await expect(
|
||||
fsp.access(join(testDir, 'router-template-slate', 'main.kcl'))
|
||||
).rejects.toThrow()
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
await expect(
|
||||
fsp.access(join(testDir, 'router-template-slate', 'nested', 'main.kcl'))
|
||||
).rejects.toThrow()
|
||||
}
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
||||
{ tag: '@electron' },
|
||||
@ -792,10 +849,10 @@ const extrude001 = extrude(200, sketch001)`)
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
|
||||
await expect(async () => {
|
||||
await page.mouse.move(0, 0, { steps: 5 })
|
||||
@ -803,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`)
|
||||
await page.mouse.click(pointOnModel.x, pointOnModel.y)
|
||||
// check user can interact with model by checking it turns yellow
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
|
||||
.toBeLessThan(10)
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
|
||||
.toBeLessThan(15)
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
|
||||
await page.getByTestId('app-logo').click()
|
||||
@ -878,24 +935,15 @@ test(
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
|
||||
@ -912,24 +960,15 @@ test(
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load the stream', async () => {
|
||||
@ -1680,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await page.waitForTimeout(60000)
|
||||
await page.waitForTimeout(2000)
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
|
@ -54,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
const crypticErrorText = `ApiError`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
})
|
||||
test('user should not have to press down twice in cmdbar', async ({
|
||||
page,
|
||||
}) => {
|
||||
// because the model has `line([0,0]..` it is valid code, but the model is invalid
|
||||
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
|
||||
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch2 = startSketchOn("XY")
|
||||
const sketch001 = startSketchAt([-0, -0])
|
||||
|> line([0, 0], %)
|
||||
|> line([-4.84, -5.29], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await page.goto('/')
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await test.step('Check arrow down works', async () => {
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
await page
|
||||
.getByRole('option', { name: 'floppy disk arrow Export' })
|
||||
.click()
|
||||
|
||||
// press arrow down key twice
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('ArrowDown')
|
||||
|
||||
// STL is the third option, which makes sense for two arrow downs
|
||||
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
|
||||
'STL'
|
||||
)
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
})
|
||||
|
||||
await test.step('Check arrow up works', async () => {
|
||||
// theme in test is dark, which is the second option, which means we can test arrow up
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
await page.getByText('The overall appearance of the').click()
|
||||
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
|
||||
'light'
|
||||
)
|
||||
})
|
||||
})
|
||||
test('executes on load', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
@ -358,6 +419,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await page.addInitScript(
|
||||
async ({ code }) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
},
|
||||
{
|
||||
code: bracket,
|
||||
@ -393,20 +455,22 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await test.step('The second export is blocked', async () => {
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage.first()).toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).toBeVisible(),
|
||||
])
|
||||
})
|
||||
|
||||
await test.step('The first export still succeeds', async () => {
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
|
||||
expect(errorToastMessage).not.toBeVisible(),
|
||||
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible({
|
||||
timeout: 15_000,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -419,10 +483,12 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible(),
|
||||
expect(errorToastMessage).not.toBeVisible(),
|
||||
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible(),
|
||||
])
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
})
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
type TestColor = [number, number, number]
|
||||
export const TEST_COLORS = {
|
||||
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
}
|
||||
return maxDiff
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
||||
new Promise(async (resolve) => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
;(async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
}, 50)
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
;(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
}
|
||||
})().catch(reportRejection)
|
||||
}, 50)
|
||||
})().catch(reportRejection)
|
||||
}),
|
||||
emulateNetworkConditions: async (
|
||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||
@ -511,10 +516,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
|
||||
editorTextMatches: async (code: string) => {
|
||||
const editor = page.locator(editorSelector)
|
||||
const editorText = await editor.textContent()
|
||||
return expect(util.toNormalizedCode(editorText || '')).toBe(
|
||||
util.toNormalizedCode(code)
|
||||
)
|
||||
return expect(editor).toHaveText(code, { useInnerText: true })
|
||||
},
|
||||
|
||||
pasteCodeInEditor: async (code: string) => {
|
||||
@ -532,18 +534,74 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
createNewFile: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}`, async () => {
|
||||
await page.getByTestId('create-file-button').click()
|
||||
await page.getByTestId('file-rename-field').fill(name)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
},
|
||||
|
||||
selectFile: async (name: string) => {
|
||||
return test?.step(`Select ${name}`, async () => {
|
||||
await page
|
||||
.getByTestId('file-pane-scroll-container')
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
await openFilePanel(page)
|
||||
await page.getByTestId('create-file-button').click()
|
||||
await page.getByTestId('file-rename-field').fill(name)
|
||||
await page.keyboard.press('Enter')
|
||||
const newFile = page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
|
||||
await expect(newFile).toBeVisible()
|
||||
await newFile.click()
|
||||
})
|
||||
},
|
||||
|
||||
renameFile: async (fromName: string, toName: string) => {
|
||||
return test?.step(`Rename ${fromName} to ${toName}`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: fromName })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-rename').click()
|
||||
await page.getByTestId('file-rename-field').fill(toName)
|
||||
await page.keyboard.press('Enter')
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: toName })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
deleteFile: async (name: string) => {
|
||||
return test?.step(`Delete ${name}`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-delete').click()
|
||||
await page.getByTestId('delete-confirmation').click()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Sorry I don't have time to fix this right now, but runs like
|
||||
* the one linked below show me that setting the open panes in this manner is not reliable.
|
||||
* You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup,
|
||||
* or you can imperatively open the panes with functions like {openKclCodePanel}
|
||||
* (or we can make a general openPane function that takes a paneId).,
|
||||
* but having a separate initScript does not seem to work reliably.
|
||||
* @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553
|
||||
*/
|
||||
panesOpen: async (paneIds: PaneId[]) => {
|
||||
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
|
||||
await page.addInitScript(
|
||||
@ -772,7 +830,6 @@ export async function setup(context: BrowserContext, page: Page) {
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
|
||||
console.log('TEST_SETTINGS.projects', settings)
|
||||
},
|
||||
{
|
||||
token: secrets.token,
|
||||
@ -812,10 +869,12 @@ export async function setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn,
|
||||
cleanProjectDir = true,
|
||||
appSettings,
|
||||
}: {
|
||||
testInfo: TestInfo
|
||||
folderSetupFn?: (projectDirName: string) => Promise<void>
|
||||
cleanProjectDir?: boolean
|
||||
appSettings?: Partial<SaveSettingsPayload>
|
||||
}) {
|
||||
// create or otherwise clear the folder
|
||||
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
|
||||
@ -849,15 +908,19 @@ export async function setupElectron({
|
||||
|
||||
if (cleanProjectDir) {
|
||||
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
||||
const settingsOverrides = TOML.stringify({
|
||||
...TEST_SETTINGS,
|
||||
settings: {
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
projectDirectory: projectDirName,
|
||||
},
|
||||
},
|
||||
})
|
||||
const settingsOverrides = TOML.stringify(
|
||||
appSettings
|
||||
? { settings: appSettings }
|
||||
: {
|
||||
...TEST_SETTINGS,
|
||||
settings: {
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
projectDirectory: projectDirName,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
||||
}
|
||||
|
||||
|
@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
let noHoverColor: [number, number, number] = [82, 82, 82]
|
||||
let hoverColor: [number, number, number] = [116, 116, 116]
|
||||
let selectColor: [number, number, number] = [144, 148, 97]
|
||||
let noHoverColor: [number, number, number] = [92, 92, 92]
|
||||
let hoverColor: [number, number, number] = [127, 127, 127]
|
||||
let selectColor: [number, number, number] = [155, 155, 105]
|
||||
|
||||
const extrudeWall = { x: 670, y: 275 }
|
||||
const extrudeText = `line([170.36, -121.61], %, $seg01)`
|
||||
@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
||||
.toBeLessThan(5)
|
||||
.toBeLessThan(15)
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(extrudeWall.x, extrudeWall.y)
|
||||
@ -798,43 +798,43 @@ const extrude001 = extrude(50, sketch001)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, hoverColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
await page.mouse.click(extrudeWall.x, extrudeWall.y)
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
await page.waitForTimeout(1000)
|
||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
// because of shading, color is not exact everywhere on the face
|
||||
noHoverColor = [104, 104, 104]
|
||||
hoverColor = [134, 134, 134]
|
||||
selectColor = [158, 162, 110]
|
||||
noHoverColor = [115, 115, 115]
|
||||
hoverColor = [145, 145, 145]
|
||||
selectColor = [168, 168, 120]
|
||||
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15)
|
||||
await page.mouse.move(cap.x, cap.y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||
removeAfterFirstParenthesis(capText)
|
||||
)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
|
||||
await page.mouse.click(cap.x, cap.y)
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||
await page.waitForTimeout(1000)
|
||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||
})
|
||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||
page,
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
|
||||
@ -154,29 +154,33 @@ test.describe('Testing settings', () => {
|
||||
|
||||
test('Project and user settings can be reset', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
await test.step(`Setup`, async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
})
|
||||
|
||||
// Selectors and constants
|
||||
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||
const resetButton = page.getByRole('button', {
|
||||
name: 'Restore default settings',
|
||||
})
|
||||
const resetButton = (level: SettingsLevel) =>
|
||||
page.getByRole('button', {
|
||||
name: `Reset ${level}-level settings`,
|
||||
})
|
||||
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
||||
const settingValues = {
|
||||
default: '259',
|
||||
user: '120',
|
||||
project: '50',
|
||||
}
|
||||
const resetToast = (level: SettingsLevel) =>
|
||||
page.getByText(`${level}-level settings were reset`)
|
||||
|
||||
// Open the settings modal with lower-right button
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await test.step(`Open the settings modal`, async () => {
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Set up theme color', async () => {
|
||||
// Verify we're looking at the project-level settings,
|
||||
@ -195,37 +199,40 @@ test.describe('Testing settings', () => {
|
||||
|
||||
await test.step('Reset project settings', async () => {
|
||||
// Click the reset settings button.
|
||||
await resetButton.click()
|
||||
await resetButton('project').click()
|
||||
|
||||
await expect(page.getByText('Settings restored to default')).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Settings restored to default')
|
||||
).not.toBeVisible()
|
||||
await expect(resetToast('project')).toBeVisible()
|
||||
await expect(resetToast('project')).not.toBeVisible()
|
||||
|
||||
// Verify it is now set to the inherited user value
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
||||
|
||||
// Check that the user setting also rolled back
|
||||
await userSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
await projectSettingsTab.click()
|
||||
await test.step(`Check that the user settings did not change`, async () => {
|
||||
await userSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
||||
})
|
||||
|
||||
// Set project-level value to 50 again to test the user-level reset
|
||||
await themeColorSetting.fill(settingValues.project)
|
||||
await userSettingsTab.click()
|
||||
await test.step(`Set project-level again to test the user-level reset`, async () => {
|
||||
await projectSettingsTab.click()
|
||||
await themeColorSetting.fill(settingValues.project)
|
||||
await userSettingsTab.click()
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Reset user settings', async () => {
|
||||
// Change the setting and click the reset settings button.
|
||||
await themeColorSetting.fill(settingValues.user)
|
||||
await resetButton.click()
|
||||
// Click the reset settings button.
|
||||
await resetButton('user').click()
|
||||
|
||||
await expect(resetToast('user')).toBeVisible()
|
||||
await expect(resetToast('user')).not.toBeVisible()
|
||||
|
||||
// Verify it is now set to the default value
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
|
||||
// Check that the project setting also changed
|
||||
await projectSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
await test.step(`Check that the project settings did not change`, async () => {
|
||||
await projectSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.project)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -288,7 +295,7 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
|
||||
await test.step('Refresh the application and see project setting applied', async () => {
|
||||
await page.reload()
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
|
||||
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||
await settingsCloseButton.click()
|
||||
@ -303,53 +310,109 @@ test.describe('Testing settings', () => {
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Load desktop app with no settings file`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
// This is what makes no settings file get created
|
||||
cleanProjectDir: false,
|
||||
testInfo,
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Selectors and constants
|
||||
const errorHeading = page.getByRole('heading', {
|
||||
name: 'An unextected error occurred',
|
||||
})
|
||||
const projectDirLink = page.getByText('Loaded from')
|
||||
|
||||
// If the app loads without exploding we're in the clear
|
||||
await expect(errorHeading).not.toBeVisible()
|
||||
await expect(projectDirLink).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Load desktop app with a settings file, but no project directory setting`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
appSettings: {
|
||||
app: {
|
||||
themeColor: '259',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Selectors and constants
|
||||
const errorHeading = page.getByRole('heading', {
|
||||
name: 'An unextected error occurred',
|
||||
})
|
||||
const projectDirLink = page.getByText('Loaded from')
|
||||
|
||||
// If the app loads without exploding we're in the clear
|
||||
await expect(errorHeading).not.toBeVisible()
|
||||
await expect(projectDirLink).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Closing settings modal should go back to the original file being viewed`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
folderSetupFn: async (dir) => {
|
||||
const bracketDir = join(dir, 'project-000')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cube.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(bracketDir, '2.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
|
||||
const kclCylinder = await fsp.readFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
clickPane,
|
||||
createNewFileAndSelect,
|
||||
openKclCodePanel,
|
||||
openFilePanel,
|
||||
waitForPageLoad,
|
||||
selectFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen([])
|
||||
|
||||
await test.step('Precondition: No projects exist', async () => {
|
||||
await test.step('Precondition: Open to second project file', async () => {
|
||||
await expect(page.getByTestId('home-section')).toBeVisible()
|
||||
const projectLinksPre = page.getByTestId('project-link')
|
||||
await expect(projectLinksPre).toHaveCount(0)
|
||||
await page.getByText('project-000').click()
|
||||
await waitForPageLoad()
|
||||
await openKclCodePanel()
|
||||
await openFilePanel()
|
||||
await editorTextMatches(kclCube)
|
||||
|
||||
await selectFile('2.kcl')
|
||||
await editorTextMatches(kclCylinder)
|
||||
})
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
await clickPane('code')
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
await clickPane('files')
|
||||
await createNewFileAndSelect('2.kcl')
|
||||
|
||||
const kclCylinder = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCylinder)
|
||||
|
||||
const settingsOpenButton = page.getByRole('link', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
@ -357,6 +420,9 @@ test.describe('Testing settings', () => {
|
||||
|
||||
await test.step('Open and close settings', async () => {
|
||||
await settingsOpenButton.click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await settingsCloseButton.click()
|
||||
})
|
||||
|
||||
@ -367,4 +433,208 @@ test.describe('Testing settings', () => {
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test('Changing modeling default unit', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await test.step(`Test setup`, async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
})
|
||||
|
||||
// Selectors and constants
|
||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||
const defaultUnitSection = page.getByText(
|
||||
'default unitRoll back default unitRoll back to match'
|
||||
)
|
||||
const defaultUnitRollbackButton = page.getByRole('button', {
|
||||
name: 'Roll back default unit',
|
||||
})
|
||||
|
||||
await test.step(`Open the settings modal`, async () => {
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Reset unit setting`, async () => {
|
||||
await userSettingsTab.click()
|
||||
await defaultUnitSection.hover()
|
||||
await defaultUnitRollbackButton.click()
|
||||
await projectSettingsTab.click()
|
||||
})
|
||||
|
||||
await test.step('Change modeling default unit within project tab', async () => {
|
||||
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
|
||||
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
|
||||
await page
|
||||
.getByTestId('modeling-defaultUnit')
|
||||
.selectOption(`${unitOfMeasure}`)
|
||||
const toastMessage = page.getByText(
|
||||
`Set default unit to "${unitOfMeasure}" for this project`
|
||||
)
|
||||
await expect(toastMessage).toBeVisible()
|
||||
})
|
||||
}
|
||||
await changeUnitOfMeasureInProjectTab('in')
|
||||
await changeUnitOfMeasureInProjectTab('ft')
|
||||
await changeUnitOfMeasureInProjectTab('yd')
|
||||
await changeUnitOfMeasureInProjectTab('mm')
|
||||
await changeUnitOfMeasureInProjectTab('cm')
|
||||
await changeUnitOfMeasureInProjectTab('m')
|
||||
})
|
||||
|
||||
// Go to the user tab
|
||||
await userSettingsTab.click()
|
||||
await test.step('Change modeling default unit within user tab', async () => {
|
||||
const changeUnitOfMeasureInUserTab = async (unitOfMeasure: string) => {
|
||||
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
|
||||
await page
|
||||
.getByTestId('modeling-defaultUnit')
|
||||
.selectOption(`${unitOfMeasure}`)
|
||||
const toastMessage = page.getByText(
|
||||
`Set default unit to "${unitOfMeasure}" as a user default`
|
||||
)
|
||||
await expect(toastMessage).toBeVisible()
|
||||
})
|
||||
}
|
||||
await changeUnitOfMeasureInUserTab('in')
|
||||
await changeUnitOfMeasureInUserTab('ft')
|
||||
await changeUnitOfMeasureInUserTab('yd')
|
||||
await changeUnitOfMeasureInUserTab('mm')
|
||||
await changeUnitOfMeasureInUserTab('cm')
|
||||
await changeUnitOfMeasureInUserTab('m')
|
||||
})
|
||||
|
||||
// Close settings
|
||||
const settingsCloseButton = page.getByTestId('settings-close-button')
|
||||
await settingsCloseButton.click()
|
||||
|
||||
await test.step('Change modeling default unit within command bar', async () => {
|
||||
const commands = page.getByRole('button', { name: 'Commands' })
|
||||
const changeUnitOfMeasureInCommandBar = async (unitOfMeasure: string) => {
|
||||
// Open command bar
|
||||
await commands.click()
|
||||
const settingsModelingDefaultUnitCommand = page.getByText(
|
||||
'Settings · modeling · default unit'
|
||||
)
|
||||
await settingsModelingDefaultUnitCommand.click()
|
||||
|
||||
const commandOption = page.getByRole('option', {
|
||||
name: unitOfMeasure,
|
||||
exact: true,
|
||||
})
|
||||
await commandOption.click()
|
||||
|
||||
const toastMessage = page.getByText(
|
||||
`Set default unit to "${unitOfMeasure}" for this project`
|
||||
)
|
||||
await expect(toastMessage).toBeVisible()
|
||||
}
|
||||
await changeUnitOfMeasureInCommandBar('in')
|
||||
await changeUnitOfMeasureInCommandBar('ft')
|
||||
await changeUnitOfMeasureInCommandBar('yd')
|
||||
await changeUnitOfMeasureInCommandBar('mm')
|
||||
await changeUnitOfMeasureInCommandBar('cm')
|
||||
await changeUnitOfMeasureInCommandBar('m')
|
||||
})
|
||||
|
||||
await test.step('Change modeling default unit within gizmo', async () => {
|
||||
const changeUnitOfMeasureInGizmo = async (
|
||||
unitOfMeasure: string,
|
||||
copy: string
|
||||
) => {
|
||||
const gizmo = page.getByRole('button', {
|
||||
name: 'Current units are: ',
|
||||
})
|
||||
await gizmo.click()
|
||||
const button = page.getByRole('button', {
|
||||
name: copy,
|
||||
exact: true,
|
||||
})
|
||||
await button.click()
|
||||
const toastMessage = page.getByText(
|
||||
`Set default unit to "${unitOfMeasure}" for this project`
|
||||
)
|
||||
await expect(toastMessage).toBeVisible()
|
||||
}
|
||||
|
||||
await changeUnitOfMeasureInGizmo('in', 'Inches')
|
||||
await changeUnitOfMeasureInGizmo('ft', 'Feet')
|
||||
await changeUnitOfMeasureInGizmo('yd', 'Yards')
|
||||
await changeUnitOfMeasureInGizmo('mm', 'Millimeters')
|
||||
await changeUnitOfMeasureInGizmo('cm', 'Centimeters')
|
||||
await changeUnitOfMeasureInGizmo('m', 'Meters')
|
||||
})
|
||||
})
|
||||
|
||||
test('Changing theme in sketch mode', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([5, 0], %)
|
||||
|> line([0, 5], %)
|
||||
|> line([-5, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(5, sketch001)
|
||||
`
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Selectors and constants
|
||||
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||
const lineToolButton = page.getByTestId('line')
|
||||
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||
const sketchOriginLocation = { x: 600, y: 250 }
|
||||
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||
|
||||
await test.step(`Get into sketch mode`, async () => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page.mouse.click(700, 200)
|
||||
await expect(editSketchButton).toBeVisible()
|
||||
await editSketchButton.click()
|
||||
|
||||
// We use the line tool as a proxy for sketch mode
|
||||
await expect(lineToolButton).toBeVisible()
|
||||
await expect(segmentOverlays).toHaveCount(4)
|
||||
// but we allow more time to pass for animating to the sketch
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
|
||||
await test.step(`Check the sketch line color before`, async () => {
|
||||
await expect
|
||||
.poll(() =>
|
||||
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
||||
)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step(`Change theme to light using command palette`, async () => {
|
||||
await page.keyboard.press('ControlOrMeta+K')
|
||||
await page.getByRole('option', { name: 'theme' }).click()
|
||||
await page.getByRole('option', { name: 'light' }).click()
|
||||
await expect(page.getByText('theme to "light"')).toBeVisible()
|
||||
|
||||
// Make sure we haven't left sketch mode
|
||||
await expect(lineToolButton).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Check the sketch line color after`, async () => {
|
||||
await expect
|
||||
.poll(() =>
|
||||
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
||||
)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
import { getUtils, setup, tearDown, setupElectron } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -532,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
|
||||
// Ensure the final toast remains.
|
||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
|
||||
// Ensure you can copy the code for the final model.
|
||||
@ -683,3 +685,75 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
}
|
||||
|
||||
test(
|
||||
'Text-to-CAD functionality',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const projectName = 'project-000'
|
||||
const prompt = 'lego 2x4'
|
||||
const textToCadFileName = 'lego-2x4.kcl'
|
||||
|
||||
const { electronApp, page, dir } = await setupElectron({ testInfo })
|
||||
const fileExists = () =>
|
||||
fs.existsSync(join(dir, projectName, textToCadFileName))
|
||||
|
||||
const {
|
||||
createAndSelectProject,
|
||||
openFilePanel,
|
||||
openKclCodePanel,
|
||||
waitForPageLoad,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Locators
|
||||
const projectMenuButton = page.getByRole('button', { name: projectName })
|
||||
const textToCadFileButton = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: textToCadFileName }),
|
||||
})
|
||||
const textToCadComment = page.getByText(
|
||||
`// Generated by Text-to-CAD: ${prompt}`
|
||||
)
|
||||
|
||||
// Create and navigate to the project
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
||||
await waitForPageLoad()
|
||||
await openFilePanel()
|
||||
await openKclCodePanel()
|
||||
|
||||
await test.step(`Test file creation`, async () => {
|
||||
await sendPromptFromCommandBar(page, prompt)
|
||||
// File is considered created if it shows up in the Project Files pane
|
||||
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
||||
expect(fileExists()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step(`Test file navigation`, async () => {
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await textToCadFileButton.click()
|
||||
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
|
||||
await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
|
||||
await expect(projectMenuButton).toContainText(textToCadFileName)
|
||||
})
|
||||
|
||||
await test.step(`Test file deletion on rejection`, async () => {
|
||||
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
||||
// A file is created and can be navigated to while this prompt is still opened
|
||||
// Click the "Reject" button within the prompt and it will delete the file.
|
||||
await rejectButton.click()
|
||||
|
||||
const submittingToastMessage = page.getByText(
|
||||
`Successfully deleted file "lego-2x4.kcl"`
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
expect(fileExists()).toBeFalsy()
|
||||
// Confirm we've navigated back to the main.kcl file after deletion
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
83
electron-builder.yml
Normal file
@ -0,0 +1,83 @@
|
||||
appId: dev.zoo.modeling-app
|
||||
|
||||
directories:
|
||||
output: out
|
||||
buildResources: assets
|
||||
|
||||
files:
|
||||
- .vite/**
|
||||
|
||||
mac:
|
||||
category: public.app-category.developer-tools
|
||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
notarize:
|
||||
teamId: 92H8YB3B95
|
||||
fileAssociations:
|
||||
- ext: kcl
|
||||
name: kcl
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
rank: Owner
|
||||
|
||||
win:
|
||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||
target:
|
||||
- target: nsis
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: msi
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
signingHashAlgorithms:
|
||||
- sha256
|
||||
sign: "./sign-win.js"
|
||||
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
|
||||
icon: "assets/icon.ico"
|
||||
fileAssociations:
|
||||
- ext: kcl
|
||||
name: kcl
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
|
||||
msi:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
allowElevation: true
|
||||
installerIcon: "assets/icon.ico"
|
||||
include: "./installer.nsh"
|
||||
|
||||
linux:
|
||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||
target:
|
||||
- target: appImage
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
fileAssociations:
|
||||
- ext: kcl
|
||||
name: kcl
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
|
||||
publish:
|
||||
- provider: generic
|
||||
url: https://dl.zoo.dev/releases/modeling-app
|
||||
channel: latest
|
@ -4,10 +4,17 @@ import { MakerZIP } from '@electron-forge/maker-zip'
|
||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm'
|
||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
||||
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||
import path from 'path'
|
||||
|
||||
interface ExtendedMakerWixConfig extends MakerWixConfig {
|
||||
// see https://github.com/electron/forge/issues/3673
|
||||
// this is an undocumented property of electron-wix-msi
|
||||
associateExtensions?: string
|
||||
}
|
||||
|
||||
const rootDir = process.cwd()
|
||||
|
||||
const config: ForgeConfig = {
|
||||
@ -23,12 +30,23 @@ const config: ForgeConfig = {
|
||||
undefined,
|
||||
executableName: 'zoo-modeling-app',
|
||||
icon: path.resolve(rootDir, 'assets', 'icon'),
|
||||
protocols: [
|
||||
{
|
||||
name: 'Zoo Studio',
|
||||
schemes: ['zoo-studio'],
|
||||
},
|
||||
],
|
||||
extendInfo: 'Info.plist', // Information for file associations.
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({
|
||||
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
||||
}),
|
||||
new MakerWix({
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
||||
associateExtensions: 'kcl',
|
||||
} as ExtendedMakerWixConfig),
|
||||
new MakerZIP({}, ['darwin']),
|
||||
new MakerRpm({
|
||||
options: {
|
||||
|
8
installer.nsh
Normal file
@ -0,0 +1,8 @@
|
||||
!macro preInit
|
||||
SetRegView 64
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
SetRegView 32
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
!macroend
|
1
interface.d.ts
vendored
@ -30,7 +30,6 @@ export interface IElectronAPI {
|
||||
join: typeof path.join
|
||||
sep: typeof path.sep
|
||||
rename: (prev: string, next: string) => typeof fs.rename
|
||||
setBaseUrl: (value: string) => void
|
||||
packageJson: {
|
||||
name: string
|
||||
}
|
||||
|
38
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.24.12",
|
||||
"version": "0.25.1",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
@ -34,22 +34,24 @@
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@tweenjs/tween.js": "^23.1.1",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
"jszip": "^3.10.1",
|
||||
"minimist": "^1.2.8",
|
||||
"openid-client": "^5.6.5",
|
||||
"re-resizable": "^6.9.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-modal-promise": "^1.0.2",
|
||||
@ -62,7 +64,7 @@
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^4.38.2"
|
||||
"xstate": "^5.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@ -82,14 +84,13 @@
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src e2e",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
||||
"postinstall": "yarn xstate:typegen",
|
||||
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src e2e packages/codemirror-lsp-client",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
"make:dev": "make dev",
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||
@ -97,7 +98,9 @@
|
||||
"tron:package": "electron-forge package",
|
||||
"tron:make": "electron-forge make",
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron"
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package": "electron-builder --config electron-builder.yml"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -124,11 +127,13 @@
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.0",
|
||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||
"@electron-forge/maker-wix": "^7.4.0",
|
||||
"@electron-forge/maker-zip": "^7.4.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
||||
"@electron-forge/plugin-vite": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
@ -137,6 +142,7 @@
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^22.5.0",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
@ -147,7 +153,6 @@
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
@ -158,10 +163,12 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "^32.0.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-import": "^2.25.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^14.3.10",
|
||||
"http-server": "^14.1.1",
|
||||
@ -169,7 +176,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.43",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"setimmediate": "^1.0.5",
|
||||
@ -182,7 +189,6 @@
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"wasm-pack": "^0.13.0",
|
||||
"ws": "^8.17.0",
|
||||
"yarn": "^1.22.22"
|
||||
|
@ -72,6 +72,7 @@ export class LanguageServerClient {
|
||||
async initialize() {
|
||||
// Start the client in the background.
|
||||
this.client.setNotifyFn(this.processNotifications.bind(this))
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.client.start()
|
||||
|
||||
this.ready = true
|
||||
@ -195,6 +196,9 @@ export class LanguageServerClient {
|
||||
}
|
||||
|
||||
private processNotifications(notification: LSP.NotificationMessage) {
|
||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||
for (const plugin of this.plugins) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.processNotification(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export default function lspFormatExt(
|
||||
run: (view: EditorView) => {
|
||||
let value = view.plugin(plugin)
|
||||
if (!value) return false
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
value.requestFormatting()
|
||||
return true
|
||||
},
|
||||
|
@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
|
||||
this.processLspNotification = options.processLspNotification
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.initialize({
|
||||
documentText: this.getDocText(),
|
||||
})
|
||||
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
|
||||
async initialize({ documentText }: { documentText: string }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
if (this.client.initializePromise) {
|
||||
await this.client.initializePromise
|
||||
}
|
||||
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
},
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.requestSemanticTokens()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateFoldingRanges()
|
||||
}
|
||||
|
||||
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.requestSemanticTokens()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateFoldingRanges()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@ -526,7 +532,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||
if (params.uri !== this.getDocUri()) return
|
||||
|
||||
const diagnostics = params.diagnostics
|
||||
// Commented to avoid the lint. See TODO below.
|
||||
// const diagnostics =
|
||||
params.diagnostics
|
||||
.map(({ range, message, severity }) => ({
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
timeout: 120_000, // override the default 30s timeout
|
||||
testDir: './e2e/playwright',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Do not retry */
|
||||
retries: process.env.CI ? 0 : 0,
|
||||
/* Different amount of parallelism on CI and local. */
|
||||
workers: process.env.CI ? 1 : 4,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
[process.env.CI ? 'dot' : 'list'],
|
||||
['json', { outputFile: './test-results/report.json' }],
|
||||
['html'],
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
actionTimeout: 15000,
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
})
|
BIN
public/wheel-loop-dark.mp4
Normal file
BIN
public/wheel-loop.mp4
Normal file
38
sign-win.js
Normal file
@ -0,0 +1,38 @@
|
||||
// From https://github.com/OpenBuilds/OpenBuilds-CONTROL/blob/4800540ffaa517925fc2cff26670809efa341ffe/signWin.js
|
||||
const { execSync } = require('node:child_process')
|
||||
|
||||
exports.default = async (configuration) => {
|
||||
if (!process.env.SM_API_KEY) {
|
||||
console.error(
|
||||
'Signing using signWin.js script: failed: SM_API_KEY ENV VAR NOT FOUND'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!process.env.WINDOWS_CERTIFICATE_THUMBPRINT) {
|
||||
console.error(
|
||||
'Signing using signWin.js script: failed: FINGERPRINT ENV VAR NOT FOUND'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!configuration.path) {
|
||||
throw new Error(
|
||||
`Signing using signWin.js script: failed: TARGET PATH NOT FOUND`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`smctl sign --fingerprint="${
|
||||
process.env.WINDOWS_CERTIFICATE_THUMBPRINT
|
||||
}" --input "${String(configuration.path)}"`,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
}
|
||||
)
|
||||
console.log('Signing using signWin.js script: successful')
|
||||
} catch (error) {
|
||||
console.error('Signing using signWin.js script: failed:', error)
|
||||
}
|
||||
}
|
14
src/App.tsx
@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
@ -80,7 +81,7 @@ export function App() {
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager.sendSceneCommand(message)
|
||||
engineCommandManager.sendSceneCommand(message).catch(reportRejection)
|
||||
}, 1000 / 15)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (state.matches('Sketch')) {
|
||||
@ -95,7 +96,7 @@ export function App() {
|
||||
})
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
if (state.matches({ idle: 'showPlanes' })) return
|
||||
if (context.store?.buttonDownInStream !== undefined) return
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -119,6 +120,15 @@ export function App() {
|
||||
paneOpacity +
|
||||
(context.store?.buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
// Override the electron window draggable region behavior as well
|
||||
// when the button is down in the stream
|
||||
style={
|
||||
isDesktop() && context.store?.buttonDownInStream
|
||||
? ({
|
||||
'-webkit-app-region': 'no-drag',
|
||||
} as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
/>
|
||||
|
@ -33,7 +33,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { getState, setState } from 'lib/desktop'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -42,6 +41,7 @@ import toast from 'react-hot-toast'
|
||||
import { coreDump } from 'lang/wasm'
|
||||
import { useMemo } from 'react'
|
||||
import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
|
||||
@ -70,23 +70,6 @@ const router = createRouter([
|
||||
path: PATHS.INDEX,
|
||||
loader: async () => {
|
||||
const onDesktop = isDesktop()
|
||||
if (onDesktop) {
|
||||
const appState = await getState()
|
||||
|
||||
if (appState) {
|
||||
// Reset the state.
|
||||
// We do this so that we load the initial state from the cli but everything
|
||||
// else we can ignore.
|
||||
await setState(undefined)
|
||||
// Redirect to the file if we have a file path.
|
||||
if (appState.current_file) {
|
||||
return redirect(
|
||||
PATHS.FILE + '/' + encodeURIComponent(appState.current_file)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return onDesktop
|
||||
? redirect(PATHS.HOME)
|
||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
@ -191,21 +174,23 @@ function CoreDump() {
|
||||
[]
|
||||
)
|
||||
useHotkeyWrapper(['mod + shift + .'], () => {
|
||||
toast.promise(
|
||||
coreDump(coreDumpManager, true),
|
||||
{
|
||||
loading: 'Starting core dump...',
|
||||
success: 'Core dump completed successfully',
|
||||
error: 'Error while exporting core dump',
|
||||
},
|
||||
{
|
||||
success: {
|
||||
// Note: this extended duration is especially important for Playwright e2e testing
|
||||
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||
duration: 6000,
|
||||
toast
|
||||
.promise(
|
||||
coreDump(coreDumpManager, true),
|
||||
{
|
||||
loading: 'Starting core dump...',
|
||||
success: 'Core dump completed successfully',
|
||||
error: 'Error while exporting core dump',
|
||||
},
|
||||
}
|
||||
)
|
||||
{
|
||||
success: {
|
||||
// Note: this extended duration is especially important for Playwright e2e testing
|
||||
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||
duration: 6000,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch(reportRejection)
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import {
|
||||
ToolbarItemResolved,
|
||||
ToolbarModeName,
|
||||
} from 'lib/toolbar'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
@ -68,12 +70,12 @@ export function Toolbar({
|
||||
*/
|
||||
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
||||
() => ({
|
||||
modelingStateMatches: state.matches,
|
||||
modelingState: state,
|
||||
modelingSend: send,
|
||||
commandBarSend,
|
||||
sketchPathId,
|
||||
}),
|
||||
[state.matches, send, commandBarSend, sketchPathId]
|
||||
[state, send, commandBarSend, sketchPathId]
|
||||
)
|
||||
|
||||
/**
|
||||
@ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
return (
|
||||
<Tooltip
|
||||
inert={false}
|
||||
wrapperStyle={
|
||||
isDesktop()
|
||||
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
@ -337,6 +344,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
<li key={link.label} className="contents">
|
||||
<a
|
||||
href={link.url}
|
||||
onClick={openExternalBrowserIfDesktop(link.url)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MouseGuard } from 'lib/cameraControls'
|
||||
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
||||
import {
|
||||
Euler,
|
||||
MathUtils,
|
||||
@ -22,18 +22,19 @@ import {
|
||||
UnreliableSubscription,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { toSync, uuidv4 } from 'lib/utils'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { isQuaternionVertical } from './helpers'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||
const FRAMES_TO_ANIMATE_IN = 30
|
||||
|
||||
const tempQuaternion = new Quaternion() // just used for maths
|
||||
|
||||
type interactionType = 'pan' | 'rotate' | 'zoom'
|
||||
export type CameraInteractionType = 'pan' | 'rotate' | 'zoom'
|
||||
|
||||
interface ThreeCamValues {
|
||||
position: Vector3
|
||||
@ -81,24 +82,7 @@ export class CameraControls {
|
||||
pendingZoom: number | null = null
|
||||
pendingRotation: Vector2 | null = null
|
||||
pendingPan: Vector2 | null = null
|
||||
interactionGuards: MouseGuard = {
|
||||
pan: {
|
||||
description: 'Right click + Shift + drag or middle click + drag',
|
||||
callback: (e) => !!(e.buttons & 4) && !e.ctrlKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => {
|
||||
console.log('event', e)
|
||||
return !!(e.buttons & 2)
|
||||
},
|
||||
},
|
||||
}
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
||||
isFovAnimationInProgress = false
|
||||
fovBeforeOrtho = 45
|
||||
get isPerspective() {
|
||||
@ -117,6 +101,7 @@ export class CameraControls {
|
||||
camProps.type === 'perspective' &&
|
||||
this.camera instanceof OrthographicCamera
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
} else if (
|
||||
camProps.type === 'orthographic' &&
|
||||
@ -144,6 +129,7 @@ export class CameraControls {
|
||||
}
|
||||
|
||||
throttledEngCmd = throttle((cmd: EngineCommand) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand(cmd)
|
||||
}, 1000 / 30)
|
||||
|
||||
@ -156,6 +142,7 @@ export class CameraControls {
|
||||
...convertThreeCamValuesToEngineCam(threeValues),
|
||||
},
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand(cmd)
|
||||
}, 1000 / 15)
|
||||
|
||||
@ -168,6 +155,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveCmd &&
|
||||
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
|
||||
this.lastPerspectiveCmdTime = Date.now()
|
||||
}
|
||||
@ -235,6 +223,7 @@ export class CameraControls {
|
||||
this.useOrthographicCamera()
|
||||
}
|
||||
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
}
|
||||
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
||||
@ -266,6 +255,7 @@ export class CameraControls {
|
||||
const doZoom = () => {
|
||||
if (this.zoomDataFromLastFrame !== undefined) {
|
||||
this.handleStart()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -283,6 +273,7 @@ export class CameraControls {
|
||||
|
||||
const doMove = () => {
|
||||
if (this.moveDataFromLastFrame !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -476,6 +467,7 @@ export class CameraControls {
|
||||
|
||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||
this.camera.updateProjectionMatrix()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
@ -558,7 +550,7 @@ export class CameraControls {
|
||||
const oldFov = this.camera.fov
|
||||
|
||||
const viewHeightFactor = (fov: number) => {
|
||||
/* *
|
||||
/* *
|
||||
/|
|
||||
/ |
|
||||
/ |
|
||||
@ -946,6 +938,7 @@ export class CameraControls {
|
||||
}
|
||||
|
||||
if (isReducedMotion()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
onComplete()
|
||||
return
|
||||
}
|
||||
@ -954,7 +947,7 @@ export class CameraControls {
|
||||
.to({ t: tweenEnd }, duration)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onUpdate(({ t }) => cameraAtTime(t))
|
||||
.onComplete(onComplete)
|
||||
.onComplete(toSync(onComplete, reportRejection))
|
||||
.start()
|
||||
})
|
||||
}
|
||||
@ -979,6 +972,7 @@ export class CameraControls {
|
||||
// Decrease the FOV
|
||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||
this.camera.updateProjectionMatrix()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.dollyZoom(currentFov)
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else if (frameWaitOnFinish > 0) {
|
||||
@ -1008,6 +1002,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveFov = 4
|
||||
let currentFov = 4
|
||||
const initialCameraUp = this.camera.up.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
const tempVec = new Vector3()
|
||||
|
||||
@ -1016,6 +1011,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
|
||||
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
|
||||
this.camera.up.copy(currentUp)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.dollyZoom(currentFov)
|
||||
}
|
||||
|
||||
@ -1044,6 +1040,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveFov = 4
|
||||
let currentFov = 4
|
||||
const initialCameraUp = this.camera.up.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
const tempVec = new Vector3()
|
||||
|
||||
@ -1192,7 +1189,7 @@ function convertThreeCamValuesToEngineCam({
|
||||
const lookAt = buildLookAt(64 / zoom, target, position)
|
||||
return {
|
||||
center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z),
|
||||
up: new Vector3(0, 0, 1),
|
||||
up: new Vector3(upVector.x, upVector.y, upVector.z),
|
||||
vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z),
|
||||
}
|
||||
}
|
||||
@ -1222,8 +1219,8 @@ function _getInteractionType(
|
||||
enablePan: boolean,
|
||||
enableRotate: boolean,
|
||||
enableZoom: boolean
|
||||
): interactionType | 'none' {
|
||||
let state: interactionType | 'none' = 'none'
|
||||
): CameraInteractionType | 'none' {
|
||||
let state: CameraInteractionType | 'none' = 'none'
|
||||
if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
|
||||
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
|
||||
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
||||
|
@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { throttle } from 'lib/utils'
|
||||
import { throttle, toSync } from 'lib/utils'
|
||||
import {
|
||||
sceneInfra,
|
||||
kclManager,
|
||||
@ -44,7 +44,7 @@ import {
|
||||
removeSingleConstraintInfo,
|
||||
} from 'lang/modifyAst'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||
@ -124,9 +124,9 @@ export const ClientSideScene = ({
|
||||
} else if (context.mouseState.type === 'isDragging') {
|
||||
cursor = 'grabbing'
|
||||
} else if (
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
state.matches({ Sketch: 'Line tool' }) ||
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' })
|
||||
) {
|
||||
cursor = 'crosshair'
|
||||
} else {
|
||||
@ -214,9 +214,9 @@ const Overlay = ({
|
||||
overlay.visible &&
|
||||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
||||
!(
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
state.matches({ Sketch: 'Line tool' }) ||
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' })
|
||||
)
|
||||
|
||||
return (
|
||||
@ -582,7 +582,7 @@ const ConstraintSymbol = ({
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={async () => {
|
||||
onClick={toSync(async () => {
|
||||
if (!isConstrained) {
|
||||
send({
|
||||
type: 'Convert to variable',
|
||||
@ -616,13 +616,14 @@ const ConstraintSymbol = ({
|
||||
)
|
||||
if (!transform) return
|
||||
const { modifiedAst } = transform
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
toast.success('Constraint removed')
|
||||
}
|
||||
}}
|
||||
}, reportRejection)}
|
||||
>
|
||||
<CustomIcon name={name} />
|
||||
</button>
|
||||
@ -688,7 +689,7 @@ const ConstraintSymbol = ({
|
||||
|
||||
const throttled = throttle((a: ReactCameraProperties) => {
|
||||
if (a.type === 'perspective' && a.fov) {
|
||||
sceneInfra.camControls.dollyZoom(a.fov)
|
||||
sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
|
||||
}
|
||||
}, 1000 / 15)
|
||||
|
||||
@ -718,6 +719,7 @@ export const CamDebugSettings = () => {
|
||||
if (camSettings.type === 'perspective') {
|
||||
sceneInfra.camControls.useOrthographicCamera()
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sceneInfra.camControls.usePerspectiveCamera(true)
|
||||
}
|
||||
}}
|
||||
@ -725,7 +727,7 @@ export const CamDebugSettings = () => {
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset Camera Position
|
||||
|
@ -28,7 +28,6 @@ import {
|
||||
OnMouseEnterLeaveArgs,
|
||||
RAYCASTABLE_PLANE,
|
||||
SEGMENT_LENGTH_LABEL,
|
||||
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
||||
SEGMENT_LENGTH_LABEL_TEXT,
|
||||
SKETCH_GROUP_SEGMENTS,
|
||||
SKETCH_LAYER,
|
||||
@ -102,8 +101,8 @@ import {
|
||||
getRectangleCallExpressions,
|
||||
updateRectangleSketch,
|
||||
} from 'lib/rectangleTool'
|
||||
import { getThemeColorForThreeJs } from 'lib/theme'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||
|
||||
@ -324,6 +323,7 @@ export class SceneEntities {
|
||||
)
|
||||
}
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick: async (args) => {
|
||||
if (!args) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
@ -634,6 +634,7 @@ export class SceneEntities {
|
||||
draftExpressionsIndices,
|
||||
})
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick: async (args) => {
|
||||
if (!args) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
@ -701,7 +702,7 @@ export class SceneEntities {
|
||||
if (profileStart) {
|
||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||
} else {
|
||||
this.setUpDraftSegment(
|
||||
await this.setUpDraftSegment(
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
up,
|
||||
@ -771,6 +772,7 @@ export class SceneEntities {
|
||||
})
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onMove: async (args) => {
|
||||
// Update the width and height of the draft rectangle
|
||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||
@ -818,6 +820,7 @@ export class SceneEntities {
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick: async (args) => {
|
||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||
const cornerPoint = args.intersectionPoint?.twoD
|
||||
@ -892,9 +895,11 @@ export class SceneEntities {
|
||||
}) => {
|
||||
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onDragEnd: async () => {
|
||||
if (addingNewSegmentStatus !== 'nothing') {
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.setupSketch({
|
||||
sketchPathToNode: pathToNode,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
@ -911,6 +916,7 @@ export class SceneEntities {
|
||||
})
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onDrag: async ({
|
||||
selected,
|
||||
intersectionPoint,
|
||||
@ -958,6 +964,7 @@ export class SceneEntities {
|
||||
|
||||
await kclManager.executeAstMock(mod.modifiedAst)
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.setupSketch({
|
||||
sketchPathToNode: pathToNode,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
@ -1161,7 +1168,7 @@ export class SceneEntities {
|
||||
)
|
||||
)
|
||||
sceneInfra.overlayCallbacks(callBacks)
|
||||
})()
|
||||
})().catch(reportRejection)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1414,20 +1421,14 @@ export class SceneEntities {
|
||||
) as CSS2DObject
|
||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `${roundOff(length)}${sceneInfra._baseUnit}`
|
||||
label.innerText = `${roundOff(length)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
const offsetFromMidpoint = new Vector2(to[0] - from[0], to[1] - from[1])
|
||||
.normalize()
|
||||
.rotateAround(new Vector2(0, 0), Math.PI / 2)
|
||||
.multiplyScalar(SEGMENT_LENGTH_LABEL_OFFSET_PX * scale)
|
||||
label.style.setProperty('--x', `${offsetFromMidpoint.x}px`)
|
||||
label.style.setProperty('--y', `${offsetFromMidpoint.y}px`)
|
||||
labelWrapper.position.set(
|
||||
(from[0] + to[0]) / 2 + offsetFromMidpoint.x,
|
||||
(from[1] + to[1]) / 2 + offsetFromMidpoint.y,
|
||||
0
|
||||
)
|
||||
|
||||
const slope = (to[1] - from[1]) / (to[0] - from[0])
|
||||
let slopeAngle = ((Math.atan(slope) * 180) / Math.PI) * -1
|
||||
label.style.setProperty('--degree', `${slopeAngle}deg`)
|
||||
label.style.setProperty('--x', `0px`)
|
||||
label.style.setProperty('--y', `0px`)
|
||||
labelWrapper.position.set((from[0] + to[0]) / 2, (from[1] + to[1]) / 2, 0)
|
||||
labelGroup.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
@ -1465,6 +1466,25 @@ export class SceneEntities {
|
||||
to,
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Update the base color of each of the THREEjs meshes
|
||||
* that represent each of the sketch segments, to get the
|
||||
* latest value from `sceneInfra._theme`
|
||||
*/
|
||||
updateSegmentBaseColor(newColor: Themes.Light | Themes.Dark) {
|
||||
const newColorThreeJs = getThemeColorForThreeJs(newColor)
|
||||
Object.values(this.activeSegments).forEach((group) => {
|
||||
group.userData.baseColor = newColorThreeJs
|
||||
group.traverse((child) => {
|
||||
if (
|
||||
child instanceof Mesh &&
|
||||
child.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
child.material.color.set(newColorThreeJs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
removeSketchGrid() {
|
||||
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
||||
}
|
||||
|
@ -42,7 +42,13 @@ export type ActionButtonProps =
|
||||
|
||||
export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
|
||||
const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||
props.iconStart ? (props.iconEnd ? 'px-0' : 'pr-2') : 'px-2'
|
||||
props.iconStart
|
||||
? props.iconEnd
|
||||
? 'px-0'
|
||||
: 'pr-2'
|
||||
: props.iconEnd
|
||||
? 'px-2'
|
||||
: 'pl-2'
|
||||
} ${props.className ? props.className : ''}`
|
||||
|
||||
switch (props.Element) {
|
||||
|
@ -35,7 +35,7 @@ export const ActionIcon = ({
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`w-fit inline-grid place-content-center ${className} ` +
|
||||
`w-fit self-stretch inline-grid place-content-center ${className} ` +
|
||||
computedBgClassName
|
||||
}
|
||||
>
|
||||
|
@ -4,4 +4,14 @@
|
||||
*/
|
||||
.header {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.header.desktopApp {
|
||||
/* Make the header act as a handle to drag the electron app window,
|
||||
* per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region
|
||||
* all interactive elements opt-out of this behavior by default in src/index.css
|
||||
*/
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
@ -6,12 +6,14 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { RefreshButton } from 'components/RefreshButton'
|
||||
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
project?: Omit<IndexLoaderData, 'code'>
|
||||
className?: string
|
||||
enableMenu?: boolean
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const AppHeader = ({
|
||||
@ -19,6 +21,7 @@ export const AppHeader = ({
|
||||
project,
|
||||
children,
|
||||
className = '',
|
||||
style,
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
@ -30,9 +33,12 @@ export const AppHeader = ({
|
||||
className={
|
||||
'w-full grid ' +
|
||||
styles.header +
|
||||
' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
|
||||
` ${
|
||||
isDesktop() ? styles.desktopApp + ' ' : ''
|
||||
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
|
||||
className
|
||||
}
|
||||
style={style}
|
||||
>
|
||||
<ProjectSidebarMenu
|
||||
enableMenu={enableMenu}
|
||||
|
@ -151,6 +151,7 @@ export function useCalc({
|
||||
})
|
||||
if (trap(error)) return
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||
import { throttle, isReducedMotion } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const updateDollyZoom = throttle(
|
||||
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||
@ -16,8 +17,8 @@ export const CamToggle = () => {
|
||||
useEffect(() => {
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.SceneReady,
|
||||
async () => {
|
||||
sceneInfra.camControls.dollyZoom(fov)
|
||||
() => {
|
||||
sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
@ -26,11 +27,11 @@ export const CamToggle = () => {
|
||||
if (isPerspective) {
|
||||
isReducedMotion()
|
||||
? sceneInfra.camControls.useOrthographicCamera()
|
||||
: sceneInfra.camControls.animateToOrthographic()
|
||||
: sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
|
||||
} else {
|
||||
isReducedMotion()
|
||||
? sceneInfra.camControls.usePerspectiveCamera()
|
||||
: sceneInfra.camControls.animateToPerspective()
|
||||
? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
|
||||
: sceneInfra.camControls.animateToPerspective().catch(reportRejection)
|
||||
}
|
||||
setIsPerspective(!isPerspective)
|
||||
}
|
||||
|
@ -71,6 +71,17 @@ function CommandArgOptionInput({
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.select()
|
||||
}, [inputRef])
|
||||
useEffect(() => {
|
||||
// work around to make sure the user doesn't have to press the down arrow key to focus the first option
|
||||
// instead this makes it move from the first hit
|
||||
const downArrowEvent = new KeyboardEvent('keydown', {
|
||||
key: 'ArrowDown',
|
||||
keyCode: 40,
|
||||
which: 40,
|
||||
bubbles: true,
|
||||
})
|
||||
inputRef?.current?.dispatchEvent(downArrowEvent)
|
||||
}, [])
|
||||
|
||||
// Filter the options based on the query,
|
||||
// resetting the query when the options change
|
||||
|
@ -1,53 +1,43 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { createActorContext } from '@xstate/react'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { createContext, useEffect } from 'react'
|
||||
import { EventFrom, StateFrom } from 'xstate'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type CommandsContextType = {
|
||||
commandBarState: StateFrom<typeof commandBarMachine>
|
||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
||||
}
|
||||
|
||||
export const CommandsContext = createContext<CommandsContextType>({
|
||||
commandBarState: commandBarMachine.initialState,
|
||||
commandBarSend: () => {},
|
||||
})
|
||||
|
||||
export const CommandBarProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||
devTools: true,
|
||||
export const CommandsContext = createActorContext(
|
||||
commandBarMachine.provide({
|
||||
guards: {
|
||||
'Command has no arguments': (context, _event) => {
|
||||
'Command has no arguments': ({ context }) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
Object.keys(context.selectedCommand?.args).length === 0
|
||||
)
|
||||
},
|
||||
'All arguments are skippable': (context, _event) => {
|
||||
'All arguments are skippable': ({ context }) => {
|
||||
return Object.values(context.selectedCommand!.args!).every(
|
||||
(argConfig) => argConfig.skip
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.setCommandBarSend(commandBarSend)
|
||||
})
|
||||
|
||||
export const CommandBarProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<CommandsContext.Provider
|
||||
value={{
|
||||
commandBarState,
|
||||
commandBarSend,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CommandsContext.Provider>
|
||||
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
||||
</CommandsContext.Provider>
|
||||
)
|
||||
}
|
||||
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.setCommandBarSend(commandBarActor.send)
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
e.preventDefault()
|
||||
commandBarSend({
|
||||
type: 'Submit command',
|
||||
data: argumentsToSubmit,
|
||||
output: argumentsToSubmit,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
||||
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const initSelectionsByType = useCallback(() => {
|
||||
const selectionsByType = useMemo(() => {
|
||||
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||
? 'none'
|
||||
: getSelectionType(selection)
|
||||
}, [selection, code])
|
||||
const selectionsByType = initSelectionsByType()
|
||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
||||
canSubmitSelectionArg(selectionsByType, arg)
|
||||
const canSubmitSelection = useMemo<boolean>(
|
||||
() => canSubmitSelectionArg(selectionsByType, arg),
|
||||
[selectionsByType]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
console.log('selection input effect', {
|
||||
selectionsByType,
|
||||
canSubmitSelection,
|
||||
arg,
|
||||
})
|
||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||
handleSubmit({
|
||||
preventDefault: () => {},
|
||||
} as React.FormEvent<HTMLFormElement>)
|
||||
handleSubmit()
|
||||
}
|
||||
}, [selectionsByType, arg])
|
||||
}, [canSubmitSelection])
|
||||
|
||||
function handleChange() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
|
||||
if (!canSubmitSelection) {
|
||||
setHasSubmitted(true)
|
||||
|
@ -11,6 +11,7 @@ export function CommandBarOpenButton() {
|
||||
<button
|
||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||
onClick={() => commandBarSend({ type: 'Open' })}
|
||||
data-testid="command-bar-open-button"
|
||||
>
|
||||
<span>Commands</span>
|
||||
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
|
||||
|
@ -135,16 +135,15 @@ interface ContextMenuItemProps {
|
||||
icon?: ActionIconProps['icon']
|
||||
onClick?: () => void
|
||||
hotkey?: string
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
export function ContextMenuItem({
|
||||
children,
|
||||
icon,
|
||||
onClick,
|
||||
hotkey,
|
||||
}: ContextMenuItemProps) {
|
||||
export function ContextMenuItem(props: ContextMenuItemProps) {
|
||||
const { children, icon, onClick, hotkey } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
data-testid={props['data-testid']}
|
||||
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -332,7 +332,7 @@ const CustomIconMap = {
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.5 4C4.11929 4 3 5.11929 3 6.5V7C3 10.0376 5.46243 12.5 8.5 12.5H8.96482C9.46635 12.5 9.93469 12.2493 10.2129 11.8321L10.5173 11.3755C11.1396 12.0849 12.0423 12.5 13 12.5H13.75H15V14C15 14.2626 14.9483 14.5227 14.8478 14.7654C14.7472 15.008 14.5999 15.2285 14.4142 15.4142C14.2285 15.5999 14.008 15.7472 13.7654 15.8478C13.5227 15.9483 13.2626 16 13 16C12.7374 16 12.4773 15.9483 12.2346 15.8478C11.992 15.7472 11.7715 15.5999 11.5858 15.4142C11.4001 15.2285 11.2528 15.008 11.1522 14.7654C11.1164 14.6789 11.0868 14.5902 11.0635 14.5H11.8544C11.9168 14.6431 12.0056 14.7734 12.1161 14.8839C12.2322 15 12.37 15.092 12.5216 15.1548C12.6733 15.2177 12.8358 15.25 13 15.25C13.1642 15.25 13.3267 15.2177 13.4784 15.1548C13.63 15.092 13.7678 15 13.8839 14.8839C14 14.7678 14.092 14.63 14.1548 14.4784C14.2177 14.3267 14.25 14.1642 14.25 14V13H13.25V14C13.25 14.0328 13.2435 14.0653 13.231 14.0957C13.2184 14.126 13.2 14.1536 13.1768 14.1768C13.1536 14.2 13.126 14.2184 13.0957 14.231C13.0653 14.2435 13.0328 14.25 13 14.25C12.9672 14.25 12.9347 14.2435 12.9043 14.231C12.874 14.2184 12.8464 14.2 12.8232 14.1768C12.8 14.1536 12.7816 14.126 12.769 14.0957C12.7565 14.0653 12.75 14.0328 12.75 14V13.5H12.25H10.5H10V14C10 14.394 10.0776 14.7841 10.2284 15.1481C10.3791 15.512 10.6001 15.8427 10.8787 16.1213C11.1573 16.3999 11.488 16.6209 11.8519 16.7716C12.2159 16.9224 12.606 17 13 17C13.394 17 13.7841 16.9224 14.1481 16.7716C14.512 16.6209 14.8427 16.3999 15.1213 16.1213C15.3999 15.8427 15.6209 15.512 15.7716 15.1481C15.9224 14.7841 16 14.394 16 14V12.5H17V11.5H16V8.5C16 6.01472 13.9853 4 11.5 4H5.5ZM11.084 10.4746L10.9226 10.2326L9.42875 7.74275L8.57125 8.25725L9.90846 10.4859L9.38084 11.2773C9.28811 11.4164 9.13199 11.5 8.96482 11.5H8.5C6.01472 11.5 4 9.48528 4 7V6.5C4 5.67157 4.67157 5 5.5 5H11.5C13.433 5 15 6.567 15 8.5V11.5H13.75H13C12.2301 11.5 11.5111 11.1152 11.084 10.4746ZM13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5Z"
|
||||
fill="black"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { CommandLog } from 'lang/std/engineConnection'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
function useEngineCommands(): [CommandLog[], () => void] {
|
||||
export function useEngineCommands(): [CommandLog[], () => void] {
|
||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
|
||||
engineCommandManager.commandLogs
|
||||
)
|
||||
@ -77,9 +78,11 @@ export const EngineCommands = () => {
|
||||
/>
|
||||
<button
|
||||
data-testid="custom-cmd-send-button"
|
||||
onClick={() =>
|
||||
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
|
||||
}
|
||||
onClick={() => {
|
||||
engineCommandManager
|
||||
.sendSceneCommand(JSON.parse(customCmd))
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Send custom command
|
||||
</button>
|
||||
|
@ -5,25 +5,28 @@ import { PATHS } from 'lib/paths'
|
||||
import React, { createContext } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
Actor,
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
EventFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
assign,
|
||||
fromPromise,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||
import {
|
||||
DEFAULT_FILE_NAME,
|
||||
DEFAULT_PROJECT_KCL_FILE,
|
||||
FILE_EXT,
|
||||
} from 'lib/constants'
|
||||
import { getProjectInfo } from 'lib/desktop'
|
||||
import { getNextDirName, getNextFileName } from 'lib/desktopFS'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
export const FileContext = createContext(
|
||||
@ -39,199 +42,234 @@ export const FileMachineProvider = ({
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
|
||||
const [state, send] = useMachine(fileMachine, {
|
||||
context: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
actions: {
|
||||
navigateToFile: (context, event) => {
|
||||
if (event.data && 'name' in event.data) {
|
||||
commandBarSend({ type: 'Close' })
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
context.selectedDirectory +
|
||||
window.electron.path.sep +
|
||||
event.data.name
|
||||
)}`
|
||||
const [state, send] = useMachine(
|
||||
fileMachine.provide({
|
||||
actions: {
|
||||
renameToastSuccess: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
createToastSuccess: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
toastSuccess: ({ event }) => {
|
||||
if (
|
||||
event.type !== 'xstate.done.actor.rename-file' &&
|
||||
event.type !== 'xstate.done.actor.delete-file'
|
||||
)
|
||||
} else if (
|
||||
event.data &&
|
||||
'path' in event.data &&
|
||||
event.data.path.endsWith(FILE_EXT)
|
||||
) {
|
||||
// Don't navigate to newly created directories
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
||||
}
|
||||
return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
toastError: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||
toast.error(event.output.message)
|
||||
},
|
||||
navigateToFile: ({ context, event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
if (event.output && 'name' in event.output) {
|
||||
commandBarSend({ type: 'Close' })
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
context.selectedDirectory +
|
||||
window.electron.path.sep +
|
||||
event.output.name
|
||||
)}`
|
||||
)
|
||||
} else if (
|
||||
event.output &&
|
||||
'path' in event.output &&
|
||||
event.output.path.endsWith(FILE_EXT)
|
||||
) {
|
||||
// Don't navigate to newly created directories
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
addFileToRenamingQueue: assign({
|
||||
itemsBeingRenamed: (context, event) => [
|
||||
...context.itemsBeingRenamed,
|
||||
event.data.path,
|
||||
],
|
||||
}),
|
||||
removeFileFromRenamingQueue: assign({
|
||||
itemsBeingRenamed: (
|
||||
context,
|
||||
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
|
||||
) =>
|
||||
context.itemsBeingRenamed.filter(
|
||||
(path) => path !== event.data.oldPath
|
||||
),
|
||||
}),
|
||||
renameToastSuccess: (_, event) => toast.success(event.data.message),
|
||||
createToastSuccess: (_, event) => toast.success(event.data.message),
|
||||
toastSuccess: (_, event) =>
|
||||
event.data && toast.success((event.data || '') + ''),
|
||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
||||
},
|
||||
services: {
|
||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||
const newFiles = isDesktop()
|
||||
? (await getProjectInfo(context.project.path)).children
|
||||
: []
|
||||
return {
|
||||
...context.project,
|
||||
children: newFiles,
|
||||
}
|
||||
},
|
||||
createAndOpenFile: async (context, event) => {
|
||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
actors: {
|
||||
readFiles: fromPromise(async ({ input }) => {
|
||||
const newFiles =
|
||||
(isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
|
||||
[]
|
||||
return {
|
||||
...input,
|
||||
children: newFiles,
|
||||
}
|
||||
}),
|
||||
createAndOpenFile: fromPromise(async ({ input }) => {
|
||||
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.mkdir(createdPath)
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully created "${createdName}"`,
|
||||
path: createdPath,
|
||||
}
|
||||
},
|
||||
createFile: async (context, event) => {
|
||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.mkdir(createdPath)
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
||||
}
|
||||
|
||||
return {
|
||||
path: createdPath,
|
||||
}
|
||||
},
|
||||
renameFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
const name = newName
|
||||
? newName.endsWith(FILE_EXT) || isDir
|
||||
? newName
|
||||
: newName + FILE_EXT
|
||||
: DEFAULT_FILE_NAME
|
||||
const oldPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
oldName
|
||||
)
|
||||
const newPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
name
|
||||
)
|
||||
|
||||
window.electron.rename(oldPath, newPath)
|
||||
|
||||
if (!file) {
|
||||
return Promise.reject(new Error('file is not defined'))
|
||||
}
|
||||
|
||||
if (oldPath === file.path && project?.path) {
|
||||
// If we just renamed the current file, navigate to the new path
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||
} else if (file?.path.includes(oldPath)) {
|
||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
file.path.replace(oldPath, newPath)
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
||||
newPath,
|
||||
oldPath,
|
||||
}
|
||||
},
|
||||
deleteFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
||||
) => {
|
||||
const isDir = !!event.data.children
|
||||
|
||||
if (isDir) {
|
||||
await window.electron
|
||||
.rm(event.data.path, {
|
||||
recursive: true,
|
||||
if (input.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
.catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await window.electron
|
||||
.rm(event.data.path)
|
||||
.catch((e) => console.error('Error deleting file', e))
|
||||
}
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.mkdir(createdPath)
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||
}
|
||||
|
||||
// If we just deleted the current file or one of its parent directories,
|
||||
// navigate to the project root
|
||||
if (
|
||||
(event.data.path === file?.path ||
|
||||
file?.path.includes(event.data.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||
}
|
||||
return {
|
||||
message: `Successfully created "${createdName}"`,
|
||||
path: createdPath,
|
||||
}
|
||||
}),
|
||||
createFile: fromPromise(async ({ input }) => {
|
||||
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
event.data.name
|
||||
}"`
|
||||
if (input.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.mkdir(createdPath)
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||
}
|
||||
|
||||
return {
|
||||
path: createdPath,
|
||||
}
|
||||
}),
|
||||
renameFile: fromPromise(async ({ input }) => {
|
||||
const { oldName, newName, isDir } = input
|
||||
const name = newName
|
||||
? newName.endsWith(FILE_EXT) || isDir
|
||||
? newName
|
||||
: newName + FILE_EXT
|
||||
: DEFAULT_FILE_NAME
|
||||
const oldPath = window.electron.path.join(
|
||||
input.selectedDirectory.path,
|
||||
oldName
|
||||
)
|
||||
const newPath = window.electron.path.join(
|
||||
input.selectedDirectory.path,
|
||||
name
|
||||
)
|
||||
|
||||
// no-op
|
||||
if (oldPath === newPath) {
|
||||
return {
|
||||
message: `Old is the same as new.`,
|
||||
newPath,
|
||||
oldPath,
|
||||
}
|
||||
}
|
||||
|
||||
// if there are any siblings with the same name, report error.
|
||||
const entries = await window.electron.readdir(
|
||||
window.electron.path.dirname(newPath)
|
||||
)
|
||||
for (let entry of entries) {
|
||||
if (entry === newName) {
|
||||
return Promise.reject(new Error('Filename already exists.'))
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.rename(oldPath, newPath)
|
||||
|
||||
if (!file) {
|
||||
return Promise.reject(new Error('file is not defined'))
|
||||
}
|
||||
|
||||
if (oldPath === file.path && project?.path) {
|
||||
// If we just renamed the current file, navigate to the new path
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||
} else if (file?.path.includes(oldPath)) {
|
||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
file.path.replace(oldPath, newPath)
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
||||
newPath,
|
||||
oldPath,
|
||||
}
|
||||
}),
|
||||
deleteFile: fromPromise(async ({ input }) => {
|
||||
const isDir = !!input.children
|
||||
|
||||
if (isDir) {
|
||||
await window.electron
|
||||
.rm(input.path, {
|
||||
recursive: true,
|
||||
})
|
||||
.catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await window.electron
|
||||
.rm(input.path)
|
||||
.catch((e) => console.error('Error deleting file', e))
|
||||
}
|
||||
|
||||
// If there are no more files at all in the project, create a main.kcl
|
||||
// for when we navigate to the root.
|
||||
if (!project?.path) {
|
||||
return Promise.reject(new Error('Project path not set.'))
|
||||
}
|
||||
|
||||
const entries = await window.electron.readdir(project.path)
|
||||
const hasKclEntries =
|
||||
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
|
||||
if (!hasKclEntries) {
|
||||
await window.electron.writeFile(
|
||||
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
|
||||
''
|
||||
)
|
||||
// Refresh the route selected above because it's possible we're on
|
||||
// the same path on the navigate, which doesn't cause anything to
|
||||
// refresh, leaving a stale execution state.
|
||||
navigate(0)
|
||||
return {
|
||||
message: 'No more files in project, created main.kcl',
|
||||
}
|
||||
}
|
||||
|
||||
// If we just deleted the current file or one of its parent directories,
|
||||
// navigate to the project root
|
||||
if (
|
||||
(input.path === file?.path || file?.path.includes(input.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
input.name
|
||||
}"`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
|
||||
if (event.type !== 'done.invoke.read-files') return false
|
||||
return !!event?.data?.children && event.data.children.length > 0
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
'Is not silent': (_, event) => !event.data?.silent,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<FileContext.Provider
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { FileEntry, IndexLoaderData } from 'lib/types'
|
||||
import type { IndexLoaderData } from 'lib/types'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import Tooltip from './Tooltip'
|
||||
@ -20,6 +20,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { FileEntry } from 'lib/project'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
@ -175,13 +176,12 @@ const FileTreeItem = ({
|
||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||
codeManager.code
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
codeManager.writeToFile()
|
||||
|
||||
// Prevent seeing the model built one piece at a time when changing files
|
||||
kclManager.isFirstRender = true
|
||||
kclManager.executeCode(true).then(() => {
|
||||
kclManager.isFirstRender = false
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.executeCode(true)
|
||||
} else {
|
||||
// Let the lsp servers know we closed a file.
|
||||
onFileClose(currentFile?.path || null, project?.path || null)
|
||||
@ -245,13 +245,13 @@ const FileTreeItem = ({
|
||||
onClickCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
@ -298,13 +298,13 @@ const FileTreeItem = ({
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -357,10 +357,18 @@ function FileTreeContextMenu({
|
||||
<ContextMenu
|
||||
menuTargetElement={itemRef}
|
||||
items={[
|
||||
<ContextMenuItem onClick={onRename} hotkey="Enter">
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-rename"
|
||||
onClick={onRename}
|
||||
hotkey="Enter"
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-delete"
|
||||
onClick={onDelete}
|
||||
hotkey={metaKey + ' + Del'}
|
||||
>
|
||||
Delete
|
||||
</ContextMenuItem>,
|
||||
]}
|
||||
@ -382,14 +390,14 @@ interface FileTreeProps {
|
||||
export const FileTreeMenu = () => {
|
||||
const { send } = useFileContext()
|
||||
|
||||
async function createFile() {
|
||||
function createFile() {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: false },
|
||||
})
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
function createFolder() {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: true },
|
||||
@ -476,7 +484,7 @@ export const FileTreeInner = ({
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileContext.project,
|
||||
directory: fileContext.project,
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
} from './ContextMenu'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
@ -67,7 +68,9 @@ export default function Gizmo() {
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
@ -75,7 +78,7 @@ export default function Gizmo() {
|
||||
)),
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
@ -299,7 +302,7 @@ const initializeMouseEvents = (
|
||||
const handleClick = () => {
|
||||
if (raycasterIntersect.current) {
|
||||
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||
sceneInfra.camControls.updateCameraToAxis(axisName)
|
||||
sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { createAndOpenNewProject } from 'lib/desktopFS'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const HelpMenuDivider = () => (
|
||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
if (isInProject) {
|
||||
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
||||
} else {
|
||||
createAndOpenNewProject({ onProjectOpen, navigate })
|
||||
createAndOpenNewProject({ onProjectOpen, navigate }).catch(
|
||||
reportRejection
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
import { Spinner } from './Spinner'
|
||||
|
||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
|
||||
@ -65,17 +67,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||
data-testid="loading"
|
||||
>
|
||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||
<circle
|
||||
cx="5"
|
||||
cy="5"
|
||||
r="4"
|
||||
stroke="var(--primary)"
|
||||
fill="none"
|
||||
strokeDasharray="4, 4"
|
||||
className="animate-spin origin-center"
|
||||
/>
|
||||
</svg>
|
||||
<Spinner />
|
||||
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
|
||||
<p
|
||||
className={
|
||||
|
@ -11,6 +11,8 @@ import toast from 'react-hot-toast'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||
import { ModelStateIndicator } from './ModelStateIndicator'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export function LowerRightControls({
|
||||
children,
|
||||
@ -24,7 +26,7 @@ export function LowerRightControls({
|
||||
const linkOverrideClassName =
|
||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||
|
||||
async function reportbug(event: {
|
||||
function reportbug(event: {
|
||||
preventDefault: () => void
|
||||
stopPropagation: () => void
|
||||
}) {
|
||||
@ -33,7 +35,9 @@ export function LowerRightControls({
|
||||
|
||||
if (!coreDumpManager) {
|
||||
// open default reporting option
|
||||
openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose')
|
||||
openWindow(
|
||||
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||
).catch(reportRejection)
|
||||
} else {
|
||||
toast
|
||||
.promise(
|
||||
@ -55,7 +59,7 @@ export function LowerRightControls({
|
||||
if (err) {
|
||||
openWindow(
|
||||
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||
)
|
||||
).catch(reportRejection)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -65,6 +69,7 @@ export function LowerRightControls({
|
||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
||||
{children}
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
||||
|
@ -15,7 +15,7 @@ import { Extension } from '@codemirror/state'
|
||||
import { LanguageSupport } from '@codemirror/language'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { FileEntry } from 'lib/types'
|
||||
import { FileEntry } from 'lib/project'
|
||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
||||
import {
|
||||
KclWorkerOptions,
|
||||
@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
// 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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.updateFoldingRanges()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.requestSemanticTokens()
|
||||
break
|
||||
case 'kcl/memoryUpdated':
|
||||
|
45
src/components/ModelStateIndicator.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useEngineCommands } from './EngineCommands'
|
||||
import { Spinner } from './Spinner'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
export const ModelStateIndicator = () => {
|
||||
const [commands] = useEngineCommands()
|
||||
|
||||
const lastCommandType = commands[commands.length - 1]?.type
|
||||
|
||||
let className = 'w-6 h-6 '
|
||||
let icon = <Spinner className={className} />
|
||||
let dataTestId = 'model-state-indicator'
|
||||
|
||||
if (lastCommandType === 'receive-reliable') {
|
||||
className +=
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||
icon = (
|
||||
<CustomIcon
|
||||
data-testid={dataTestId + '-receive-reliable'}
|
||||
name="checkmark"
|
||||
/>
|
||||
)
|
||||
} else if (lastCommandType === 'execution-done') {
|
||||
className +=
|
||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||
icon = (
|
||||
<CustomIcon
|
||||
data-testid={dataTestId + '-execution-done'}
|
||||
name="checkmark"
|
||||
/>
|
||||
)
|
||||
} else if (lastCommandType === 'export-done') {
|
||||
className +=
|
||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||
icon = (
|
||||
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} data-testid="model-state-indicator">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => handleConvertToVarClick()}
|
||||
onClick={() => {
|
||||
handleConvertToVarClick().catch(reportRejection)
|
||||
}}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
|
@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
icon: 'printer3d',
|
||||
iconClassName: '!p-0',
|
||||
keybinding: 'Ctrl + Shift + M',
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
action: async () => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
|
@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
|
||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[NetworkHealthState.Ok]: 'Connected',
|
||||
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
|
||||
</div>
|
||||
{issues[name as ConnectingTypeGroup] && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
onClick={toSync(async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
JSON.stringify(error, null, 2) || ''
|
||||
)
|
||||
setHasCopied(true)
|
||||
setTimeout(() => setHasCopied(false), 5000)
|
||||
}}
|
||||
}, reportRejection)}
|
||||
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
|
||||
>
|
||||
{hasCopied ? 'Copied' : 'Copy Error'}
|
||||
|
@ -49,7 +49,11 @@ export const NetworkMachineIndicator = ({
|
||||
{Object.entries(machineManager.machines).map(
|
||||
([hostname, machine]) => (
|
||||
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
|
||||
<p className="">{machine.model || machine.manufacturer}</p>
|
||||
<p className="">
|
||||
{machine.make_model.model ||
|
||||
machine.make_model.manufacturer ||
|
||||
'Unknown Machine'}
|
||||
</p>
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
Hostname {hostname}
|
||||
</p>
|
||||
|
@ -7,7 +7,9 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from '../Tooltip'
|
||||
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
||||
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project } from 'lib/project'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
function ProjectCard({
|
||||
project,
|
||||
@ -165,10 +167,10 @@ function ProjectCard({
|
||||
{isConfirmingDelete && (
|
||||
<DeleteConfirmationDialog
|
||||
title="Delete Project"
|
||||
onConfirm={async () => {
|
||||
onConfirm={toSync(async () => {
|
||||
await handleDeleteProject(project)
|
||||
setIsConfirmingDelete(false)
|
||||
}}
|
||||
}, reportRejection)}
|
||||
onDismiss={() => setIsConfirmingDelete(false)}
|
||||
>
|
||||
<p className="my-4">
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { HTMLProps, forwardRef } from 'react'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
|
||||
project: Project
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project } from 'lib/project'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
const now = new Date()
|
||||
const projectWellFormed = {
|
||||
|
@ -25,8 +25,17 @@ const ProjectSidebarMenu = ({
|
||||
project?: IndexLoaderData['project']
|
||||
file?: IndexLoaderData['file']
|
||||
}) => {
|
||||
// Make room for traffic lights on desktop left side.
|
||||
// TODO: make sure this doesn't look like shit on Linux or Windows
|
||||
const trafficLightsOffset =
|
||||
isDesktop() && window.electron.os.isMac ? 'ml-20' : ''
|
||||
return (
|
||||
<div className="!no-underline h-full mr-auto max-h-min min-h-12 min-w-max flex items-center gap-2">
|
||||
<div
|
||||
className={
|
||||
'!no-underline h-full mr-auto max-h-min min-h-12 min-w-max flex items-center gap-2 ' +
|
||||
trafficLightsOffset
|
||||
}
|
||||
>
|
||||
<AppLogoLink project={project} file={file} />
|
||||
{enableMenu ? (
|
||||
<ProjectMenuPopover project={project} file={file} />
|
||||
|
@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import Tooltip from './Tooltip'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
|
||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||
// Window may not be available in some environments
|
||||
window?.location.reload()
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={refresh}
|
||||
onClick={toSync(refresh, reportRejection)}
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
||||
>
|
||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||
|