Merge remote-tracking branch 'origin' into kurt-circle

This commit is contained in:
Kurt Hutten Irev-Dev
2024-09-07 09:09:33 +10:00
108 changed files with 7849 additions and 1047 deletions

View File

@ -2,7 +2,9 @@ NODE_ENV=development
DEV=true DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev 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_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 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"

View File

@ -1,4 +1,4 @@
name: build-test-publish-apps name: build-publish-apps
on: on:
pull_request: pull_request:
@ -21,7 +21,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
prepare-json-files: prepare-files:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows) runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs: outputs:
version: ${{ steps.export_version.outputs.version }} version: ${{ steps.export_version.outputs.version }}
@ -33,6 +33,19 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' 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 - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
run: | 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 # 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 - uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with: with:
name: prepared-files
path: | path: |
package.json package.json
src/wasm-lib/pkg/wasm_lib*
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-app-macos: build-apps:
needs: [prepare-json-files] needs: [prepare-files]
runs-on: macos-14 strategy:
fail-fast: false
matrix:
os: [macos-14, windows-2022, ubuntu-22.04]
runs-on: ${{ matrix.os }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
if: github.event_name == 'schedule' name: prepared-files
- name: Copy updated .json files - name: Copy prepared files
if: github.event_name == 'schedule'
run: | run: |
ls -l artifact ls -R prepared-files
cp artifact/package.json package.json 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 - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -81,79 +106,10 @@ jobs:
- run: yarn install - run: yarn install
- name: Setup Rust - run: yarn tronb:vite
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
- name: Prepare certificate and variables (Windows only) - name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
run: | run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12 cat /d/Certificate_pkcs12.p12
@ -168,7 +124,7 @@ jobs:
shell: bash shell: bash
- name: Setup certicate with SSM KSP (Windows only) - name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
run: | 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 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 msiexec /i smtools-windows-x64.msi /quiet /qn
@ -178,83 +134,22 @@ jobs:
smksp_cert_sync.exe smksp_cert_sync.exe
shell: cmd shell: cmd
- name: Build the app for x64 - name: Build the app
run: "yarn electron-forge make --arch x64" run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
- name: Build the app for arm64 - name: List artifacts in out/
run: "yarn electron-forge make --arch arm64" run: ls -R out
- 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 }}"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
path: "out/make/*/*/*" name: out-${{ matrix.os }}
path: |
# TODO: Run e2e tests out/Zoo*.*
out/latest*.yml
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"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
# TODO: sign the app (and updater bundle potentially) # TODO: add the updater tests back
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*"
publish-apps-release: publish-apps-release:
@ -262,88 +157,76 @@ jobs:
permissions: permissions:
contents: write contents: write
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} 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: env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-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 }} 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) }} NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }} BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }} 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' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/checkout@v4
- name: Generate the update static endpoint - uses: actions/download-artifact@v3
run: | with:
ls -l artifact/*/*oo* name: out-windows-2022
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig` path: out
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig` - uses: actions/download-artifact@v3
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION} with:
jq --null-input \ name: out-macos-14
--arg version "${VERSION}" \ path: out
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \ - uses: actions/download-artifact@v3
--arg darwin_sig "$DARWIN_SIG" \ with:
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \ name: out-ubuntu-22.04
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \ path: out
--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
- name: Generate the download static endpoint - name: Generate the download static endpoint
run: | run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION} RELEASE_DIR=https://${WEBSITE_DIR}
jq --null-input \ jq --null-input \
--arg version "${VERSION}" \ --arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \ --arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \ --arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \ --arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \ --arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \ --arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.msi" \
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.msi" \
--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, "version": $version,
"pub_date": $pub_date, "pub_date": $pub_date,
"notes": $notes, "notes": $notes,
"platforms": { "platforms": {
"dmg-universal": { "dmg-arm64": {
"url": $darwin_url "url": $mac_arm64_url
}, },
"msi-x86_64": { "dmg-x64": {
"url": $windows_x86_64_url "url": $mac_x64_url
}, },
"msi-aarch64": { "msi-arm64": {
"url": $windows_aarch64_url "url": $windows_arm64_url
},
"msi-x64": {
"url": $windows_x64_url
},
"appimage-arm64": {
"url": $linux_arm64_url
},
"appimage-x64": {
"url": $linux_x64_url
} }
} }
}' > last_download.json }' > last_download.json
cat last_download.json cat last_download.json
- name: List artifacts
run: "ls -R out"
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.5' uses: 'google-github-actions/auth@v2.1.5'
with: with:
@ -352,24 +235,44 @@ jobs:
- name: Set up Google Cloud SDK - name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0 uses: google-github-actions/setup-gcloud@v2.1.0
with: with:
project_id: kittycadapi project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket - 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: with:
path: artifact path: out
glob: '*/Zoo*' glob: 'Zoo*'
parent: false 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 }} 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 - 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: with:
path: last_download.json path: last_download.json
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
@ -378,7 +281,9 @@ jobs:
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: 'artifact/*/Zoo*' files: 'out/Zoo*'
# TODO: Add GitHub publisher
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]

View File

@ -28,6 +28,7 @@ jobs:
dir: ['src/wasm-lib'] dir: ['src/wasm-lib']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: taiki-e/install-action@just
- name: Install latest rust - name: Install latest rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -41,7 +42,7 @@ jobs:
- name: Run clippy - name: Run clippy
run: | run: |
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings just lint
# If this fails, run "cargo check" to update Cargo.lock, # If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR. # then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating - name: Check Cargo.lock doesn't need updating

View File

@ -262,7 +262,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-14] os: [ubuntu-latest, windows-latest, macos-14]
timeout-minutes: 30 timeout-minutes: 40
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: check-rust-changes needs: check-rust-changes
steps: steps:
@ -381,7 +381,7 @@ jobs:
echo "retried=true" >>$GITHUB_OUTPUT echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry" echo "run playwright with last failed tests and retry $retry"
if [[ "$IS_UBUNTU" == "true" ]]; then 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 else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi fi

1
.gitignore vendored
View File

@ -59,6 +59,7 @@ src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg
venv venv
.vite/ .vite/

View File

@ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS) dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
yarn start 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) $(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)' yarn xstate typegen 'src/**/*.ts?(x)'

View File

@ -351,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
</details> </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 ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

View File

@ -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

View File

@ -56,6 +56,7 @@ layout: manual
* [`line`](kcl/line) * [`line`](kcl/line)
* [`lineTo`](kcl/lineTo) * [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln) * [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log) * [`log`](kcl/log)
* [`log10`](kcl/log10) * [`log10`](kcl/log10)
* [`log2`](kcl/log2) * [`log2`](kcl/log2)
@ -63,6 +64,7 @@ layout: manual
* [`max`](kcl/max) * [`max`](kcl/max)
* [`min`](kcl/min) * [`min`](kcl/min)
* [`mm`](kcl/mm) * [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d) * [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d) * [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d) * [`patternLinear2d`](kcl/patternLinear2d)

516
docs/kcl/loft.md Normal file

File diff suppressed because one or more lines are too long

138
docs/kcl/offsetPlane.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
} }
// deselect line tool // 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) const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect await expect
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE)) .poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
.toBeLessThan(3) .toBeLessThan(3)
await page.waitForTimeout(100)
await expect await expect
.poll(() => u.getGreatestPixDiff(line1, [249, 249, 249])) .poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249]))
.toBeLessThan(3) .toBeLessThan(3)
await page.waitForTimeout(100)
} }
// click between first two clicks to get center of the line // click between first two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
if (openPanes.includes('code')) { 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) await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
} }
// hold down shift // hold down shift
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100)
// click between the latest two clicks to get center of the line // click between the latest two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
await page.waitForTimeout(100)
// selected two lines therefore there should be two cursors // selected two lines therefore there should be two cursors
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(100)
} }
await page.getByRole('button', { name: 'Length: open menu' }).click() await page.getByRole('button', { name: 'Length: open menu' }).click()

View File

@ -27,9 +27,19 @@ test.describe('Code pane and errors', () => {
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript((code) => { await page.addInitScript(() => {
localStorage.setItem('persistCode', code) localStorage.setItem(
}, bracket) '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 page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -261,10 +271,7 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
}) })
// If they're open by default, we're not actually testing anything. // 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 page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
}) })
await test.step('All panes opened before should be visible', async () => { await test.step('All panes opened before should be visible', async () => {

View File

@ -43,12 +43,6 @@ test(
// open the project // open the project
await page.getByText(`bracket`).click() 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 // expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -56,6 +50,12 @@ test(
const exportButton = page.getByTestId('export-pane-button') const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible() 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 gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export') const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`) const exportingToastMessage = page.getByText(`Exporting...`)
@ -104,7 +104,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBe(477327) .toBe(477481)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')

View File

@ -112,7 +112,8 @@ test.describe('when using the file tree to', () => {
}) })
const { const {
panesOpen, openKclCodePanel,
openFilePanel,
createAndSelectProject, createAndSelectProject,
pasteCodeInEditor, pasteCodeInEditor,
createNewFileAndSelect, createNewFileAndSelect,
@ -124,9 +125,9 @@ test.describe('when using the file tree to', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents // File the main.kcl with contents
const kclCube = await fsp.readFile( const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl', 'src/wasm-lib/tests/executor/inputs/cube.kcl',
@ -201,4 +202,78 @@ test.describe('when using the file tree to', () => {
await electronApp.close() 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()
})
}
)
}) })

View File

@ -147,9 +147,6 @@ test.describe('Can export from electron app', () => {
const u = await getUtils(page) const u = await getUtils(page)
page.on('console', console.log) page.on('console', console.log)
await electronApp.context().addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
})
const pointOnModel = { x: 630, y: 280 } 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 // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(10) .toBeLessThan(15)
}) })
const exportLocations: Array<Paths> = [] const exportLocations: Array<Paths> = []
@ -207,7 +204,7 @@ test.describe('Can export from electron app', () => {
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBe(477327) .toBe(477481)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')
@ -495,10 +492,6 @@ test(
await file.click() await file.click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText( await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface' 'A mounting bracket for the Focusrite Scarlett Solo audio interface'
) )
@ -856,10 +849,10 @@ const extrude001 = extrude(200, sketch001)`)
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(10) .toBeLessThan(15)
await expect(async () => { await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 }) await page.mouse.move(0, 0, { steps: 5 })
@ -867,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`)
await page.mouse.click(pointOnModel.x, pointOnModel.y) await page.mouse.click(pointOnModel.x, pointOnModel.y)
// check user can interact with model by checking it turns yellow // check user can interact with model by checking it turns yellow
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132])) .poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
.toBeLessThan(10) .toBeLessThan(15)
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
await page.getByTestId('app-logo').click() await page.getByTestId('app-logo').click()
@ -942,24 +935,15 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(10) .toBeLessThan(15)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -976,24 +960,15 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(10) .toBeLessThan(15)
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('Opening the router-template project should load the stream', async () => {
@ -1744,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
}) })
await test.step('Rename the folder', async () => { await test.step('Rename the folder', async () => {
await page.waitForTimeout(60000) await page.waitForTimeout(2000)
await folderToRename.click({ button: 'right' }) await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible() await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click() await renameMenuItem.click()

View File

@ -358,6 +358,7 @@ const sketch001 = startSketchAt([-0, -0])
await page.addInitScript( await page.addInitScript(
async ({ code }) => { async ({ code }) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, },
{ {
code: bracket, code: bracket,
@ -393,20 +394,22 @@ const sketch001 = startSketchAt([-0, -0])
await test.step('The second export is blocked', async () => { await test.step('The second export is blocked', async () => {
// Find the toast. // Find the toast.
// Look out for the toast message // Look out for the toast message
await expect(exportingToastMessage).toBeVisible() await Promise.all([
await expect(alreadyExportingToastMessage).toBeVisible() expect(exportingToastMessage.first()).toBeVisible(),
expect(alreadyExportingToastMessage).toBeVisible(),
await page.waitForTimeout(1000) ])
}) })
await test.step('The first export still succeeds', async () => { await test.step('The first export still succeeds', async () => {
await expect(exportingToastMessage).not.toBeVisible() await Promise.all([
await expect(errorToastMessage).not.toBeVisible() expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
await expect(engineErrorToastMessage).not.toBeVisible() expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
await expect(successToastMessage).toBeVisible() expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
expect(alreadyExportingToastMessage).not.toBeVisible({
await expect(alreadyExportingToastMessage).not.toBeVisible() timeout: 15_000,
}),
])
}) })
}) })
@ -419,10 +422,12 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed. // Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible() await Promise.all([
await expect(errorToastMessage).not.toBeVisible() expect(exportingToastMessage).not.toBeVisible(),
await expect(engineErrorToastMessage).not.toBeVisible() expect(errorToastMessage).not.toBeVisible(),
await expect(alreadyExportingToastMessage).not.toBeVisible() expect(engineErrorToastMessage).not.toBeVisible(),
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -548,13 +548,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFileAndSelect: async (name: string) => { createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => { return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page const newFile = page
.locator('[data-testid="file-pane-scroll-container"] button') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name }) .filter({ hasText: name })
.click()
await expect(newFile).toBeVisible()
await newFile.click()
}) })
}, },
@ -585,6 +588,15 @@ export async function getUtils(page: Page, test_?: typeof test) {
}) })
}, },
/**
* @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[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
@ -852,10 +864,12 @@ export async function setupElectron({
testInfo, testInfo,
folderSetupFn, folderSetupFn,
cleanProjectDir = true, cleanProjectDir = true,
appSettings,
}: { }: {
testInfo: TestInfo testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void> folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) { }) {
// create or otherwise clear the folder // create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir') const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -889,7 +903,10 @@ export async function setupElectron({
if (cleanProjectDir) { if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME) const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify({ const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS, ...TEST_SETTINGS,
settings: { settings: {
app: { app: {
@ -897,7 +914,8 @@ export async function setupElectron({
projectDirectory: projectDirName, projectDirectory: projectDirName,
}, },
}, },
}) }
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides) await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
} }

View File

@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
let noHoverColor: [number, number, number] = [82, 82, 82] let noHoverColor: [number, number, number] = [92, 92, 92]
let hoverColor: [number, number, number] = [116, 116, 116] let hoverColor: [number, number, number] = [127, 127, 127]
let selectColor: [number, number, number] = [144, 148, 97] let selectColor: [number, number, number] = [155, 155, 105]
const extrudeWall = { x: 670, y: 275 } const extrudeWall = { x: 670, y: 275 }
const extrudeText = `line([170.36, -121.61], %, $seg01)` const extrudeText = `line([170.36, -121.61], %, $seg01)`
@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
await expect await expect
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor)) .poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(5) .toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.move(extrudeWall.x, extrudeWall.y) await page.mouse.move(extrudeWall.x, extrudeWall.y)
@ -798,43 +798,43 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor) await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(6) ).toBeLessThan(15)
await page.mouse.click(extrudeWall.x, extrudeWall.y) await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(6) ).toBeLessThan(15)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // check color stays there, i.e. not overridden (this was a bug previously)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(6) ).toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300) await page.waitForTimeout(300)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face // because of shading, color is not exact everywhere on the face
noHoverColor = [104, 104, 104] noHoverColor = [115, 115, 115]
hoverColor = [134, 134, 134] hoverColor = [145, 145, 145]
selectColor = [158, 162, 110] 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 page.mouse.move(cap.x, cap.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText( await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText) removeAfterFirstParenthesis(capText)
) )
await page.waitForTimeout(200) 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 page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200) 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) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // 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 ({ test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
page, page,

View File

@ -288,7 +288,7 @@ test.describe('Testing settings', () => {
}) })
await test.step('Refresh the application and see project setting applied', async () => { 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 expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click() await settingsCloseButton.click()
@ -303,53 +303,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( test(
`Closing settings modal should go back to the original file being viewed`, `Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, 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 { const {
panesOpen, openKclCodePanel,
createAndSelectProject, openFilePanel,
pasteCodeInEditor, waitForPageLoad,
clickPane, selectFile,
createNewFileAndSelect,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen([]) await test.step('Precondition: Open to second project file', async () => {
await test.step('Precondition: No projects exist', async () => {
await expect(page.getByTestId('home-section')).toBeVisible() await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link') await page.getByText('project-000').click()
await expect(projectLinksPre).toHaveCount(0) 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', { const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
@ -357,6 +413,9 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => { await test.step('Open and close settings', async () => {
await settingsOpenButton.click() await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click() await settingsCloseButton.click()
}) })

View File

@ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
// Ensure the final toast remains. // Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible() 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() await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model. // Ensure you can copy the code for the final model.
@ -690,40 +690,53 @@ test(
'Text-to-CAD functionality', 'Text-to-CAD functionality',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo }) const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () => const fileExists = () =>
fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl')) fs.existsSync(join(dir, projectName, textToCadFileName))
const { createAndSelectProject, panesOpen } = await getUtils(page, test) const {
createAndSelectProject,
openFilePanel,
openKclCodePanel,
waitForPageLoad,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await panesOpen(['code', 'files']) // 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 // Create and navigate to the project
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command // Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect( await waitForPageLoad()
page.getByRole('button', { name: 'Start Sketch' }) await openFilePanel()
).toBeEnabled({ await openKclCodePanel()
timeout: 20_000,
})
await test.step(`Test file creation`, async () => { await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, 'lego 2x4') await sendPromptFromCommandBar(page, prompt)
// File is considered created if it shows up in the Project Files pane // File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
await expect(file).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy() expect(fileExists()).toBeTruthy()
}) })
await test.step(`Test file navigation`, async () => { await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) await expect(projectMenuButton).toContainText('main.kcl')
await file.click() await textToCadFileButton.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane // File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
await expect(kclComment).toBeVisible({ timeout: 20_000 }) await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
await expect(projectMenuButton).toContainText(textToCadFileName)
}) })
await test.step(`Test file deletion on rejection`, async () => { await test.step(`Test file deletion on rejection`, async () => {
@ -737,6 +750,8 @@ test(
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy() expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()

83
electron-builder.yml Normal file
View 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

8
installer.nsh Normal file
View 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

2
interface.d.ts vendored
View File

@ -30,8 +30,6 @@ export interface IElectronAPI {
join: typeof path.join join: typeof path.join
sep: typeof path.sep sep: typeof path.sep
rename: (prev: string, next: string) => typeof fs.rename rename: (prev: string, next: string) => typeof fs.rename
setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: { packageJson: {
name: string name: string
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.24.12", "version": "0.25.0",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -39,6 +39,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.3.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@ -50,7 +51,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "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-json-view": "^1.21.3",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
@ -97,7 +98,9 @@
"tron:package": "electron-forge package", "tron:package": "electron-forge package",
"tron:make": "electron-forge make", "tron:make": "electron-forge make",
"tron:publish": "electron-forge publish", "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": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
@ -150,7 +153,6 @@
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
@ -161,10 +163,12 @@
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"electron": "^32.0.1", "electron": "^32.0.1",
"electron-builder": "^24.13.3",
"electron-notarize": "^1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.25.0", "eslint-plugin-import": "^2.30.0",
"eslint-plugin-suggest-no-throw": "^1.0.0", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.10",
"http-server": "^14.1.1", "http-server": "^14.1.1",
@ -172,7 +176,7 @@
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.43",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
@ -185,7 +189,6 @@
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"wasm-pack": "^0.13.0", "wasm-pack": "^0.13.0",
"ws": "^8.17.0", "ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"

38
sign-win.js Normal file
View 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)
}
}

View File

@ -122,11 +122,11 @@ export function App() {
// Override the electron window draggable region behavior as well // Override the electron window draggable region behavior as well
// when the button is down in the stream // when the button is down in the stream
style={ style={
{ isDesktop() && context.store?.buttonDownInStream
'-webkit-app-region': context.store?.buttonDownInStream ? ({
? 'no-drag' '-webkit-app-region': 'no-drag',
: '', } as React.CSSProperties)
} as React.CSSProperties : {}
} }
project={{ project, file }} project={{ project, file }}
enableMenu={true} enableMenu={true}

View File

@ -69,19 +69,6 @@ const router = createRouter([
path: PATHS.INDEX, path: PATHS.INDEX,
loader: async () => { loader: async () => {
const onDesktop = isDesktop() const onDesktop = isDesktop()
if (onDesktop) {
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}
}
return onDesktop return onDesktop
? redirect(PATHS.HOME) ? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME) : redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)

View File

@ -20,6 +20,8 @@ import {
ToolbarItemResolved, ToolbarItemResolved,
ToolbarModeName, ToolbarModeName,
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
return ( return (
<Tooltip <Tooltip
inert={false} inert={false}
wrapperStyle={
isDesktop()
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
position="bottom" position="bottom"
wrapperClassName="!p-4 !pointer-events-auto" 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" 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"> <li key={link.label} className="contents">
<a <a
href={link.url} href={link.url}
onClick={openExternalBrowserIfDesktop(link.url)}
target="_blank" target="_blank"
rel="noreferrer" 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" 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"

View File

@ -6,6 +6,9 @@
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
}
.header.desktopApp {
/* Make the header act as a handle to drag the electron app window, /* 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 * 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 * all interactive elements opt-out of this behavior by default in src/index.css

View File

@ -6,6 +6,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton' import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton' import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
@ -32,7 +33,9 @@ export const AppHeader = ({
className={ className={
'w-full grid ' + 'w-full grid ' +
styles.header + 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 className
} }
style={style} style={style}

View File

@ -2,7 +2,7 @@ import { CommandLog } from 'lang/std/engineConnection'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
function useEngineCommands(): [CommandLog[], () => void] { export function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>( const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
engineCommandManager.commandLogs engineCommandManager.commandLogs
) )

View File

@ -179,10 +179,7 @@ const FileTreeItem = ({
codeManager.writeToFile() codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files // Prevent seeing the model built one piece at a time when changing files
kclManager.isFirstRender = true kclManager.executeCode(true)
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
} else { } else {
// Let the lsp servers know we closed a file. // Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null) onFileClose(currentFile?.path || null, project?.path || null)

View File

@ -11,6 +11,8 @@ import {
import { engineCommandManager } from '../lib/singletons' import { engineCommandManager } from '../lib/singletons'
import { Spinner } from './Spinner'
const Loading = ({ children }: React.PropsWithChildren) => { const Loading = ({ children }: React.PropsWithChildren) => {
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset) 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" className="body-bg flex flex-col items-center justify-center h-screen"
data-testid="loading" data-testid="loading"
> >
<svg viewBox="0 0 10 10" className="w-8 h-8"> <Spinner />
<circle
cx="5"
cy="5"
r="4"
stroke="var(--primary)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p> <p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
<p <p
className={ className={

View File

@ -11,6 +11,7 @@ import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow' import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator' import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator'
export function LowerRightControls({ export function LowerRightControls({
children, children,
@ -65,6 +66,7 @@ export function LowerRightControls({
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none"> <section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children} {children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a <a
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}` `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`

View 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>
)
}

View File

@ -67,7 +67,6 @@ import {
hasExtrudableGeometry, hasExtrudableGeometry,
isSingleCursorInPipe, isSingleCursorInPipe,
} from 'lang/queryAst' } from 'lang/queryAst'
import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
@ -162,9 +161,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause() store.videoElement?.pause()
kclManager.isFirstRender = true
kclManager.executeCode().then(() => { kclManager.executeCode().then(() => {
kclManager.isFirstRender = false
if (engineCommandManager.engineConnection?.idleMode) return if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => { store.videoElement?.play().catch((e) => {
@ -366,7 +363,7 @@ export const ModelingMachineProvider = ({
return {} return {}
}), }),
Make: async (_, event) => { Make: async (_, event) => {
if (event.type !== 'Make' || TEST) return if (event.type !== 'Make') return
// Check if we already have an export intent. // Check if we already have an export intent.
if (engineCommandManager.exportIntent) { if (engineCommandManager.exportIntent) {
toast.error('Already exporting') toast.error('Already exporting')
@ -410,7 +407,7 @@ export const ModelingMachineProvider = ({
) )
}, },
'Engine export': async (_, event) => { 'Engine export': async (_, event) => {
if (event.type !== 'Export' || TEST) return if (event.type !== 'Export') return
if (engineCommandManager.exportIntent) { if (engineCommandManager.exportIntent) {
toast.error('Already exporting') toast.error('Already exporting')
return return

View File

@ -193,10 +193,7 @@ export const SettingsAuthProviderBase = ({
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
// Unit changes requires a re-exec of code // Unit changes requires a re-exec of code
kclManager.isFirstRender = true kclManager.executeCode(true)
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
} else { } else {
// For any future logging we'd like to do // For any future logging we'd like to do
// console.log( // console.log(

View File

@ -0,0 +1,17 @@
import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return (
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
<circle
cx="5"
cy="5"
r="4"
stroke="var(--primary)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
)
}

View File

@ -54,12 +54,10 @@ export const Stream = () => {
* central place, we can move this code there. * central place, we can move this code there.
*/ */
async function executeCodeAndPlayStream() { async function executeCodeAndPlayStream() {
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => { kclManager.executeCode(true).then(() => {
videoRef.current?.play().catch((e) => { videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current) console.warn('Video playing was prevented', e, videoRef.current)
}) })
kclManager.isFirstRender = false
setStreamState(StreamState.Playing) setStreamState(StreamState.Playing)
}) })
} }
@ -219,7 +217,7 @@ export const Stream = () => {
* Play the vid * Play the vid
*/ */
useEffect(() => { useEffect(() => {
if (!kclManager.isFirstRender) { if (!kclManager.isExecuting) {
setTimeout(() => setTimeout(() =>
// execute in the next event loop // execute in the next event loop
videoRef.current?.play().catch((e) => { videoRef.current?.play().catch((e) => {
@ -227,7 +225,7 @@ export const Stream = () => {
}) })
) )
} }
}, [kclManager.isFirstRender]) }, [kclManager.isExecuting])
useEffect(() => { useEffect(() => {
if ( if (
@ -382,15 +380,15 @@ export const Stream = () => {
</div> </div>
</div> </div>
)} )}
{(!isNetworkOkay || isLoading || kclManager.isFirstRender) && ( {(!isNetworkOkay || isLoading) && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>
{!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? ( {!isNetworkOkay && !isLoading ? (
<span data-testid="loading-stream">Stream disconnected...</span> <span data-testid="loading-stream">Stream disconnected...</span>
) : !isLoading && kclManager.isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span>
) : ( ) : (
!isLoading && (
<span data-testid="loading-stream">Loading stream...</span> <span data-testid="loading-stream">Loading stream...</span>
)
)} )}
</Loading> </Loading>
</div> </div>

View File

@ -12,6 +12,7 @@ interface TooltipProps extends React.PropsWithChildren {
position?: TooltipPosition position?: TooltipPosition
wrapperClassName?: string wrapperClassName?: string
contentClassName?: string contentClassName?: string
wrapperStyle?: React.CSSProperties
delay?: number delay?: number
hoverOnly?: boolean hoverOnly?: boolean
inert?: boolean inert?: boolean
@ -22,6 +23,7 @@ export default function Tooltip({
position = 'top', position = 'top',
wrapperClassName: className, wrapperClassName: className,
contentClassName, contentClassName,
wrapperStyle = {},
delay = 200, delay = 200,
hoverOnly = false, hoverOnly = false,
inert = true, inert = true,
@ -36,7 +38,10 @@ export default function Tooltip({
} ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${ } ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${
styles[position] styles[position]
} ${className}`} } ${className}`}
style={{ '--_delay': delay + 'ms' } as React.CSSProperties} style={Object.assign(
{ '--_delay': delay + 'ms' } as React.CSSProperties,
wrapperStyle
)}
> >
<div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}> <div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}>
{children} {children}

View File

@ -8,7 +8,7 @@ import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange, parse, recast } from 'lang/wasm' import { PathToNode, SourceRange } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal) export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -23,8 +23,7 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
const parsed = parse(recast(ast)) const parsed = ast
if (trap(parsed)) return
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(
parsed, parsed,

View File

@ -50,6 +50,14 @@ body.dark {
@apply text-chalkboard-10; @apply text-chalkboard-10;
} }
@media (prefers-color-scheme: dark) {
body,
.body-bg,
.dark .body-bg {
@apply bg-chalkboard-100;
}
}
select { select {
@apply bg-chalkboard-20; @apply bg-chalkboard-20;
} }
@ -287,32 +295,11 @@ code {
} }
@layer utilities { @layer utilities {
/* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */ /*
@keyframes circle-in-hesitate { This is where your own custom Tailwind utility classes can go,
0% { which lets you use them with @apply in your CSS, and get
clip-path: circle( autocomplete in classNames in your JSX.
var(--circle-size-start, 0%) at var(--circle-x, 50%) */
var(--circle-y, 50%)
);
}
40% {
clip-path: circle(
var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%)
);
}
100% {
clip-path: circle(
var(--circle-size-end, 125%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
}
.in-circle-hesitate {
animation: var(--circle-duration, 2.5s)
var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate
both;
}
} }
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,

View File

@ -60,8 +60,6 @@ export class KclManager {
private _wasmInitFailedCallback: (arg: boolean) => void = () => {} private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {} private _executeCallback: () => void = () => {}
isFirstRender = true
get ast() { get ast() {
return this._ast return this._ast
} }

View File

@ -3,6 +3,8 @@ import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
export type ArtifactId = string
interface CommonCommandProperties { interface CommonCommandProperties {
range: SourceRange range: SourceRange
pathToNode: PathToNode pathToNode: PathToNode
@ -10,7 +12,7 @@ interface CommonCommandProperties {
export interface PlaneArtifact { export interface PlaneArtifact {
type: 'plane' type: 'plane'
pathIds: Array<string> pathIds: Array<ArtifactId>
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
export interface PlaneArtifactRich { export interface PlaneArtifactRich {
@ -21,16 +23,16 @@ export interface PlaneArtifactRich {
export interface PathArtifact { export interface PathArtifact {
type: 'path' type: 'path'
planeId: string planeId: ArtifactId
segIds: Array<string> segIds: Array<ArtifactId>
extrusionId: string extrusionId: ArtifactId
solid2dId?: string solid2dId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface solid2D { interface solid2D {
type: 'solid2D' type: 'solid2D'
pathId: string pathId: ArtifactId
} }
export interface PathArtifactRich { export interface PathArtifactRich {
type: 'path' type: 'path'
@ -42,10 +44,10 @@ export interface PathArtifactRich {
interface SegmentArtifact { interface SegmentArtifact {
type: 'segment' type: 'segment'
pathId: string pathId: ArtifactId
surfaceId: string surfaceId: ArtifactId
edgeIds: Array<string> edgeIds: Array<ArtifactId>
edgeCutId?: string edgeCutId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface SegmentArtifactRich { interface SegmentArtifactRich {
@ -59,9 +61,9 @@ interface SegmentArtifactRich {
interface ExtrusionArtifact { interface ExtrusionArtifact {
type: 'extrusion' type: 'extrusion'
pathId: string pathId: ArtifactId
surfaceIds: Array<string> surfaceIds: Array<ArtifactId>
edgeIds: Array<string> edgeIds: Array<ArtifactId>
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface ExtrusionArtifactRich { interface ExtrusionArtifactRich {
@ -74,23 +76,23 @@ interface ExtrusionArtifactRich {
interface WallArtifact { interface WallArtifact {
type: 'wall' type: 'wall'
segId: string segId: ArtifactId
edgeCutEdgeIds: Array<string> edgeCutEdgeIds: Array<ArtifactId>
extrusionId: string extrusionId: ArtifactId
pathIds: Array<string> pathIds: Array<ArtifactId>
} }
interface CapArtifact { interface CapArtifact {
type: 'cap' type: 'cap'
subType: 'start' | 'end' subType: 'start' | 'end'
edgeCutEdgeIds: Array<string> edgeCutEdgeIds: Array<ArtifactId>
extrusionId: string extrusionId: ArtifactId
pathIds: Array<string> pathIds: Array<ArtifactId>
} }
interface ExtrudeEdge { interface ExtrudeEdge {
type: 'extrudeEdge' type: 'extrudeEdge'
segId: string segId: ArtifactId
extrusionId: string extrusionId: ArtifactId
subType: 'opposite' | 'adjacent' subType: 'opposite' | 'adjacent'
} }
@ -98,16 +100,16 @@ interface ExtrudeEdge {
interface EdgeCut { interface EdgeCut {
type: 'edgeCut' type: 'edgeCut'
subType: 'fillet' | 'chamfer' subType: 'fillet' | 'chamfer'
consumedEdgeId: string consumedEdgeId: ArtifactId
edgeIds: Array<string> edgeIds: Array<ArtifactId>
surfaceId: string surfaceId: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface EdgeCutEdge { interface EdgeCutEdge {
type: 'edgeCutEdge' type: 'edgeCutEdge'
edgeCutId: string edgeCutId: ArtifactId
surfaceId: string surfaceId: ArtifactId
} }
export type Artifact = export type Artifact =
@ -122,7 +124,7 @@ export type Artifact =
| EdgeCutEdge | EdgeCutEdge
| solid2D | solid2D
export type ArtifactGraph = Map<string, Artifact> export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type'] export type EngineCommand = Models['WebSocketRequest_type']
@ -149,7 +151,7 @@ export function createArtifactGraph({
responseMap: ResponseMap responseMap: ResponseMap
ast: Program ast: Program
}) { }) {
const myMap = new Map<string, Artifact>() const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */ /** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = '' let currentPlaneId = ''
@ -166,7 +168,7 @@ export function createArtifactGraph({
const artifactsToUpdate = getArtifactsToUpdate({ const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand, orderedCommand,
responseMap, responseMap,
getArtifact: (id: string) => myMap.get(id), getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId, currentPlaneId,
ast, ast,
}) })
@ -224,11 +226,11 @@ export function getArtifactsToUpdate({
orderedCommand: OrderedCommand orderedCommand: OrderedCommand
responseMap: ResponseMap responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */ /** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: string) => Artifact | undefined getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: string currentPlaneId: ArtifactId
ast: Program ast: Program
}): Array<{ }): Array<{
id: string id: ArtifactId
artifact: Artifact artifact: Artifact
}> { }> {
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
@ -514,7 +516,7 @@ export function filterArtifacts<T extends Artifact['type'][]>(
(!predicate || (!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>)) predicate(value as Extract<Artifact, { type: T[number] }>))
) )
) as Map<string, Extract<Artifact, { type: T[number] }>> ) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
} }
export function getArtifactsOfTypes<T extends Artifact['type'][]>( export function getArtifactsOfTypes<T extends Artifact['type'][]>(
@ -528,7 +530,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
}, },
map: ArtifactGraph map: ArtifactGraph
): Map<string, Extract<Artifact, { type: T[number] }>> { ): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
return new Map( return new Map(
[...map].filter( [...map].filter(
([key, value]) => ([key, value]) =>
@ -537,7 +539,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
(!predicate || (!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>)) predicate(value as Extract<Artifact, { type: T[number] }>))
) )
) as Map<string, Extract<Artifact, { type: T[number] }>> ) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
} }
export function getArtifactOfTypes<T extends Artifact['type'][]>( export function getArtifactOfTypes<T extends Artifact['type'][]>(
@ -545,7 +547,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
key, key,
types, types,
}: { }: {
key: string key: ArtifactId
types: T types: T
}, },
map: ArtifactGraph map: ArtifactGraph
@ -718,7 +720,7 @@ export function getExtrudeEdgeCodeRef(
} }
export function getExtrusionFromSuspectedExtrudeSurface( export function getExtrusionFromSuspectedExtrudeSurface(
id: string, id: ArtifactId,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error { ): ExtrusionArtifact | Error {
const artifact = getArtifactOfTypes( const artifact = getArtifactOfTypes(
@ -733,7 +735,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
} }
export function getExtrusionFromSuspectedPath( export function getExtrusionFromSuspectedPath(
id: string, id: ArtifactId,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error { ): ExtrusionArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph) const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)

View File

@ -1252,6 +1252,10 @@ export type CommandLog =
type: 'execution-done' type: 'execution-done'
data: null data: null
} }
| {
type: 'export-done'
data: null
}
export enum EngineCommandManagerEvents { export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run // engineConnection is available but scene setup may not have run
@ -1918,7 +1922,13 @@ export class EngineCommandManager extends EventTarget {
} else if (cmd.type === 'export') { } else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => { const promise = new Promise<null>((resolve, reject) => {
this.pendingExport = { this.pendingExport = {
resolve, resolve: (passThrough) => {
this.addCommandLog({
type: 'export-done',
data: null,
})
resolve(passThrough)
},
reject: (reason: string) => { reject: (reason: string) => {
this.exportIntent = null this.exportIntent = null
reject(reason) reject(reason)

View File

@ -95,8 +95,6 @@ export const wasmUrl = () => {
document.location.pathname.split('/').slice(0, -1).join('/') + document.location.pathname.split('/').slice(0, -1).join('/') +
'/wasm_lib_bg.wasm' '/wasm_lib_bg.wasm'
console.log(`Full URL for WASM: ${fullUrl}`)
return fullUrl return fullUrl
} }

View File

@ -8,7 +8,6 @@ import {
parseProjectSettings, parseProjectSettings,
} from 'lang/wasm' } from 'lang/wasm'
import { import {
DEFAULT_HOST,
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
PROJECT_FOLDER, PROJECT_FOLDER,
PROJECT_SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME,
@ -462,29 +461,60 @@ export const readProjectSettingsFile = async (
*/ */
export const readAppSettingsFile = async () => { export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath() let settingsPath = await getAppSettingsFilePath()
const initialProjectDirConfig: DeepPartial<
Configuration['settings']['project']
> = { directory: await getInitialDefaultDir() }
// The file exists, read it and parse it. // The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) { if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath) const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml) const parsedAppConfig = parseAppSettings(configToml)
if (err(configObj)) { if (err(parsedAppConfig)) {
return Promise.reject(configObj) return Promise.reject(parsedAppConfig)
} }
return configObj const hasProjectDirectorySetting =
parsedAppConfig.settings?.project?.directory ||
parsedAppConfig.settings?.app?.project_directory
if (hasProjectDirectorySetting) {
return parsedAppConfig
} else {
// inject the default project directory setting
const mergedConfig: DeepPartial<Configuration> = {
...parsedAppConfig,
settings: {
...parsedAppConfig.settings,
project: Object.assign(
{},
parsedAppConfig.settings?.project,
initialProjectDirConfig
),
},
}
return mergedConfig
}
} }
// The file doesn't exist, create a new one. // The file doesn't exist, create a new one.
// This defaultAppConfig is truly an empty object every time.
const defaultAppConfig = defaultAppSettings() const defaultAppConfig = defaultAppSettings()
if (err(defaultAppConfig)) { if (err(defaultAppConfig)) {
return Promise.reject(defaultAppConfig) return Promise.reject(defaultAppConfig)
} }
const initialDirConfig: DeepPartial<Configuration> = {
settings: { project: { directory: await getInitialDefaultDir() } }, // inject the default project directory setting
const mergedDefaultConfig: DeepPartial<Configuration> = {
...defaultAppConfig,
settings: {
...defaultAppConfig.settings,
project: Object.assign(
{},
defaultAppConfig.settings?.project,
initialProjectDirConfig
),
},
} }
const config = Object.assign(defaultAppConfig, initialDirConfig) return mergedDefaultConfig
return config
} }
export const writeAppSettingsFile = async (tomlStr: string) => { export const writeAppSettingsFile = async (tomlStr: string) => {
@ -525,28 +555,6 @@ export const getUser = async (
token: string, token: string,
hostname: string hostname: string
): Promise<Models['User_type']> => { ): Promise<Models['User_type']> => {
// Use the host passed in if it's set.
// Otherwise, use the default host.
const host = !hostname ? DEFAULT_HOST : hostname
// Change the baseURL to the one we want.
let baseurl = host
if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) {
baseurl = `https://${host}`
if (host.indexOf('localhost') === 0) {
baseurl = `http://${host}`
}
}
// Use kittycad library to fetch the user info from /user/me
if (baseurl !== DEFAULT_HOST) {
// The TypeScript generated library uses environment variables for this
// because it was intended for NodeJS.
// Needs to stay like this because window.electron.kittycad needs it
// internally.
window.electron.setBaseUrl(baseurl)
}
try { try {
const user = await window.electron.kittycad('users.get_user_self', { const user = await window.electron.kittycad('users.get_user_self', {
client: { token }, client: { token },

View File

@ -31,11 +31,11 @@ const bracket = startSketchOn('XY')
|> extrude(width, %) |> extrude(width, %)
|> fillet({ |> fillet({
radius: filletR, radius: filletR,
tags: [getPreviousAdjacentEdge(innerEdge)] tags: [getNextAdjacentEdge(innerEdge)]
}, %) }, %)
|> fillet({ |> fillet({
radius: filletR + thickness, radius: filletR + thickness,
tags: [getPreviousAdjacentEdge(outerEdge)] tags: [getNextAdjacentEdge(outerEdge)]
}, %)` }, %)`
/** /**

View File

@ -14,7 +14,7 @@ const save_ = async (file: ModelingAppFile) => {
extensions.push(extension) extensions.push(extension)
} }
if (!(window as any).playwrightSkipFilePicker) { if (window.electron.process.env.IS_PLAYWRIGHT) {
// skip file picker, save to default location // skip file picker, save to default location
await window.electron.writeFile( await window.electron.writeFile(
file.name, file.name,

View File

@ -81,7 +81,6 @@ export class MachineManager {
} }
this._machines = await window.electron.listMachines() this._machines = await window.electron.listMachines()
console.log('Machines:', this._machines)
} }
private async updateMachineApiIp(): Promise<void> { private async updateMachineApiIp(): Promise<void> {

View File

@ -5,7 +5,7 @@ import {
kclManager, kclManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { CallExpression, SourceRange, Expr, parse, recast } from 'lang/wasm' import { CallExpression, SourceRange, Expr, parse } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state' import { EditorSelection, SelectionRange } from '@codemirror/state'
@ -302,8 +302,7 @@ export function processCodeMirrorRanges({
} }
function updateSceneObjectColors(codeBasedSelections: Selection[]) { function updateSceneObjectColors(codeBasedSelections: Selection[]) {
const updated = parse(recast(kclManager.ast)) const updated = kclManager.ast
if (err(updated)) return
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if ( if (

View File

@ -14,6 +14,7 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { mouseControlsToCameraSystem } from 'lib/cameraControls' import { mouseControlsToCameraSystem } from 'lib/cameraControls'
import { appThemeToTheme } from 'lib/theme' import { appThemeToTheme } from 'lib/theme'
import { import {
getInitialDefaultDir,
readAppSettingsFile, readAppSettingsFile,
readProjectSettingsFile, readProjectSettingsFile,
writeAppSettingsFile, writeAppSettingsFile,
@ -176,6 +177,11 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload) if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings() const settings = createSettings()
// Because getting the default directory is async, we need to set it after
if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir()
}
setSettingsAtLevel( setSettingsAtLevel(
settings, settings,
'user', 'user',

View File

@ -16,7 +16,6 @@ window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created. // This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)
kclManager.isFirstRender = true
engineCommandManager.kclManager = kclManager engineCommandManager.kclManager = kclManager
engineCommandManager.getAstCb = () => kclManager.ast engineCommandManager.getAstCb = () => kclManager.ast

View File

@ -129,12 +129,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
id: 'loft', id: 'loft',
onClick: () => console.error('Loft not yet implemented'), onClick: () => console.error('Loft not yet implemented'),
icon: 'loft', icon: 'loft',
status: 'unavailable', status: 'kcl-only',
title: 'Loft', title: 'Loft',
hotkey: 'L', hotkey: 'L',
description: description:
'Create a 3D body by blending between two or more sketches.', 'Create a 3D body by blending between two or more sketches.',
links: [ links: [
{
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/loft',
},
{ {
label: 'GitHub discussion', label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/613', url: 'https://github.com/KittyCAD/modeling-app/discussions/613',

View File

@ -2,14 +2,23 @@
// template that ElectronJS provides. // template that ElectronJS provides.
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' import {
app,
BrowserWindow,
ipcMain,
dialog,
shell,
nativeTheme,
} from 'electron'
import path from 'path' import path from 'path'
import { Issuer } from 'openid-client' import { Issuer } from 'openid-client'
import { Bonjour, Service } from 'bonjour-service' import { Bonjour, Service } from 'bonjour-service'
// @ts-ignore: TS1343 // @ts-ignore: TS1343
import * as kittycad from '@kittycad/lib/import' import * as kittycad from '@kittycad/lib/import'
import electronUpdater, { type AppUpdater } from 'electron-updater'
import minimist from 'minimist' import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile' import getCurrentProjectFile from 'lib/getCurrentProjectFile'
import os from 'node:os'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
@ -22,8 +31,20 @@ if (!process.env.NODE_ENV)
console.warn( console.warn(
'*FOX SCREAM* process.env.NODE_ENV is not explicitly set!, defaulting to production' '*FOX SCREAM* process.env.NODE_ENV is not explicitly set!, defaulting to production'
) )
// Default prod values
// dotenv override when present
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
console.log(process.env)
process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
process.env.VITE_KC_SKIP_AUTH ??= 'false'
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
// Handle creating/removing shortcuts on Windows when installing/uninstalling. // Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { if (require('electron-squirrel-startup')) {
app.quit() app.quit()
@ -46,7 +67,7 @@ if (process.defaultApp) {
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() registerStartupListeners()
const createWindow = (): BrowserWindow => { const createWindow = (filePath?: string): BrowserWindow => {
const newWindow = new BrowserWindow({ const newWindow = new BrowserWindow({
autoHideMenuBar: true, autoHideMenuBar: true,
show: false, show: false,
@ -59,17 +80,35 @@ const createWindow = (): BrowserWindow => {
preload: path.join(__dirname, './preload.js'), preload: path.join(__dirname, './preload.js'),
}, },
icon: path.resolve(process.cwd(), 'assets', 'icon.png'), icon: path.resolve(process.cwd(), 'assets', 'icon.png'),
frame: false, frame: os.platform() !== 'darwin',
titleBarStyle: 'hiddenInset', titleBarStyle: 'hiddenInset',
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1C1C1C' : '#FCFCFC',
}) })
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else { } else {
newWindow.loadFile( getProjectPathAtStartup(filePath).then((projectPath) => {
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`) const startIndex = path.join(
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
) )
if (projectPath === null) {
newWindow.loadFile(startIndex)
return
}
console.log('Loading file', projectPath)
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl)
newWindow.loadFile(startIndex, {
hash: fullUrl,
})
})
} }
// Open the DevTools. // Open the DevTools.
@ -80,13 +119,11 @@ const createWindow = (): BrowserWindow => {
return newWindow return newWindow
} }
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q. // explicitly with Cmd + Q, but it is a really weird behavior with our app.
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit() app.quit()
}
}) })
// This method will be called when Electron has finished // This method will be called when Electron has finished
@ -191,7 +228,39 @@ ipcMain.handle('find_machine_api', () => {
}) })
}) })
ipcMain.handle('loadProjectAtStartup', async () => { export function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
const { autoUpdater } = electronUpdater
return autoUpdater
}
export async function checkForUpdates(autoUpdater: AppUpdater) {
// TODO: figure out how to get the update modal back
const result = await autoUpdater.checkForUpdatesAndNotify()
console.log(result)
}
app.on('ready', async () => {
const autoUpdater = getAutoUpdater()
checkForUpdates(autoUpdater)
const fifteenMinutes = 15 * 60 * 1000
setInterval(() => {
checkForUpdates(autoUpdater)
}, fifteenMinutes)
autoUpdater.on('update-available', (info) => {
console.log('update-available', info)
})
autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info)
})
})
const getProjectPathAtStartup = async (
filePath?: string
): Promise<string | null> => {
// If we are in development mode, we don't want to load a project at // If we are in development mode, we don't want to load a project at
// startup. // startup.
// Since the args passed are always '.' // Since the args passed are always '.'
@ -199,7 +268,8 @@ ipcMain.handle('loadProjectAtStartup', async () => {
return null return null
} }
let projectPath: string | null = null let projectPath: string | null = filePath || null
if (projectPath === null) {
// macOS: open-file events that were received before the app is ready // macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles const macOpenFiles: string[] = (global as any).macOpenFiles
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) { if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
@ -228,24 +298,25 @@ ipcMain.handle('loadProjectAtStartup', async () => {
args._[1] = '' args._[1] = ''
} }
} }
}
if (projectPath) { if (projectPath) {
// We have a project path, load the project information. // We have a project path, load the project information.
console.log(`Loading project at startup: ${projectPath}`) console.log(`Loading project at startup: ${projectPath}`)
try {
const currentFile = await getCurrentProjectFile(projectPath) const currentFile = await getCurrentProjectFile(projectPath)
if (currentFile instanceof Error) {
console.error(currentFile)
return null
}
console.log(`Project loaded: ${currentFile}`) console.log(`Project loaded: ${currentFile}`)
return currentFile return currentFile
} catch (e) {
console.error(e)
} }
return null return null
} }
return null
})
function parseCLIArgs(): minimist.ParsedArgs { function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {}) return minimist(process.argv, {})
} }
@ -261,10 +332,11 @@ function registerStartupListeners() {
app.on('open-file', function (event, path) { app.on('open-file', function (event, path) {
event.preventDefault() event.preventDefault()
macOpenFiles.push(path)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow() createWindow(path)
} else {
macOpenFiles.push(path)
} }
}) })
@ -280,10 +352,11 @@ function registerStartupListeners() {
) { ) {
event.preventDefault() event.preventDefault()
openUrls.push(url)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow() createWindow(url)
} else {
openUrls.push(url)
} }
} }

View File

@ -60,9 +60,6 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> => const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api') ipcRenderer.invoke('find_machine_api')
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
login, login,
// Passing fs directly is not recommended since it gives a lot of power // Passing fs directly is not recommended since it gives a lot of power
@ -96,10 +93,6 @@ contextBridge.exposeInMainWorld('electron', {
isWindows, isWindows,
isLinux, isLinux,
}, },
loadProjectAtStartup,
// IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is
// no way to set it across the bridge boundary. We need to make it a command.
setBaseUrl: (value: string) => (process.env.BASE_URL = value),
process: { process: {
// Setter/getter has to be created because // Setter/getter has to be created because
// these are read-only over the boundary. // these are read-only over the boundary.

View File

@ -107,10 +107,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
codeManager.updateCodeStateEditor(bracket) codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile() await codeManager.writeToFile()
kclManager.isFirstRender = true await kclManager.executeCode(true)
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
props.setShouldShowWarning(false) props.setShouldShowWarning(false)
}} }}
nextText="Overwrite code and continue" nextText="Overwrite code and continue"

View File

@ -13,10 +13,7 @@ export default function Sketching() {
async function clearEditor() { async function clearEditor() {
// We do want to update both the state and editor here. // We do want to update both the state and editor here.
codeManager.updateCodeStateEditor('') codeManager.updateCodeStateEditor('')
kclManager.isFirstRender = true await kclManager.executeCode(true)
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
} }
clearEditor() clearEditor()

View File

@ -82,10 +82,7 @@ export function useDemoCode() {
if (!editorManager.editorView || codeManager.code === bracket) return if (!editorManager.editorView || codeManager.code === bracket) return
setTimeout(async () => { setTimeout(async () => {
codeManager.updateCodeStateEditor(bracket) codeManager.updateCodeStateEditor(bracket)
kclManager.isFirstRender = true await kclManager.executeCode(true)
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await codeManager.writeToFile() await codeManager.writeToFile()
}) })
}, [editorManager.editorView]) }, [editorManager.editorView])

View File

@ -58,19 +58,23 @@ const SignIn = () => {
} }
return ( return (
<main className="bg-primary h-screen grid place-items-stretch m-0 p-2"> <main
className="bg-primary h-screen grid place-items-stretch m-0 p-2"
style={
isDesktop()
? ({
'-webkit-app-region': 'drag',
} as CSSProperties)
: {}
}
>
<div <div
style={ style={
{ isDesktop()
height: 'calc(100vh - 16px)', ? ({ '-webkit-app-region': 'no-drag' } as CSSProperties)
'--circle-x': '14%', : {}
'--circle-y': '12%',
'--circle-size-mid': '15%',
'--circle-size-end': '200%',
'--circle-timing': 'cubic-bezier(0.25, 1, 0.4, 0.9)',
} as CSSProperties
} }
className="in-circle-hesitate body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto" className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
> >
<div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5"> <div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5">
<div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8"> <div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8">
@ -194,7 +198,7 @@ const SignIn = () => {
<div className="flex gap-4 flex-wrap items-center"> <div className="flex gap-4 flex-wrap items-center">
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://zoo.dev/docs/kcl-samples/ball-bearing" to="https://zoo.dev/docs/kcl-samples/a-parametric-bearing-pillow-block"
iconStart={{ icon: 'settings' }} iconStart={{ icon: 'settings' }}
className="border-chalkboard-30 dark:border-chalkboard-80" className="border-chalkboard-30 dark:border-chalkboard-80"
> >

121
src/wasm-lib/Cargo.lock generated
View File

@ -127,18 +127,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.81" version = "0.1.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -149,7 +149,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -370,9 +370,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.16" version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -380,9 +380,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.15" version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@ -397,7 +397,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -591,7 +591,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -602,7 +602,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -620,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.0.1" version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@ -657,7 +657,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"synstructure", "synstructure",
] ]
@ -672,7 +672,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.25" version = "0.1.26"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -686,7 +686,7 @@ dependencies = [
"rustfmt-wrapper", "rustfmt-wrapper",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -697,7 +697,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -724,7 +724,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -896,7 +896,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -986,7 +986,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1345,7 +1345,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.11" version = "0.2.14"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -1357,7 +1357,7 @@ dependencies = [
"clap", "clap",
"convert_case", "convert_case",
"criterion", "criterion",
"dashmap 6.0.1", "dashmap 6.1.0",
"databake", "databake",
"derive-docs", "derive-docs",
"expectorate", "expectorate",
@ -1399,7 +1399,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winnow 0.5.40", "winnow",
"zip", "zip",
] ]
@ -1412,12 +1412,12 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.9" version = "0.1.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper", "hyper",
@ -1430,9 +1430,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.3.17" version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2" checksum = "94feea5b1cf851b33dd108aa35aa01bde99772aa74d2ba1590295aac0b7ca33e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1799,7 +1799,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1852,7 +1852,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2016,7 +2016,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2029,7 +2029,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-build-config", "pyo3-build-config",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2491,7 +2491,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2565,7 +2565,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2576,14 +2576,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.127" version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.2.5",
"itoa", "itoa",
@ -2600,7 +2600,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2621,7 +2621,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2752,7 +2752,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2763,7 +2763,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2807,9 +2807,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.76" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2830,7 +2830,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2937,7 +2937,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -3008,9 +3008,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.39.3" version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -3032,7 +3032,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -3117,7 +3117,7 @@ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.18", "winnow",
] ]
[[package]] [[package]]
@ -3185,7 +3185,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -3213,7 +3213,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -3290,7 +3290,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"termcolor", "termcolor",
] ]
@ -3448,7 +3448,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -3509,7 +3509,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3544,7 +3544,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3800,15 +3800,6 @@ version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.18"
@ -3869,7 +3860,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]

View File

@ -15,8 +15,8 @@ data-encoding = "2.6.0"
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad.workspace = true kittycad.workspace = true
serde_json = "1.0.127" serde_json = "1.0.128"
tokio = { version = "1.39.3", features = ["sync"] } tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19" toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
@ -29,7 +29,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true } kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
reqwest = { version = "0.11.26", default-features = false } reqwest = { version = "0.11.26", default-features = false }
tokio = { version = "1.39.3", features = ["rt-multi-thread", "macros", "time"] } tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8" twenty-twenty = "0.8"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
@ -70,7 +70,7 @@ members = [
[workspace.dependencies] [workspace.dependencies]
http = "0.2.12" http = "0.2.12"
kittycad = { version = "0.3.17", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.3.18", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4" kittycad-modeling-session = "0.1.4"
[[test]] [[test]]

View File

@ -1,7 +1,7 @@
[package] [package]
name = "derive-docs" name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.25" version = "0.1.26"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -20,7 +20,7 @@ quote = "1"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.76", features = ["full"] } syn = { version = "2.0.77", features = ["full"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"

View File

@ -2,3 +2,6 @@
new-test name: new-test name:
echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs
TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})' TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})'
lint:
cargo clippy --all --tests --benches -- -D warnings

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" } kcl-lib = { path = "../kcl" }
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2.0.76", features = ["full"] } syn = { version = "2.0.77", features = ["full"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-test-server" name = "kcl-test-server"
description = "A test server for KCL" description = "A test server for KCL"
version = "0.1.9" version = "0.1.10"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -11,5 +11,5 @@ hyper = { version = "0.14.29", features = ["server"] }
kcl-lib = { version = "0.2", path = "../kcl" } kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0" pico-args = "0.5.0"
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127" serde_json = "1.0.128"
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.11" version = "0.2.14"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -13,14 +13,14 @@ keywords = ["kcl", "KittyCAD", "CAD"]
[dependencies] [dependencies]
anyhow = { version = "1.0.86", features = ["backtrace"] } anyhow = { version = "1.0.86", features = ["backtrace"] }
async-recursion = "1.1.1" async-recursion = "1.1.1"
async-trait = "0.1.81" async-trait = "0.1.82"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] } clap = { version = "4.5.17", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0" convert_case = "0.6.0"
dashmap = "6.0.1" dashmap = "6.1.0"
databake = { version = "0.1.8", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.24", path = "../derive-docs" } derive-docs = { version = "0.1.26", path = "../derive-docs" }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
git_rev = "0.1.0" git_rev = "0.1.0"
@ -37,7 +37,7 @@ reqwest = { version = "0.11.26", default-features = false, features = ["stream",
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127" serde_json = "1.0.128"
sha2 = "0.10.8" sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true } tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.63" thiserror = "1.0.63"
@ -47,12 +47,12 @@ url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40" winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false } zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.69" }
tokio = { version = "1.39.3", features = ["sync", "time"] } tokio = { version = "1.40.0", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] } tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42" wasm-bindgen-futures = "0.4.42"
@ -94,7 +94,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] }
insta = { version = "1.38.0", features = ["json"] } insta = { version = "1.38.0", features = ["json"] }
itertools = "0.13.0" itertools = "0.13.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
tokio = { version = "1.39.2", features = ["rt-multi-thread", "macros", "time"] } tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8.0" twenty-twenty = "0.8.0"
[[bench]] [[bench]]

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::test_server; use kcl_lib::{settings::types::UnitLength::Mm, test_server};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) { pub fn bench_execute(c: &mut Criterion) {
@ -13,26 +13,42 @@ pub fn bench_execute(c: &mut Criterion) {
// Configure Criterion.rs to detect smaller differences and increase sample size to improve // Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise. // precision and counteract the resulting noise.
group.sample_size(10); group.sample_size(10);
group.bench_with_input(BenchmarkId::new("execute_", name), &code, |b, &s| { group.bench_with_input(BenchmarkId::new("execute", name), &code, |b, &s| {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime // Spawn a future onto the runtime
b.iter(|| { b.iter(|| {
rt.block_on(test_server::execute_and_snapshot( rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap();
s,
kcl_lib::settings::types::UnitLength::Mm,
))
.unwrap();
}); });
}); });
group.finish(); group.finish();
} }
} }
criterion_group!(benches, bench_execute); pub fn bench_lego(c: &mut Criterion) {
let mut group = c.benchmark_group("executor_lego_pattern");
// Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise.
group.sample_size(10);
// Create lego bricks with N x 10 bumps, where N is each element of `sizes`.
let sizes = vec![1, 2, 4];
for size in sizes {
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let rt = Runtime::new().unwrap();
let code = LEGO_PROGRAM.replace("{{N}}", &size.to_string());
// Spawn a future onto the runtime
b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(&code, Mm)).unwrap();
});
});
}
group.finish();
}
criterion_group!(benches, bench_lego, bench_execute);
criterion_main!(benches); criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl"); const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl"); const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl"); const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl"); const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");
const LEGO_PROGRAM: &str = include_str!("../../tests/executor/inputs/slow_lego.kcl.tmpl");

View File

@ -995,20 +995,20 @@ impl SketchSurface {
} }
pub(crate) fn x_axis(&self) -> Point3d { pub(crate) fn x_axis(&self) -> Point3d {
match self { match self {
SketchSurface::Plane(plane) => plane.x_axis.clone(), SketchSurface::Plane(plane) => plane.x_axis,
SketchSurface::Face(face) => face.x_axis.clone(), SketchSurface::Face(face) => face.x_axis,
} }
} }
pub(crate) fn y_axis(&self) -> Point3d { pub(crate) fn y_axis(&self) -> Point3d {
match self { match self {
SketchSurface::Plane(plane) => plane.y_axis.clone(), SketchSurface::Plane(plane) => plane.y_axis,
SketchSurface::Face(face) => face.y_axis.clone(), SketchSurface::Face(face) => face.y_axis,
} }
} }
pub(crate) fn z_axis(&self) -> Point3d { pub(crate) fn z_axis(&self) -> Point3d {
match self { match self {
SketchSurface::Plane(plane) => plane.z_axis.clone(), SketchSurface::Plane(plane) => plane.z_axis,
SketchSurface::Face(face) => face.z_axis.clone(), SketchSurface::Face(face) => face.z_axis,
} }
} }
} }
@ -1304,7 +1304,7 @@ impl Point2d {
} }
} }
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, Default)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)]
#[ts(export)] #[ts(export)]
pub struct Point3d { pub struct Point3d {
pub x: f64, pub x: f64,
@ -1313,6 +1313,7 @@ pub struct Point3d {
} }
impl Point3d { impl Point3d {
pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 };
pub fn new(x: f64, y: f64, z: f64) -> Self { pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z } Self { x, y, z }
} }

View File

@ -927,7 +927,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
match body_items_within_function.parse_next(i) { match body_items_within_function.parse_next(i) {
Err(ErrMode::Backtrack(_)) => { Err(ErrMode::Backtrack(_)) => {
i.reset(start); i.reset(&start);
break; break;
} }
Err(e) => return Err(e), Err(e) => return Err(e),
@ -937,7 +937,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
} }
} }
(Err(ErrMode::Backtrack(_)), _) => { (Err(ErrMode::Backtrack(_)), _) => {
i.reset(start); i.reset(&start);
break; break;
} }
(Err(e), _) => return Err(e), (Err(e), _) => return Err(e),
@ -1276,7 +1276,7 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
/// Consume tokens that make up a binary expression, but don't actually return them. /// Consume tokens that make up a binary expression, but don't actually return them.
/// Why not? /// Why not?
/// Because this is designed to be used with .recognize() within the `binary_expression` parser. /// Because this is designed to be used with .take() within the `binary_expression` parser.
fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> { fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> {
let first = operand.parse_next(i).map(BinaryExpressionToken::from)?; let first = operand.parse_next(i).map(BinaryExpressionToken::from)?;
let remaining: Vec<_> = repeat( let remaining: Vec<_> = repeat(
@ -1308,7 +1308,7 @@ fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> {
} }
fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> { fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> {
let span_with_brackets = bracketed_section.recognize().parse_next(i)?; let span_with_brackets = bracketed_section.take().parse_next(i)?;
let n = span_with_brackets.len(); let n = span_with_brackets.len();
let mut span_no_brackets = &span_with_brackets[1..n - 1]; let mut span_no_brackets = &span_with_brackets[1..n - 1];
let expr = binary_expression.parse_next(&mut span_no_brackets)?; let expr = binary_expression.parse_next(&mut span_no_brackets)?;

View File

@ -1,5 +1,6 @@
use winnow::{ use winnow::{
error::{ErrorKind, ParseError, StrContext}, error::{ErrorKind, ParseError, StrContext},
stream::Stream,
Located, Located,
}; };
@ -102,14 +103,17 @@ impl<C> std::default::Default for ContextError<C> {
} }
} }
impl<I, C> winnow::error::ParserError<I> for ContextError<C> { impl<I, C> winnow::error::ParserError<I> for ContextError<C>
where
I: Stream,
{
#[inline] #[inline]
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self { fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
Self::default() Self::default()
} }
#[inline] #[inline]
fn append(self, _input: &I, _kind: ErrorKind) -> Self { fn append(self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, _kind: ErrorKind) -> Self {
self self
} }
@ -119,9 +123,12 @@ impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
} }
} }
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> { impl<C, I> winnow::error::AddContext<I, C> for ContextError<C>
where
I: Stream,
{
#[inline] #[inline]
fn add_context(mut self, _input: &I, ctx: C) -> Self { fn add_context(mut self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, ctx: C) -> Self {
self.context.push(ctx); self.context.push(ctx);
self self
} }

View File

@ -295,6 +295,13 @@ impl Args {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
pub(crate) fn get_sketch_groups_and_data<'a, T>(&'a self) -> Result<(Vec<SketchGroup>, Option<T>), KclError>
where
T: FromArgs<'a> + serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError> pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
where where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized, T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
@ -361,6 +368,13 @@ impl Args {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
pub(crate) fn get_data_and_float<'a, T>(&'a self) -> Result<(T, f64), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> { pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
@ -622,6 +636,8 @@ impl_from_arg_via_json!(super::revolve::RevolveData);
impl_from_arg_via_json!(super::sketch::SketchData); impl_from_arg_via_json!(super::sketch::SketchData);
impl_from_arg_via_json!(crate::std::import::ImportFormat); impl_from_arg_via_json!(crate::std::import::ImportFormat);
impl_from_arg_via_json!(crate::std::polar::PolarCoordsData); impl_from_arg_via_json!(crate::std::polar::PolarCoordsData);
impl_from_arg_via_json!(crate::std::loft::LoftData);
impl_from_arg_via_json!(crate::std::planes::StandardPlane);
impl_from_arg_via_json!(SketchGroup); impl_from_arg_via_json!(SketchGroup);
impl_from_arg_via_json!(FaceTag); impl_from_arg_via_json!(FaceTag);
impl_from_arg_via_json!(String); impl_from_arg_via_json!(String);
@ -692,3 +708,13 @@ impl<'a> FromKclValue<'a> for SketchSurface {
} }
} }
} }
impl<'a> FromKclValue<'a> for Vec<SketchGroup> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<SketchGroup>>().map(|x| x.0)
}
}

View File

@ -1,7 +1,10 @@
//! Functions related to extruding. //! Functions related to extruding.
use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kittycad::types::{ExtrusionFaceCapType, ExtrusionFaceInfo};
use schemars::JsonSchema; use schemars::JsonSchema;
use uuid::Uuid; use uuid::Uuid;
@ -90,7 +93,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
adjust_camera: false, adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_group.on { planar_normal: if let SketchSurface::Plane(plane) = &sketch_group.on {
// We pass in the normal for the plane here. // We pass in the normal for the plane here.
Some(plane.z_axis.clone().into()) Some(plane.z_axis.into())
} else { } else {
None None
}, },
@ -98,7 +101,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
) )
.await?; .await?;
args.send_modeling_cmd( args.batch_modeling_cmd(
id, id,
kittycad::types::ModelingCmd::Extrude { kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id, target: sketch_group.id,
@ -111,7 +114,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
// Disable the sketch mode. // Disable the sketch mode.
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {}) args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
.await?; .await?;
extrude_groups.push(do_post_extrude(sketch_group.clone(), length, id, args.clone()).await?); extrude_groups.push(do_post_extrude(sketch_group.clone(), length, args.clone()).await?);
} }
Ok(extrude_groups.into()) Ok(extrude_groups.into())
@ -120,7 +123,6 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
pub(crate) async fn do_post_extrude( pub(crate) async fn do_post_extrude(
sketch_group: SketchGroup, sketch_group: SketchGroup,
length: f64, length: f64,
id: Uuid,
args: Args, args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> { ) -> Result<Box<ExtrudeGroup>, KclError> {
// Bring the object to the front of the scene. // Bring the object to the front of the scene.
@ -164,7 +166,7 @@ pub(crate) async fn do_post_extrude(
let solid3d_info = args let solid3d_info = args
.send_modeling_cmd( .send_modeling_cmd(
id, uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo {
edge_id, edge_id,
object_id: sketch_group.id, object_id: sketch_group.id,
@ -181,9 +183,21 @@ pub(crate) async fn do_post_extrude(
vec![] vec![]
}; };
for face_info in face_infos.iter() { for (curve_id, face_id) in face_infos
if face_info.cap == kittycad::types::ExtrusionFaceCapType::None { .iter()
.filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
.filter_map(|face_info| {
if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) { if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
Some((curve_id, face_id))
} else {
None
}
})
{
// Batch these commands, because the Rust code doesn't actually care about the outcome.
// So, there's no need to await them.
// Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm)
// uses this to build the artifact graph, which the UI needs.
args.batch_modeling_cmd( args.batch_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { kittycad::types::ModelingCmd::Solid3DGetOppositeEdge {
@ -204,30 +218,17 @@ pub(crate) async fn do_post_extrude(
) )
.await?; .await?;
} }
}
}
// Create a hashmap for quick id lookup
let mut face_id_map = std::collections::HashMap::new();
// creating fake ids for start and end caps is to make extrudes mock-execute safe
let mut start_cap_id = if args.ctx.is_mock { Some(Uuid::new_v4()) } else { None };
let mut end_cap_id = if args.ctx.is_mock { Some(Uuid::new_v4()) } else { None };
for face_info in face_infos {
match face_info.cap {
kittycad::types::ExtrusionFaceCapType::Bottom => start_cap_id = face_info.face_id,
kittycad::types::ExtrusionFaceCapType::Top => end_cap_id = face_info.face_id,
_ => {
if let Some(curve_id) = face_info.curve_id {
face_id_map.insert(curve_id, face_info.face_id);
}
}
}
}
let Faces {
sides: face_id_map,
start_cap_id,
end_cap_id,
} = analyze_faces(&args, face_infos);
// Iterate over the sketch_group.value array and add face_id to GeoMeta // Iterate over the sketch_group.value array and add face_id to GeoMeta
let mut new_value: Vec<ExtrudeSurface> = Vec::new(); let new_value = sketch_group
for path in sketch_group.value.iter() { .value
.iter()
.flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) { if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path { match path {
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } | Path::Circle { .. } => { Path::TangentialArc { .. } | Path::TangentialArcTo { .. } | Path::Circle { .. } => {
@ -239,7 +240,7 @@ pub(crate) async fn do_post_extrude(
metadata: path.get_base().geo_meta.metadata.clone(), metadata: path.get_base().geo_meta.metadata.clone(),
}, },
}); });
new_value.push(extrude_surface); Some(extrude_surface)
} }
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => { Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
@ -250,12 +251,13 @@ pub(crate) async fn do_post_extrude(
metadata: path.get_base().geo_meta.metadata.clone(), metadata: path.get_base().geo_meta.metadata.clone(),
}, },
}); });
new_value.push(extrude_surface); Some(extrude_surface)
} }
} }
} else if args.ctx.is_mock { } else if args.ctx.is_mock {
// Only pre-populate the extrude surface if we are in mock mode. // Only pre-populate the extrude surface if we are in mock mode.
new_value.push(ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
// pushing this values with a fake face_id to make extrudes mock-execute safe // pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: Uuid::new_v4(), face_id: Uuid::new_v4(),
tag: path.get_base().tag.clone(), tag: path.get_base().tag.clone(),
@ -263,9 +265,13 @@ pub(crate) async fn do_post_extrude(
id: path.get_base().geo_meta.id, id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(), metadata: path.get_base().geo_meta.metadata.clone(),
}, },
})); });
} Some(extrude_surface)
} else {
None
} }
})
.collect();
Ok(Box::new(ExtrudeGroup { Ok(Box::new(ExtrudeGroup {
// Ok so you would think that the id would be the id of the extrude group, // Ok so you would think that the id would be the id of the extrude group,
@ -273,11 +279,45 @@ pub(crate) async fn do_post_extrude(
// sketch group. // sketch group.
id: sketch_group.id, id: sketch_group.id,
value: new_value, value: new_value,
sketch_group: sketch_group.clone(), meta: sketch_group.meta.clone(),
sketch_group,
height: length, height: length,
start_cap_id, start_cap_id,
end_cap_id, end_cap_id,
edge_cuts: vec![], edge_cuts: vec![],
meta: sketch_group.meta,
})) }))
} }
#[derive(Default)]
struct Faces {
/// Maps curve ID to face ID for each side.
sides: HashMap<Uuid, Option<Uuid>>,
/// Top face ID.
end_cap_id: Option<Uuid>,
/// Bottom face ID.
start_cap_id: Option<Uuid>,
}
fn analyze_faces(args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
let mut faces = Faces {
sides: HashMap::with_capacity(face_infos.len()),
..Default::default()
};
if args.ctx.is_mock {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(Uuid::new_v4());
faces.end_cap_id = Some(Uuid::new_v4());
}
for face_info in face_infos {
match face_info.cap {
ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
faces.sides.insert(curve_id, face_info.face_id);
}
}
}
}
faces
}

View File

@ -304,7 +304,7 @@ async fn inner_get_next_adjacent_edge(tag: TagIdentifier, args: Args) -> Result<
let resp = args let resp = args
.send_modeling_cmd( .send_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetPrevAdjacentEdge { ModelingCmd::Solid3DGetNextAdjacentEdge {
edge_id: tagged_path.id, edge_id: tagged_path.id,
object_id: tagged_path.sketch_group, object_id: tagged_path.sketch_group,
face_id, face_id,
@ -312,7 +312,7 @@ async fn inner_get_next_adjacent_edge(tag: TagIdentifier, args: Args) -> Result<
) )
.await?; .await?;
let kittycad::types::OkWebSocketResponseData::Modeling { let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetPrevAdjacentEdge { data: ajacent_edge }, modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetNextAdjacentEdge { data: ajacent_edge },
} = &resp } = &resp
else { else {
return Err(KclError::Engine(KclErrorDetails { return Err(KclError::Engine(KclErrorDetails {
@ -386,7 +386,7 @@ async fn inner_get_previous_adjacent_edge(tag: TagIdentifier, args: Args) -> Res
let resp = args let resp = args
.send_modeling_cmd( .send_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetNextAdjacentEdge { ModelingCmd::Solid3DGetPrevAdjacentEdge {
edge_id: tagged_path.id, edge_id: tagged_path.id,
object_id: tagged_path.sketch_group, object_id: tagged_path.sketch_group,
face_id, face_id,
@ -394,7 +394,7 @@ async fn inner_get_previous_adjacent_edge(tag: TagIdentifier, args: Args) -> Res
) )
.await?; .await?;
let kittycad::types::OkWebSocketResponseData::Modeling { let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetNextAdjacentEdge { data: ajacent_edge }, modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetPrevAdjacentEdge { data: ajacent_edge },
} = &resp } = &resp
else { else {
return Err(KclError::Engine(KclErrorDetails { return Err(KclError::Engine(KclErrorDetails {

View File

@ -0,0 +1,174 @@
//! Standard library lofts.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, KclValue, SketchGroup},
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
};
const DEFAULT_V_DEGREE: u32 = 2;
/// Data for a loft.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LoftData {
/// Degree of the interpolation. Must be greater than zero.
/// For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction.
/// This defaults to 2, if not specified.
pub v_degree: Option<std::num::NonZeroU32>,
/// Attempt to approximate rational curves (such as arcs) using a bezier.
/// This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
/// Over time, this field won't be necessary.
#[serde(default)]
pub bez_approximate_rational: Option<bool>,
/// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
#[serde(default)]
pub base_curve_index: Option<u32>,
/// Tolerance for the loft operation.
#[serde(default)]
pub tolerance: Option<f64>,
}
impl Default for LoftData {
fn default() -> Self {
Self {
// This unwrap is safe because the default value is always greater than zero.
v_degree: Some(std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
bez_approximate_rational: None,
base_curve_index: None,
tolerance: None,
}
}
}
/// Create a 3D surface or solid by interpolating between two or more sketches.
pub async fn loft(args: Args) -> Result<KclValue, KclError> {
let (sketch_groups, data): (Vec<SketchGroup>, Option<LoftData>) = args.get_sketch_groups_and_data()?;
let extrude_group = inner_loft(sketch_groups, data, args).await?;
Ok(KclValue::ExtrudeGroup(extrude_group))
}
/// Create a 3D surface or solid by interpolating between two or more sketches.
///
/// The sketches need to closed and on the same plane.
///
/// ```no_run
/// // Loft a square and a triangle.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const triangleSketch = startSketchOn(offsetPlane('XY', 75))
/// |> startProfileAt([0, 125], %)
/// |> line([-15, -30], %)
/// |> line([30, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// loft([squareSketch, triangleSketch])
/// ```
///
/// ```no_run
/// // Loft a square, a circle, and another circle.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// |> circle([0, 100], 50, %)
///
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// |> circle([0, 100], 20, %)
///
/// loft([squareSketch, circleSketch0, circleSketch1])
/// ```
///
/// ```no_run
/// // Loft a square, a circle, and another circle with options.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// |> circle([0, 100], 50, %)
///
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// |> circle([0, 100], 20, %)
///
/// loft([squareSketch, circleSketch0, circleSketch1], {
/// // This can be set to override the automatically determined
/// // topological base curve, which is usually the first section encountered.
/// baseCurveIndex: 0,
/// // Attempt to approximate rational curves (such as arcs) using a bezier.
/// // This will remove banding around interpolations between arcs and non-arcs.
/// // It may produce errors in other scenarios Over time, this field won't be necessary.
/// bezApproximateRational: false,
/// // Tolerance for the loft operation.
/// tolerance: 0.000001,
/// // Degree of the interpolation. Must be greater than zero.
/// // For example, use 2 for quadratic, or 3 for cubic interpolation in
/// // the V direction. This defaults to 2, if not specified.
/// vDegree: 2,
/// })
/// ```
#[stdlib {
name = "loft",
}]
async fn inner_loft(
sketch_groups: Vec<SketchGroup>,
data: Option<LoftData>,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
// Make sure we have at least two sketches.
if sketch_groups.len() < 2 {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Loft requires at least two sketches, but only {} were provided.",
sketch_groups.len()
),
source_ranges: vec![args.source_range],
}));
}
// Get the loft data.
let data = data.unwrap_or_default();
let id = uuid::Uuid::new_v4();
args.batch_modeling_cmd(
id,
ModelingCmd::Loft {
section_ids: sketch_groups.iter().map(|group| group.id).collect(),
base_curve_index: data.base_curve_index,
bez_approximate_rational: data.bez_approximate_rational.unwrap_or(false),
tolerance: data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units)),
v_degree: data
.v_degree
.unwrap_or_else(|| std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap())
.into(),
},
)
.await?;
// Using the first sketch as the base curve, idk we might want to change this later.
do_post_extrude(sketch_groups[0].clone(), 0.0, args).await
}

View File

@ -9,8 +9,10 @@ pub mod fillet;
pub mod helix; pub mod helix;
pub mod import; pub mod import;
pub mod kcl_stdlib; pub mod kcl_stdlib;
pub mod loft;
pub mod math; pub mod math;
pub mod patterns; pub mod patterns;
pub mod planes;
pub mod polar; pub mod polar;
pub mod revolve; pub mod revolve;
pub mod segment; pub mod segment;
@ -98,6 +100,8 @@ lazy_static! {
Box::new(crate::std::shell::Shell), Box::new(crate::std::shell::Shell),
Box::new(crate::std::shell::Hollow), Box::new(crate::std::shell::Hollow),
Box::new(crate::std::revolve::Revolve), Box::new(crate::std::revolve::Revolve),
Box::new(crate::std::loft::Loft),
Box::new(crate::std::planes::OffsetPlane),
Box::new(crate::std::import::Import), Box::new(crate::std::import::Import),
Box::new(crate::std::math::Cos), Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin), Box::new(crate::std::math::Sin),
@ -484,7 +488,7 @@ layout: manual
buf.push_str(&fn_docs); buf.push_str(&fn_docs);
// Write the file. // Write the file.
expectorate::assert_contents(&format!("../../../docs/kcl/{}.md", internal_fn.name()), &buf); expectorate::assert_contents(format!("../../../docs/kcl/{}.md", internal_fn.name()), &buf);
} }
} }

View File

@ -0,0 +1,168 @@
//! Standard library plane helpers.
use derive_docs::stdlib;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
executor::{KclValue, Metadata, Plane, UserVal},
std::{sketch::PlaneData, Args},
};
/// One of the standard planes.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum StandardPlane {
/// The XY plane.
#[serde(rename = "XY", alias = "xy")]
XY,
/// The opposite side of the XY plane.
#[serde(rename = "-XY", alias = "-xy")]
NegXY,
/// The XZ plane.
#[serde(rename = "XZ", alias = "xz")]
XZ,
/// The opposite side of the XZ plane.
#[serde(rename = "-XZ", alias = "-xz")]
NegXZ,
/// The YZ plane.
#[serde(rename = "YZ", alias = "yz")]
YZ,
/// The opposite side of the YZ plane.
#[serde(rename = "-YZ", alias = "-yz")]
NegYZ,
}
impl From<StandardPlane> for PlaneData {
fn from(value: StandardPlane) -> Self {
match value {
StandardPlane::XY => PlaneData::XY,
StandardPlane::NegXY => PlaneData::NegXY,
StandardPlane::XZ => PlaneData::XZ,
StandardPlane::NegXZ => PlaneData::NegXZ,
StandardPlane::YZ => PlaneData::YZ,
StandardPlane::NegYZ => PlaneData::NegYZ,
}
}
}
/// Offset a plane by a distance along its normal.
pub async fn offset_plane(args: Args) -> Result<KclValue, KclError> {
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
let plane = inner_offset_plane(std_plane, offset).await?;
Ok(KclValue::UserVal(UserVal::set(
vec![Metadata {
source_range: args.source_range,
}],
plane,
)))
}
/// Offset a plane by a distance along its normal.
///
/// For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ'
/// plane and 10 units away from it.
///
/// ```no_run
/// // Loft a square and a circle on the `XY` plane using offset.
/// const squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('XY', 150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `XZ` plane using offset.
/// const squareSketch = startSketchOn('XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('XZ', 150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `YZ` plane using offset.
/// const squareSketch = startSketchOn('YZ')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('YZ', 150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `-XZ` plane using offset.
/// const squareSketch = startSketchOn('-XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
/// |> line([-200, 0], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch = startSketchOn(offsetPlane('-XZ', -150))
/// |> circle([0, 100], 50, %)
///
/// loft([squareSketch, circleSketch])
/// ```
#[stdlib {
name = "offsetPlane",
}]
async fn inner_offset_plane(std_plane: StandardPlane, offset: f64) -> Result<PlaneData, KclError> {
// Convert to the plane type.
let plane_data: PlaneData = std_plane.into();
// Convert to a plane.
let mut plane = Plane::from(plane_data);
match std_plane {
StandardPlane::XY => {
plane.origin.z += offset;
}
StandardPlane::XZ => {
plane.origin.y -= offset;
}
StandardPlane::YZ => {
plane.origin.x += offset;
}
StandardPlane::NegXY => {
plane.origin.z -= offset;
}
StandardPlane::NegXZ => {
plane.origin.y += offset;
}
StandardPlane::NegYZ => {
plane.origin.x -= offset;
}
}
Ok(PlaneData::Plane {
origin: Box::new(plane.origin),
x_axis: Box::new(plane.x_axis),
y_axis: Box::new(plane.y_axis),
z_axis: Box::new(plane.z_axis),
})
}

View File

@ -299,7 +299,7 @@ async fn inner_revolve(
} }
} }
do_post_extrude(sketch_group, 0.0, id, args).await do_post_extrude(sketch_group, 0.0, args).await
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1269,7 +1269,7 @@ pub(crate) async fn inner_start_profile_at(
adjust_camera: false, adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface { planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
// We pass in the normal for the plane here. // We pass in the normal for the plane here.
Some(plane.z_axis.clone().into()) Some(plane.z_axis.into())
} else { } else {
None None
}, },

View File

@ -50,13 +50,13 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
} }
fn block_comment(i: &mut Located<&str>) -> PResult<Token> { fn block_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = ("/*", take_until(0.., "*/"), "*/").recognize(); let inner = ("/*", take_until(0.., "*/"), "*/").take();
let (value, range) = inner.with_span().parse_next(i)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::BlockComment, value.to_string())) Ok(Token::from_range(range, TokenType::BlockComment, value.to_string()))
} }
fn line_comment(i: &mut Located<&str>) -> PResult<Token> { fn line_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).recognize(); let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).take();
let (value, range) = inner.with_span().parse_next(i)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::LineComment, value.to_string())) Ok(Token::from_range(range, TokenType::LineComment, value.to_string()))
} }
@ -68,7 +68,7 @@ fn number(i: &mut Located<&str>) -> PResult<Token> {
// No digits before the decimal point. // No digits before the decimal point.
('.', digit1).map(|_| ()), ('.', digit1).map(|_| ()),
)); ));
let (value, range) = number_parser.recognize().with_span().parse_next(i)?; let (value, range) = number_parser.take().with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::Number, value.to_string())) Ok(Token::from_range(range, TokenType::Number, value.to_string()))
} }
@ -79,12 +79,12 @@ fn whitespace(i: &mut Located<&str>) -> PResult<Token> {
fn inner_word(i: &mut Located<&str>) -> PResult<()> { fn inner_word(i: &mut Located<&str>) -> PResult<()> {
one_of(('a'..='z', 'A'..='Z', '_')).parse_next(i)?; one_of(('a'..='z', 'A'..='Z', '_')).parse_next(i)?;
repeat(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?; repeat::<_, _, (), _, _>(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?;
Ok(()) Ok(())
} }
fn word(i: &mut Located<&str>) -> PResult<Token> { fn word(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = inner_word.recognize().with_span().parse_next(i)?; let (value, range) = inner_word.take().with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::Word, value.to_string())) Ok(Token::from_range(range, TokenType::Word, value.to_string()))
} }
@ -162,9 +162,9 @@ fn inner_single_quote(i: &mut Located<&str>) -> PResult<()> {
} }
fn string(i: &mut Located<&str>) -> PResult<Token> { fn string(i: &mut Located<&str>) -> PResult<Token> {
let single_quoted_string = ('\'', inner_single_quote.recognize(), '\''); let single_quoted_string = ('\'', inner_single_quote.take(), '\'');
let double_quoted_string = ('"', inner_double_quote.recognize(), '"'); let double_quoted_string = ('"', inner_double_quote.take(), '"');
let either_quoted_string = alt((single_quoted_string.recognize(), double_quoted_string.recognize())); let either_quoted_string = alt((single_quoted_string.take(), double_quoted_string.take()));
let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?; let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::String, value.to_string())) Ok(Token::from_range(range, TokenType::String, value.to_string()))
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,3 +1,3 @@
[toolchain] [toolchain]
channel = "1.80.1" channel = "1.81.0"
components = ["clippy", "rustfmt"] components = ["clippy", "rustfmt"]

View File

@ -56,10 +56,10 @@ const bracketBody = bs
|> fillet({ |> fillet({
radius: radius, radius: radius,
tags: [ tags: [
getNextAdjacentEdge(bs.sketchGroup.tags.edge7), getPreviousAdjacentEdge(bs.sketchGroup.tags.edge7),
getNextAdjacentEdge(bs.sketchGroup.tags.edge2), getPreviousAdjacentEdge(bs.sketchGroup.tags.edge2),
getNextAdjacentEdge(bs.sketchGroup.tags.edge3), getPreviousAdjacentEdge(bs.sketchGroup.tags.edge3),
getNextAdjacentEdge(bs.sketchGroup.tags.edge6) getPreviousAdjacentEdge(bs.sketchGroup.tags.edge6)
] ]
}, %) }, %)

View File

@ -55,10 +55,10 @@ const bracketBody = bs
|> fillet({ |> fillet({
radius: radius, radius: radius,
tags: [ tags: [
getNextAdjacentEdge(bs.tags.edge7), getPreviousAdjacentEdge(bs.tags.edge7),
getNextAdjacentEdge(bs.tags.edge2), getPreviousAdjacentEdge(bs.tags.edge2),
getNextAdjacentEdge(bs.tags.edge3), getPreviousAdjacentEdge(bs.tags.edge3),
getNextAdjacentEdge(bs.tags.edge6) getPreviousAdjacentEdge(bs.tags.edge6)
] ]
}, %) }, %)

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