Compare commits

..

2 Commits

162 changed files with 4227 additions and 12808 deletions

View File

@ -3,3 +3,4 @@ VITE_KC_API_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_SENTRY_DSN=

View File

@ -3,3 +3,4 @@ VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

@ -1,85 +0,0 @@
name: Bug Report
description: File a bug report for the Zoo Modeling App
title: "[BUG]: "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us resolve it."
- type: textarea
id: describe-bug
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: "Explain the bug..."
validations:
required: true
- type: textarea
id: reproduce-bug
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: Description of what you expected to happen.
placeholder: "I expected that..."
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots and Recordings
description: If applicable, add screenshots to help explain your problem. Maximum upload size is 10MB.
placeholder: "You can attach images or video recordings here."
validations:
required: false
- type: input
id: desktop-os
attributes:
label: Desktop OS
description: "Your operating system"
placeholder: "example: Windows 10, MacOS Big Sur"
validations:
required: true
- type: input
id: browser
attributes:
label: Browser
description: "If you are using the web version, please specify the browser you are using."
placeholder: "example: Chrome, Safari"
validations:
required: false
- type: input
id: version
attributes:
label: Version
description: "The version of the Zoo Modeling App you're using."
placeholder: "example: v0.15.0. You can find this in the settings."
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
placeholder: "Anything else you want to add..."
validations:
required: false

View File

@ -40,20 +40,6 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest - uses: taiki-e/install-action@nextest
- name: Rust Cache - name: Rust Cache
@ -62,7 +48,7 @@ jobs:
shell: bash shell: bash
run: |- run: |-
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log cargo nextest run --workspace --no-fail-fast -P ci
env: env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000 RUST_MIN_STACK: 10485760000

View File

@ -336,7 +336,7 @@ jobs:
cat last_download.json cat last_download.json
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.2' uses: 'google-github-actions/auth@v2.1.1'
with: with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}' credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
@ -374,7 +374,6 @@ jobs:
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'release'
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -4,11 +4,6 @@ on:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60
@ -19,7 +14,7 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- uses: KittyCAD/action-install-cli@v0.2.21 - uses: KittyCAD/action-install-cli@v0.2.16
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
@ -85,6 +80,7 @@ jobs:
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-14
needs: playwright-ubuntu
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

View File

@ -1,21 +0,0 @@
[sources.github-actions-file]
type = "file"
data_dir = "/tmp/vector"
include = ["/tmp/github-actions.log"]
# Modify the logs to include the action name.
[transforms.add-action-name]
type = "remap"
inputs = [ "github-actions-file" ]
source = '''
.action = "GITHUB_WORKFLOW"
.repo = "GITHUB_REPOSITORY"
.sha = "GITHUB_SHA"
.ref = "GITHUB_REF_NAME"
'''
[sinks.axiom]
type = "axiom"
inputs = ["add-action-name"]
token = "GH_ACTIONS_AXIOM_TOKEN"
dataset = "github-actions"

3
.gitignore vendored
View File

@ -33,7 +33,6 @@ src/wasm-lib/bindings
src/wasm-lib/kcl/bindings src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info src/wasm-lib/lcov.info
src/wasm-lib/grackle/test_json_output
e2e/playwright/playwright-secrets.env e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png e2e/playwright/temp1.png
@ -55,5 +54,3 @@ e2e/playwright/export-snapshots/*embedded.gltf
## generated files ## generated files
src/**/*.typegen.ts src/**/*.typegen.ts
src/wasm-lib/grackle/stdlib_cube_partial.json

View File

@ -6,9 +6,11 @@ once fixed in engine will just start working here with no language changes.
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you - **Sketch on Face**: If your sketch is outside the edges of the face (on which you
are sketching) you will get multiple models returned instead of one single are sketching) you will get multiple models returned instead of one single
model for that sketch and its underlying 3D object. model for that sketch and its underlying 3D object.
If you see a red line around your model, it means this is happening.
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
item in the pattern is being subtracted. This is an engine bug that is being
worked on.
- **Import**: Right now you can import a file, even if that file has brep data - **Import**: Right now you can import a file, even if that file has brep data
you cannot edit it, after v1, the engine will account for this. You also cannot you cannot edit it. You also cannot move or transform the imported objects at
currently move or transform the imported objects at all, once we have assemblies all. In the future, after v1, the engine will account for this.
this will work.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -3,8 +3,6 @@ import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme' import { Themes } from '../../src/lib/theme'
import { roundOff } from 'lib/utils'
import { platform } from 'node:os'
/* /*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
@ -16,12 +14,6 @@ document.addEventListener('mousemove', (e) =>
) )
*/ */
const commonPoints = {
startAt: '[9.06, -12.22]',
num1: 9.14,
num2: 18.2,
}
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up // wait for Vite preview server to be up
await waitOn({ await waitOn({
@ -60,15 +52,14 @@ test('Basic sketch', async ({ page }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await u.doAndWaitForImageDiff(
await page.waitForTimeout(100) () => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane // select a plane
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
@ -81,33 +72,35 @@ test('Basic sketch', async ({ page }) => {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num = 23.97
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> line([${num}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${num}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${num}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${num}, 0], %)
|> line([0, ${commonPoints.num1}], %) |> line([0, ${num}], %)
|> line([-${commonPoints.num2}, 0], %)`) |> line([-47.71, 0], %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
@ -129,132 +122,12 @@ test('Basic sketch', async ({ page }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %) |> line({ to: [${num}, 0], tag: 'seg01' }, %)
|> line([0, ${commonPoints.num1}], %) |> line([0, ${num}], %)
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
test('Can moving camera', async ({ page, context }) => {
test.skip(process.platform === 'darwin', 'Can moving camera')
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async (
mouseActions: any,
xyz: [number, number, number],
cnt = 0
) => {
// hack that we're implemented our own retry instead of using retries built into playwright.
// however each of these camera drags can be flaky, because of udp
// and so putting them together means only one needs to fail to make this test extra flaky.
// this way we can retry within the test
// We could break them out into separate tests, but the longest past of the test is waiting
// for the stream to start, so it can be good to bundle related things together.
await u.updateCamPosition(camPos)
await page.waitForTimeout(100)
// rotate
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// const yo = page.getByTestId('cam-x-position').inputValue()
await u.doAndWaitForImageDiff(async () => {
await mouseActions()
await u.openAndClearDebugPanel()
await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300)
await u.openAndClearDebugPanel()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const xError = Math.abs(Number(vals[0]) + xyz[0])
const yError = Math.abs(Number(vals[1]) + xyz[1])
const zError = Math.abs(Number(vals[2]) + xyz[2])
let shouldRetry = false
if (xError > 5 || yError > 5 || zError > 5) {
if (cnt > 2) {
console.log('xVal', vals[0], 'xError', xError)
console.log('yVal', vals[1], 'yError', yError)
console.log('zVal', vals[2], 'zError', zError)
throw new Error('Camera position not as expected')
}
shouldRetry = true
}
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(100)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
}
await bakeInRetries(async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 303)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 })
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-10, -85, -85])
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(200)
// zoom
await u.doAndWaitForImageDiff(async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel()
}, 300)
// zoom with scroll
await u.openAndClearDebugPanel()
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await bakeInRetries(async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -100)
}, [1, -94, -94])
})
test('if you write invalid kcl you get inlined errors', async ({ page }) => { test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
@ -403,9 +276,10 @@ test('Can create sketches on all planes and their back sides', async ({
}) => { }) => {
await u.openDebugPanel() await u.openDebugPanel()
await u.updateCamPosition(viewCmd)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.updateCamPosition(viewCmd)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y) await page.mouse.click(clickCoords.x, clickCoords.y)
@ -431,9 +305,11 @@ test('Can create sketches on all planes and their back sides', async ({
} }
const codeTemplate = ( const codeTemplate = (
plane = 'XY' plane = 'XY',
rounded = false,
otherThing = '1'
) => `const part001 = startSketchOn('${plane}') ) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([1.14, -1.54], %)` |> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)`
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camPos,
expectedCode: codeTemplate('XY'), expectedCode: codeTemplate('XY'),
@ -442,8 +318,8 @@ test('Can create sketches on all planes and their back sides', async ({
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camPos,
expectedCode: codeTemplate('YZ'), expectedCode: codeTemplate('YZ', true),
clickCoords: { x: 700, y: 250 }, // green plane clickCoords: { x: 700, y: 300 }, // green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camPos,
@ -453,7 +329,7 @@ test('Can create sketches on all planes and their back sides', async ({
const camCmdBackSide: [number, number, number] = [-100, -100, -100] const camCmdBackSide: [number, number, number] = [-100, -100, -100]
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY'), expectedCode: codeTemplate('-XY', false, '3'),
clickCoords: { x: 601, y: 118 }, // back of red plane clickCoords: { x: 601, y: 118 }, // back of red plane
}) })
await TestSinglePlane({ await TestSinglePlane({
@ -463,7 +339,7 @@ test('Can create sketches on all planes and their back sides', async ({
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ'), expectedCode: codeTemplate('-XZ', true),
clickCoords: { x: 680, y: 427 }, // back of blue plane clickCoords: { x: 680, y: 427 }, // back of blue plane
}) })
}) })
@ -504,16 +380,12 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// finish line with comment await page.keyboard.type('(5, %)')
await page.keyboard.type('(5, %) // lin')
await page.waitForTimeout(100)
// there shouldn't be any auto complete options for 'lin' in the comment
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XY') .toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %) |> startProfileAt([0,0], %)
|> xLine(5, %) // lin`) |> xLine(5, %)`)
}) })
// Onboarding tests // Onboarding tests
@ -581,9 +453,6 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
page.mouse.click(767, 396).then(() => page.waitForTimeout(100)) page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
await u.clearCommandLogs() await u.clearCommandLogs()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
// select a plane // select a plane
@ -592,32 +461,35 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${startAt}, %)`)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const num = 23.97
const num2 = '47.71'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> line([${num}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${num}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${num}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${num}, 0], %)
|> line([0, ${commonPoints.num1}], %) |> line([0, ${num}], %)
|> line([-${commonPoints.num2}, 0], %)`) |> line([-${num2}, 0], %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
@ -667,7 +539,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick() await emptySpaceClick()
// check the same selection again by putting cursor in code first then selecting axis // check the same selection again by putting cursor in code first then selecting axis
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await xAxisClick() await xAxisClick()
@ -678,7 +550,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick() await emptySpaceClick()
// select segment in editor than another segment in scene and check there are two cursors // select segment in editor than another segment in scene and check there are two cursors
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.waitForTimeout(300) await page.waitForTimeout(300)
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1) await expect(page.locator('.cm-cursor')).toHaveCount(1)
@ -703,7 +575,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// select a line // select a line
// await topHorzSegmentClick() // await topHorzSegmentClick()
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick() await page.getByText(startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// enter sketch again // enter sketch again
@ -743,12 +615,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
const themeOption = page.getByRole('option', { name: 'Set Theme' }) const themeOption = page.getByRole('option', { name: 'Set Theme' })
await expect(themeOption).toBeVisible() await expect(themeOption).toBeVisible()
await themeOption.click() await themeOption.click()
const themeInput = page.getByPlaceholder('system') const themeInput = page.getByPlaceholder('Select an option')
await expect(themeInput).toBeVisible() await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused() await expect(themeInput).toBeFocused()
// Select dark theme // Select dark theme
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute( await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
'data-headlessui-state', 'data-headlessui-state',
'active' 'active'
@ -765,15 +637,12 @@ test('Can extrude from the command bar', async ({ page, context }) => {
await context.addInitScript(async (token) => { await context.addInitScript(async (token) => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
` `const part001 = startSketchOn('-XZ')
const distance = sqrt(20)
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %) |> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
|> line([0.73, -14.93], %) |> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)`
`
) )
}) })
@ -798,44 +667,24 @@ test('Can extrude from the command bar', async ({ page, context }) => {
// Click to select face and set distance // Click to select face and set distance
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await page.getByRole('button', { name: 'Continue' }).click() await page.getByRole('button', { name: 'Continue' }).click()
// Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
// Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click()
// Review step and argument hotkeys
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeEnabled()
await page.keyboard.press('Backspace')
await expect(
page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible() // Review step and argument hotkeys
await page.keyboard.press('2')
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
await page.keyboard.press('Enter')
// Check that the code was updated // Check that the code was updated
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`const distance = sqrt(20) `const part001 = startSketchOn('-XZ')
const distance001 = 5 + 7
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %) |> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
|> line([0.73, -14.93], %) |> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines |> extrude(5, %)`
) )
}) })
@ -847,9 +696,6 @@ test('Can add multiple sketches', async ({ page }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
@ -870,32 +716,34 @@ test('Can add multiple sketches', async ({ page }) => {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num = 23.97
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> line([${num}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${num}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${num}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ') const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${num}, 0], %)
|> line([0, ${commonPoints.num1}], %) |> line([0, ${num}], %)
|> line([-${commonPoints.num2}, 0], %)` |> line([-47.71, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch) await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
// exit the sketch // exit the sketch
@ -917,7 +765,7 @@ test('Can add multiple sketches', async ({ page }) => {
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[0.93,-1.25]' const startAt2 = '[23.61, -31.85]'
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
@ -931,7 +779,7 @@ const part002 = startSketchOn('XY')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num2 = 0.94 const num2 = 23.83
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
@ -949,7 +797,7 @@ const part002 = startSketchOn('XY')
const part002 = startSketchOn('XY') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '') |> line([0, ${num2}], %)`.replace(/\s/g, '')
) )
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect( await expect(
@ -959,8 +807,8 @@ const part002 = startSketchOn('XY')
const part002 = startSketchOn('XY') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff(num2 - 0.01)}], %) |> line([0, ${num2}], %)
|> line([-1.87, 0], %)`.replace(/\s/g, '') |> line([-47.44, 0], %)`.replace(/\s/g, '')
) )
}) })
@ -1070,11 +918,6 @@ fn yohey = (pos) => {
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
// wait for start sketch as a proxy for the stream being ready
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click() await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect( await expect(
@ -1116,13 +959,11 @@ test('Deselecting line tool should mean nothing happens on click', async ({
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
@ -1173,160 +1014,3 @@ test('Deselecting line tool should mean nothing happens on click', async ({
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
}) })
test('Can edit segments by dragging their handles', async ({
page,
context,
}) => {
const u = getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const startPX = [652, 418]
const lineEndPX = [794, 416]
const arcEndPX = [893, 318]
const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(100)
let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 }
// drag startProfieAt handle
await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
await page.mouse.move(lineEndPX[0] + dragPX, lineEndPX[1] - dragPX)
await page.mouse.down()
await page.mouse.move(
lineEndPX[0] + dragPX * 2,
lineEndPX[1] - dragPX * 2,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArcTo handle
await page.mouse.move(arcEndPX[0], arcEndPX[1])
await page.mouse.down()
await page.mouse.move(arcEndPX[0] + dragPX, arcEndPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([7.01, -11.79], %)
|> line([14.69, 2.73], %)
|> tangentialArcTo([27.6, -3.25], %)`)
})
test('Snap to close works (at any scale)', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
const doSnapAtDifferentScales = async (
camPos: [number, number, number],
expectedCode: string
) => {
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.updateCamPosition(camPos)
await u.closeDebugPanel()
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('XZ')`
)
let prevContent = await page.locator('.cm-content').innerText()
const pointA = [700, 200]
const pointB = [900, 200]
const pointC = [900, 400]
// draw three lines
await page.mouse.click(pointA[0], pointA[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.click(pointB[0], pointB[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.click(pointC[0], pointC[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointA[0] - 12, pointA[1] + 12)
const pointNotQuiteA = [pointA[0] - 7, pointA[1] + 7]
await page.mouse.move(pointNotQuiteA[0], pointNotQuiteA[1], { steps: 10 })
await page.mouse.click(pointNotQuiteA[0], pointNotQuiteA[1])
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.removeCurrentCode()
}
const codeTemplate = (
scale = 1,
fudge = 0
) => `const part001 = startSketchOn('XZ')
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|> line([${roundOff(scale * 175.36)}, 0], %)
|> line([0, -${roundOff(scale * 175.37) + fudge}], %)
|> close(%)`
await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
})

View File

@ -29,10 +29,92 @@ test.beforeEach(async ({ context, page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.setTimeout(60_000) test.setTimeout(60000)
test('change camera, show planes', async ({ page, context }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
const camPos: [number, number, number] = [0, 85, 85]
await u.updateCamPosition(camPos)
// rotate
await u.closeDebugPanel()
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 300)
await page.mouse.up({ button: 'right' })
await u.openDebugPanel()
await page.waitForTimeout(500)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
// pan
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
// zoom
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('exports of each format should work', async ({ page, context }) => { test('exports of each format should work', async ({ page, context }) => {
test.setTimeout(120_000)
// FYI this test doesn't work with only engine running locally // FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed // And you will need to have the KittyCAD CLI installed
const u = getUtils(page) const u = getUtils(page)
@ -91,6 +173,8 @@ const part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.getByRole('button', { name: APP_NAME }).click()
interface Paths { interface Paths {
modelPath: string modelPath: string
imagePath: string imagePath: string
@ -99,21 +183,19 @@ const part001 = startSketchOn('-XZ')
const doExport = async ( const doExport = async (
output: Models['OutputFormat_type'] output: Models['OutputFormat_type']
): Promise<Paths> => { ): Promise<Paths> => {
await page.getByRole('button', { name: APP_NAME }).click() await page.getByRole('button', { name: 'Export Model' }).click()
await page.getByRole('button', { name: 'Export Part' }).click()
const exportSelect = page.getByTestId('export-type')
await exportSelect.selectOption({ label: output.type })
// Go through export via command bar
await page.getByRole('option', { name: output.type, exact: false }).click()
if ('storage' in output) { if ('storage' in output) {
await page.getByRole('button', { name: 'storage', exact: false }).click() const storageSelect = page.getByTestId('export-storage')
await page await storageSelect.selectOption({ label: output.storage })
.getByRole('option', { name: output.storage, exact: false })
.click()
} }
await page.getByRole('button', { name: 'Submit command' }).click()
// Handle download const downloadPromise = page.waitForEvent('download')
const download = await page.waitForEvent('download') await page.getByRole('button', { name: 'Export', exact: true }).click()
const download = await downloadPromise
const downloadLocationer = (extra = '', isImage = false) => const downloadLocationer = (extra = '', isImage = false) =>
`./e2e/playwright/export-snapshots/${output.type}-${ `./e2e/playwright/export-snapshots/${output.type}-${
'storage' in output ? output.storage : '' 'storage' in output ? output.storage : ''
@ -260,33 +342,24 @@ const part001 = startSketchOn('-XZ')
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes // snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
// context: https://github.com/KittyCAD/modeling-app/issues/1222 // context: https://github.com/KittyCAD/modeling-app/issues/1222
for (const { modelPath, imagePath, outputType } of exportLocations) { for (const { modelPath, imagePath, outputType } of exportLocations) {
console.log( const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
`taking snapshot of using: "zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}"`
)
const cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
const child = spawn(cliCommand, { shell: true }) const child = spawn(cliCommand, { shell: true })
const result = await new Promise<string>((resolve, reject) => { await new Promise((resolve, reject) => {
child.on('error', (code: any, msg: any) => { child.on('error', (code: any, msg: any) => {
console.log('error', code, msg) console.log('error', code, msg)
reject('error') reject()
}) })
child.on('exit', (code, msg) => { child.on('exit', (code, msg) => {
console.log('exit', code, msg) console.log('exit', code, msg)
if (code !== 0) { if (code !== 0) {
reject(`exit code ${code} for model ${modelPath}`) reject(`exit code ${code} for model ${modelPath}`)
} else { } else {
resolve('success') resolve(true)
} }
}) })
child.stderr.on('data', (data) => console.log(`stderr: ${data}`)) child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
child.stdout.on('data', (data) => console.log(`stdout: ${data}`)) child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
}) })
expect(result).toBe('success')
if (result === 'success') {
console.log(`snapshot taken for ${modelPath}`)
} else {
console.log(`snapshot failed for ${modelPath}`)
}
} }
}) })
@ -296,13 +369,13 @@ test('extrude on each default plane should be stable', async ({
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %) |> startProfileAt([0.70, 0.44], %)
|> line([6.60, -0.20], %) |> line([0.66, -0.02], %)
|> line([2.80, 5.00], %) |> line([0.28, 0.50], %)
|> line([-5.60, 4.40], %) |> line([-0.56, 0.44], %)
|> line([-5.40, -3.80], %) |> line([-0.54, -0.38], %)
|> close(%) |> close(%)
|> extrude(10.00, %) |> extrude(1.00, %)
` `
await context.addInitScript(async (code) => { await context.addInitScript(async (code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
@ -347,23 +420,7 @@ test('extrude on each default plane should be stable', async ({
await runSnapshotsForOtherPlanes('-YZ') await runSnapshotsForOtherPlanes('-YZ')
}) })
test('Draft segments should look right', async ({ page, context }) => { test('Draft segments should look right', async ({ page }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -371,9 +428,6 @@ test('Draft segments should look right', async ({ page, context }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
@ -394,9 +448,10 @@ test('Draft segments should look right', async ({ page, context }) => {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)`) |> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -408,10 +463,11 @@ test('Draft segments should look right', async ({ page, context }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num = 23.97
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %) |> startProfileAt(${startAt}, %)
|> line([9.14, 0], %)`) |> line([${num}, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -421,202 +477,3 @@ test('Draft segments should look right', async ({ page, context }) => {
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
test('Client side scene scale should match engine scale inch', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('Client side scene scale should match engine scale mm', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'mm',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'metric',
})
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -1,23 +1,16 @@
import { expect, Page, errors } from '@playwright/test' import { expect, Page } from '@playwright/test'
import { EngineCommand } from '../../src/lang/std/engineConnection' import { EngineCommand } from '../../src/lang/std/engineConnection'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import pixelMatch from 'pixelmatch' import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs' import { PNG } from 'pngjs'
async function waitForPageLoad(page: Page) { async function waitForPageLoad(page: Page) {
try {
// wait for 'Loading stream...' spinner // wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor() await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone // wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' }) await page.getByTestId('loading').waitFor({ state: 'detached' })
await page.getByTestId('start-sketch').waitFor() await page.getByTestId('start-sketch').waitFor()
} catch (e) {
if (e instanceof errors.TimeoutError) {
console.log('Timeout while waiting for page load.')
} else {
throw e // re-throw the error if it is not a TimeoutError
}
}
} }
async function removeCurrentCode(page: Page) { async function removeCurrentCode(page: Page) {

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.15.4", "version": "0.15.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.10.2", "@codemirror/autocomplete": "^6.10.2",
@ -10,11 +10,12 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.54", "@kittycad/lib": "^0.0.53",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0", "@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.77.0",
"@tauri-apps/api": "^1.5.1", "@tauri-apps/api": "^1.5.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
@ -84,7 +85,7 @@
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src", "lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen", "postinstall": "patch-package && yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"" "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
}, },
"prettier": { "prettier": {
@ -133,6 +134,7 @@
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^10.8.0", "happy-dom": "^10.8.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"patch-package": "^8.0.0",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",

138
patches/three+0.160.0.patch Normal file
View File

@ -0,0 +1,138 @@
diff --git a/node_modules/three/examples/jsm/controls/OrbitControls.js b/node_modules/three/examples/jsm/controls/OrbitControls.js
index f29e7fe..0ef636b 100644
--- a/node_modules/three/examples/jsm/controls/OrbitControls.js
+++ b/node_modules/three/examples/jsm/controls/OrbitControls.js
@@ -113,6 +113,25 @@ class OrbitControls extends EventDispatcher {
// public methods
//
+ this.interactionGuards = {
+ pan: {
+ description: 'Right click + Shift + drag or middle click + drag',
+ callback: (e) => e.button === 2 && !e.ctrlKey,
+ },
+ zoom: {
+ description: 'Scroll wheel or Right click + Ctrl + drag',
+ dragCallback: (e) => e.button === 2 && e.ctrlKey,
+ scrollCallback: () => true,
+ },
+ rotate: {
+ description: 'Right click + drag',
+ callback: (e) => e.button === 0,
+ },
+ }
+ this.setMouseGuards = (interactionGuards) => {
+ this.interactionGuards = interactionGuards
+ }
+
this.getPolarAngle = function () {
return spherical.phi;
@@ -1057,92 +1076,21 @@ class OrbitControls extends EventDispatcher {
function onMouseDown( event ) {
- let mouseAction;
-
- switch ( event.button ) {
-
- case 0:
-
- mouseAction = scope.mouseButtons.LEFT;
- break;
-
- case 1:
-
- mouseAction = scope.mouseButtons.MIDDLE;
- break;
-
- case 2:
-
- mouseAction = scope.mouseButtons.RIGHT;
- break;
-
- default:
-
- mouseAction = - 1;
-
- }
-
- switch ( mouseAction ) {
-
- case MOUSE.DOLLY:
-
- if ( scope.enableZoom === false ) return;
-
- handleMouseDownDolly( event );
-
- state = STATE.DOLLY;
-
- break;
-
- case MOUSE.ROTATE:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- } else {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- }
-
- break;
-
- case MOUSE.PAN:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- } else {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- }
-
- break;
-
- default:
-
- state = STATE.NONE;
-
- }
+ if (scope.interactionGuards.pan.callback(event)) {
+ if (scope.enablePan === false) return
+ handleMouseDownPan(event)
+ state = STATE.PAN
+ } else if (scope.interactionGuards.rotate.callback(event)) {
+ if (scope.enableRotate === false) return
+ handleMouseDownRotate(event)
+ state = STATE.ROTATE
+ } else if (scope.interactionGuards.zoom.dragCallback(event)) {
+ if (scope.enableZoom === false) return
+ handleMouseDownDolly(event)
+ state = STATE.DOLLY
+ } else {
+ return
+ }
if ( state !== STATE.NONE ) {

View File

@ -1,34 +1,16 @@
import re
import os
import requests import requests
import os
webhook_url = os.getenv('DISCORD_WEBHOOK_URL') webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
release_version = os.getenv('RELEASE_VERSION') release_version = os.getenv('RELEASE_VERSION')
release_body = os.getenv('RELEASE_BODY') release_body = os.getenv('RELEASE_BODY')
# Regular expression to match URLs # message to send to Discord
url_pattern = r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)'
# Function to encase URLs in <>
def encase_urls_with_angle_brackets(match):
url = match.group(0)
return f'<{url}>'
# Replace all URLs in the release_body with their <> enclosed version
modified_release_body = re.sub(url_pattern, encase_urls_with_angle_brackets, release_body)
# Ensure the modified_release_body does not exceed Discord's character limit
max_length = 500 # Adjust as needed
if len(modified_release_body) > max_length:
modified_release_body = modified_release_body[:max_length].rsplit(' ', 1)[0] # Avoid cutting off in the middle of a word
modified_release_body += "... for full changelog, check out the link above."
# Message to send to Discord
data = { data = {
"content": "content":
f''' f'''
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download> **{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download
{modified_release_body} {release_body}
''', ''',
"username": "Modeling App Release Updates", "username": "Modeling App Release Updates",
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png" "avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
@ -41,7 +23,4 @@ response = requests.post(webhook_url, json=data)
if response.status_code == 204: if response.status_code == 204:
print("Successfully sent the message to Discord.") print("Successfully sent the message to Discord.")
else: else:
print(f"Failed to send the message to Discord. Status code: {response.status_code}, Response: {response.text}") print("Failed to send the message to Discord.")
print(modified_release_body)
print(data["content"])

22
src-tauri/Cargo.lock generated
View File

@ -67,9 +67,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.80" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]] [[package]]
name = "app" name = "app"
@ -1664,9 +1664,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.58" version = "0.2.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb" checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -3235,9 +3235,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -3253,9 +3253,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.197" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3275,9 +3275,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.114" version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [ dependencies = [
"itoa 1.0.6", "itoa 1.0.6",
"ryu", "ryu",
@ -3872,7 +3872,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs-extra" name = "tauri-plugin-fs-extra"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#ed682dd96eb765e7cd3cdbc3cc64f794a0d6f9df" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#01211ff0759d578e0e9ac8c98c31fdf09077eb34"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",

View File

@ -16,7 +16,7 @@ tauri-build = { version = "1.5.1", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.58" kittycad = "0.2.53"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -7,6 +7,7 @@ use std::io::Read;
use anyhow::Result; use anyhow::Result;
use oauth2::TokenResponse; use oauth2::TokenResponse;
use std::process::Command;
use tauri::{InvokeError, Manager}; use tauri::{InvokeError, Manager};
const DEFAULT_HOST: &str = "https://api.kittycad.io"; const DEFAULT_HOST: &str = "https://api.kittycad.io";

View File

@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "zoo-modeling-app", "productName": "zoo-modeling-app",
"version": "0.15.4" "version": "0.15.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -137,9 +137,14 @@ export function App() {
enableMenu={true} enableMenu={true}
/> />
<ModalContainer /> <ModalContainer />
<div className="h-full">
<div className="h-full flex">
<div className="h-full w-5 z-10 pointer-events-auto"></div>
<div className="flex flex-col">
<div className="h-5 z-10 pointer-events-auto"></div>
<Resizable <Resizable
className={ className={
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' + 'pointer-events-none h-full flex flex-col flex-1 z-10 pr-1 transition-opacity transition-duration-75 ' +
+paneOpacity +paneOpacity
} }
defaultSize={{ defaultSize={{
@ -193,6 +198,10 @@ export function App() {
</section> </section>
</div> </div>
</Resizable> </Resizable>
<div className="h-5 z-10 pointer-events-auto"></div>
</div>
</div>
</div>
<Stream className="absolute inset-0 z-0" /> <Stream className="absolute inset-0 z-0" />
{showDebugPanel && ( {showDebugPanel && (
<DebugPanel <DebugPanel

View File

@ -3,8 +3,15 @@ import {
createBrowserRouter, createBrowserRouter,
Outlet, Outlet,
redirect, redirect,
useLocation,
RouterProvider, RouterProvider,
} from 'react-router-dom' } from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage' import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings' import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding' import Onboarding, { onboardingRoutes } from './routes/Onboarding'
@ -28,9 +35,9 @@ import {
settingsMachine, settingsMachine,
} from './machines/settingsMachine' } from './machines/settingsMachine'
import { ContextFrom } from 'xstate' import { ContextFrom } from 'xstate'
import CommandBarProvider, { import CommandBarProvider from 'components/CommandBar/CommandBar'
CommandBar, import { TEST, VITE_KC_SENTRY_DSN } from './env'
} from 'components/CommandBar/CommandBar' import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider' import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSingleton' import { KclContextProvider, kclManager } from 'lang/KclSingleton'
import FileMachineProvider from 'components/FileMachineProvider' import FileMachineProvider from 'components/FileMachineProvider'
@ -39,6 +46,38 @@ import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types' import { IndexLoaderData, HomeLoaderData } from 'lib/types'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({
dsn: VITE_KC_SENTRY_DSN,
// TODO(paultag): pass in the right env here.
// environment: "production",
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
),
}),
new Sentry.Replay(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
tracesSampleRate: 1.0,
// TODO: Add in kittycad.io endpoints
tracePropagationTargets: ['localhost'],
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
export const BROWSER_FILE_NAME = 'new' export const BROWSER_FILE_NAME = 'new'
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0] type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
@ -78,7 +117,6 @@ const router = createBrowserRouter(
<ModelingMachineProvider> <ModelingMachineProvider>
<Outlet /> <Outlet />
<App /> <App />
<CommandBar />
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner /> <WasmErrBanner />
</FileMachineProvider> </FileMachineProvider>
@ -178,7 +216,6 @@ const router = createBrowserRouter(
<Auth> <Auth>
<Outlet /> <Outlet />
<Home /> <Home />
<CommandBar />
</Auth> </Auth>
), ),
loader: async (): Promise<HomeLoaderData | Response> => { loader: async (): Promise<HomeLoaderData | Response> => {

View File

@ -6,12 +6,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { isSingleCursorInPipe } from 'lang/queryAst' import { isSingleCursorInPipe } from 'lang/queryAst'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSingleton'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore'
export const Toolbar = () => { export const Toolbar = () => {
const platform = usePlatform() const platform = usePlatform()
@ -29,13 +24,6 @@ export const Toolbar = () => {
context.selectionRanges context.selectionRanges
) )
}, [engineCommandManager.artifactMap, context.selectionRanges]) }, [engineCommandManager.artifactMap, context.selectionRanges])
const { overallState } = useNetworkStatus()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) { function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current const span = toolbarButtonsRef.current
@ -72,7 +60,6 @@ export const Toolbar = () => {
icon: 'sketch', icon: 'sketch',
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons}
> >
<span data-testid="start-sketch">Start Sketch</span> <span data-testid="start-sketch">Start Sketch</span>
</ActionButton> </ActionButton>
@ -87,7 +74,6 @@ export const Toolbar = () => {
icon: 'sketch', icon: 'sketch',
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons}
> >
Edit Sketch Edit Sketch
</ActionButton> </ActionButton>
@ -102,7 +88,6 @@ export const Toolbar = () => {
icon: 'arrowLeft', icon: 'arrowLeft',
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons}
> >
Exit Sketch Exit Sketch
</ActionButton> </ActionButton>
@ -124,7 +109,6 @@ export const Toolbar = () => {
icon: 'line', icon: 'line',
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons}
> >
Line Line
</ActionButton> </ActionButton>
@ -144,9 +128,8 @@ export const Toolbar = () => {
bgClassName, bgClassName,
}} }}
disabled={ disabled={
(!state.can('Equip tangential arc to') && !state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')) || !state.matches('Sketch.Tangential arc to')
disableAllButtons
} }
> >
Tangential Arc Tangential Arc
@ -186,7 +169,7 @@ export const Toolbar = () => {
disabled={ disabled={
!state.nextEvents !state.nextEvents
.filter((event) => state.can(event as any)) .filter((event) => state.can(event as any))
.includes(eventName) || disableAllButtons .includes(eventName)
} }
title={eventName} title={eventName}
icon={{ icon={{
@ -211,7 +194,7 @@ export const Toolbar = () => {
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', ownerMachine: 'modeling' },
}) })
} }
disabled={!state.can('Extrude') || disableAllButtons} disabled={!state.can('Extrude')}
title={ title={
state.can('Extrude') state.can('Extrude')
? 'extrude' ? 'extrude'

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,11 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { DEBUG_SHOW_BOTH_SCENES, sceneInfra } from './sceneInfra' import {
import { ReactCameraProperties } from './CameraControls' DEBUG_SHOW_BOTH_SCENES,
ReactCameraProperties,
sceneInfra,
} from './sceneInfra'
import { throttle } from 'lib/utils' import { throttle } from 'lib/utils'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
@ -15,7 +18,7 @@ function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const { state } = useModelingContext() const { state } = useModelingContext()
useEffect(() => { useEffect(() => {
sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => { sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
setIsCamMoving(isMoving) setIsCamMoving(isMoving)
setIsTween(isTween) setIsTween(isTween)
}) })
@ -49,8 +52,7 @@ export const ClientSideScene = ({
// Listen for changes to the camera controls setting // Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly. // and update the client-side scene's controls accordingly.
useEffect(() => { useEffect(() => {
sceneInfra.camControls.interactionGuards = sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
cameraMouseDragGuards[cameraControls]
}, [cameraControls]) }, [cameraControls])
useEffect(() => { useEffect(() => {
sceneInfra.updateOtherSelectionColors( sceneInfra.updateOtherSelectionColors(
@ -91,7 +93,7 @@ export const ClientSideScene = ({
const throttled = throttle((a: ReactCameraProperties) => { const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) { if (a.type === 'perspective' && a.fov) {
sceneInfra.camControls.dollyZoom(a.fov) sceneInfra.dollyZoom(a.fov)
} }
}, 1000 / 15) }, 1000 / 15)
@ -105,7 +107,7 @@ export const CamDebugSettings = () => {
const [fov, setFov] = useState(12) const [fov, setFov] = useState(12)
useEffect(() => { useEffect(() => {
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings) sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
}, [sceneInfra]) }, [sceneInfra])
useEffect(() => { useEffect(() => {
if (camSettings.type === 'perspective' && camSettings.fov) { if (camSettings.type === 'perspective' && camSettings.fov) {
@ -122,9 +124,9 @@ export const CamDebugSettings = () => {
checked={camSettings.type === 'perspective'} checked={camSettings.type === 'perspective'}
onChange={(e) => { onChange={(e) => {
if (camSettings.type === 'perspective') { if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera() sceneInfra.useOrthographicCamera()
} else { } else {
sceneInfra.camControls.usePerspectiveCamera() sceneInfra.usePerspectiveCamera()
} }
}} }}
/> />
@ -154,7 +156,7 @@ export const CamDebugSettings = () => {
value={camSettings.fov} value={camSettings.fov}
className="text-black w-16" className="text-black w-16"
onChange={(e) => { onChange={(e) => {
sceneInfra.camControls.setCam({ sceneInfra.setCam({
...camSettings, ...camSettings,
fov: parseFloat(e.target.value), fov: parseFloat(e.target.value),
}) })
@ -171,7 +173,7 @@ export const CamDebugSettings = () => {
value={camSettings.zoom} value={camSettings.zoom}
className="text-black w-16" className="text-black w-16"
onChange={(e) => { onChange={(e) => {
sceneInfra.camControls.setCam({ sceneInfra.setCam({
...camSettings, ...camSettings,
zoom: parseFloat(e.target.value), zoom: parseFloat(e.target.value),
}) })
@ -192,7 +194,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[0]} value={camSettings.position[0]}
className="text-black w-16" className="text-black w-16"
onChange={(e) => { onChange={(e) => {
sceneInfra.camControls.setCam({ sceneInfra.setCam({
...camSettings, ...camSettings,
position: [ position: [
parseFloat(e.target.value), parseFloat(e.target.value),
@ -212,7 +214,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[1]} value={camSettings.position[1]}
className="text-black w-16" className="text-black w-16"
onChange={(e) => { onChange={(e) => {
sceneInfra.camControls.setCam({ sceneInfra.setCam({
...camSettings, ...camSettings,
position: [ position: [
camSettings.position[0], camSettings.position[0],
@ -232,7 +234,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[2]} value={camSettings.position[2]}
className="text-black w-16" className="text-black w-16"
onChange={(e) => { onChange={(e) => {
sceneInfra.camControls.setCam({ sceneInfra.setCam({
...camSettings, ...camSettings,
position: [ position: [
camSettings.position[0], camSettings.position[0],

View File

@ -1,4 +1,3 @@
import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { import {
GridHelper, GridHelper,
LineBasicMaterial, LineBasicMaterial,
@ -6,8 +5,6 @@ import {
PerspectiveCamera, PerspectiveCamera,
Group, Group,
Mesh, Mesh,
Quaternion,
Vector3,
} from 'three' } from 'three'
export function createGridHelper({ export function createGridHelper({
@ -34,9 +31,3 @@ export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) => export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov) / 4000 (group.position.distanceTo(cam.position) * cam.fov) / 4000
export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q)
// no x or y components means it's vertical
return compareVec2Epsilon2([v.x, v.y], [0, 0])
}

View File

@ -3,13 +3,10 @@ import {
DoubleSide, DoubleSide,
ExtrudeGeometry, ExtrudeGeometry,
Group, Group,
Intersection,
LineCurve3, LineCurve3,
Matrix4, Matrix4,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Object3D,
Object3DEventMap,
OrthographicCamera, OrthographicCamera,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
@ -27,7 +24,7 @@ import {
defaultPlaneColor, defaultPlaneColor,
getSceneScale, getSceneScale,
INTERSECTION_PLANE_LAYER, INTERSECTION_PLANE_LAYER,
OnMouseEnterLeaveArgs, isQuaternionVertical,
RAYCASTABLE_PLANE, RAYCASTABLE_PLANE,
sceneInfra, sceneInfra,
SKETCH_GROUP_SEGMENTS, SKETCH_GROUP_SEGMENTS,
@ -37,7 +34,6 @@ import {
Y_AXIS, Y_AXIS,
YZ_PLANE, YZ_PLANE,
} from './sceneInfra' } from './sceneInfra'
import { isQuaternionVertical } from './helpers'
import { import {
CallExpression, CallExpression,
getTangentialArcToInfo, getTangentialArcToInfo,
@ -60,7 +56,6 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import { import {
createArcGeometry, createArcGeometry,
dashedStraight, dashedStraight,
profileStart,
straightSegment, straightSegment,
tangentialArcToSegment, tangentialArcToSegment,
} from './segments' } from './segments'
@ -68,7 +63,7 @@ import {
addCloseToPipe, addCloseToPipe,
addNewSketchLn, addNewSketchLn,
changeSketchArguments, changeSketchArguments,
updateStartProfileAtArgs, compareVec2Epsilon2,
} from 'lang/std/sketch' } from 'lang/std/sketch'
import { isReducedMotion, throttle } from 'lib/utils' import { isReducedMotion, throttle } from 'lib/utils'
import { import {
@ -90,7 +85,6 @@ export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body' export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH = export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed' 'tangential-arc-to-segment-body-dashed'
export const PROFILE_START = 'profile-start'
// This singleton Class is responsible for all of the things the user sees and interacts with. // This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements. // That mostly mean sketch elements.
@ -104,18 +98,17 @@ class SceneEntities {
currentSketchQuaternion: Quaternion | null = null currentSketchQuaternion: Quaternion | null = null
constructor() { constructor() {
this.scene = sceneInfra?.scene this.scene = sceneInfra?.scene
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange) sceneInfra?.setOnCamChange(this.onCamChange)
} }
onCamChange = () => { onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camera)
Object.values(this.activeSegments).forEach((segment) => { Object.values(this.activeSegments).forEach((segment) => {
const factor = const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera sceneInfra.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, segment)) / : perspScale(sceneInfra.camera, segment)
sceneInfra._baseUnitMultiplier
if ( if (
segment.userData.from && segment.userData.from &&
segment.userData.to && segment.userData.to &&
@ -143,29 +136,21 @@ class SceneEntities {
scale: factor, scale: factor,
}) })
} }
if (segment.name === PROFILE_START) {
segment.scale.set(factor, factor, factor)
}
}) })
if (this.axisGroup) { if (this.axisGroup) {
const factor = const factor =
sceneInfra.camControls.camera instanceof OrthographicCamera sceneInfra.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, this.axisGroup) : perspScale(sceneInfra.camera, this.axisGroup)
const x = this.axisGroup.getObjectByName(X_AXIS) const x = this.axisGroup.getObjectByName(X_AXIS)
x?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1) x?.scale.set(1, factor, 1)
const y = this.axisGroup.getObjectByName(Y_AXIS) const y = this.axisGroup.getObjectByName(Y_AXIS)
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1) y?.scale.set(factor, 1, 1)
} }
} }
createIntersectionPlane() { createIntersectionPlane() {
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) { const planeGeometry = new PlaneGeometry(100000, 100000)
console.warn('createIntersectionPlane called when it already exists')
return
}
const hundredM = 1000000
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
const planeMaterial = new MeshBasicMaterial({ const planeMaterial = new MeshBasicMaterial({
color: 0xff0000, color: 0xff0000,
side: DoubleSide, side: DoubleSide,
@ -179,7 +164,6 @@ class SceneEntities {
this.scene.add(this.intersectionPlane) this.scene.add(this.intersectionPlane)
} }
createSketchAxis(sketchPathToNode: PathToNode) { createSketchAxis(sketchPathToNode: PathToNode) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055 const baseXColor = 0x000055
const baseYColor = 0x550000 const baseYColor = 0x550000
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01) const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
@ -211,22 +195,13 @@ class SceneEntities {
this.axisGroup = new Group() this.axisGroup = new Group()
const gridHelper = createGridHelper({ size: 100, divisions: 10 }) const gridHelper = createGridHelper({ size: 100, divisions: 10 })
gridHelper.position.z = -0.01
gridHelper.renderOrder = -3 // is this working? gridHelper.renderOrder = -3 // is this working?
gridHelper.name = 'gridHelper' gridHelper.name = 'gridHelper'
const sceneScale = getSceneScale( const sceneScale = getSceneScale(
sceneInfra.camControls.camera, sceneInfra.camera,
sceneInfra.camControls.target sceneInfra.controls.target
) )
gridHelper.scale.set(sceneScale, sceneScale, sceneScale) gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
const factor =
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
xAxisMesh?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
yAxisMesh?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper) this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
this.currentSketchQuaternion && this.currentSketchQuaternion &&
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion) this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
@ -265,6 +240,15 @@ class SceneEntities {
}) { }) {
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
this.createIntersectionPlane() this.createIntersectionPlane()
const distance = sceneInfra.controls.target.distanceTo(
sceneInfra.camera.position
)
// TODO this should probably be distance to the sketch group, more important after sketch on face
// since sketches won't always so close to the origin
// is this the best place to adjust camera far?
if (sceneInfra.camera.far < distance * 1.5) {
sceneInfra.camera.far = distance * 2
}
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
this.prepareTruncatedMemoryAndAst( this.prepareTruncatedMemoryAndAst(
@ -296,57 +280,19 @@ class SceneEntities {
sketchGroup.position[1], sketchGroup.position[1],
sketchGroup.position[2] sketchGroup.position[2]
) )
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camera)
const factor = const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera sceneInfra.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, dummy)) / : perspScale(sceneInfra.camera, dummy)
sceneInfra._baseUnitMultiplier
const segPathToNode = getNodePathFromSourceRange(
kclManager.ast,
sketchGroup.start.__geoMeta.sourceRange
)
const _profileStart = profileStart({
from: sketchGroup.start.from,
id: sketchGroup.start.__geoMeta.id,
pathToNode: segPathToNode,
scale: factor,
})
_profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => {
child.layers.set(SKETCH_LAYER)
})
group.add(_profileStart)
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
sketchGroup.value.forEach((segment, index) => { sketchGroup.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange( let segPathToNode = getNodePathFromSourceRange(
kclManager.ast, draftSegment ? truncatedAst : kclManager.ast,
segment.__geoMeta.sourceRange segment.__geoMeta.sourceRange
) )
if (draftSegment && (sketchGroup.value[index - 1] || sketchGroup.start)) {
const previousSegment =
sketchGroup.value[index - 1] || sketchGroup.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
kclManager.ast,
previousSegment.__geoMeta.sourceRange
)
const bodyIndex = previousSegmentPathToNode[1][0]
segPathToNode = getNodePathFromSourceRange(
truncatedAst,
segment.__geoMeta.sourceRange
)
segPathToNode[1][0] = bodyIndex
}
const isDraftSegment = const isDraftSegment =
draftSegment && index === sketchGroup.value.length - 1 draftSegment && index === sketchGroup.value.length - 1
let seg let seg
const callExpName = getNodeFromPath<CallExpression>(
kclManager.ast,
segPathToNode,
'CallExpression'
)?.node?.callee?.name
if (segment.type === 'TangentialArcTo') { if (segment.type === 'TangentialArcTo') {
seg = tangentialArcToSegment({ seg = tangentialArcToSegment({
prevSegment: sketchGroup.value[index - 1], prevSegment: sketchGroup.value[index - 1],
@ -365,7 +311,6 @@ class SceneEntities {
pathToNode: segPathToNode, pathToNode: segPathToNode,
isDraftSegment, isDraftSegment,
scale: factor, scale: factor,
callExpName,
}) })
} }
seg.layers.set(SKETCH_LAYER) seg.layers.set(SKETCH_LAYER)
@ -387,19 +332,17 @@ class SceneEntities {
this.scene.add(group) this.scene.add(group)
if (!draftSegment) { if (!draftSegment) {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => { onDrag: (args) => {
if (mouseEvent.which !== 1) return if (args.event.which !== 1) return
this.onDragSegment({ this.onDragSegment({
object: selected, ...args,
intersection2d: intersectionPoint.twoD,
intersects,
sketchPathToNode, sketchPathToNode,
}) })
}, },
onMove: () => {}, onMove: () => {},
onClick: (args) => { onClick: (args) => {
if (args?.mouseEvent.which !== 1) return if (args?.event.which !== 1) return
if (!args || !args.selected) { if (!args || !args.object) {
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Set selection', type: 'Set selection',
data: { data: {
@ -408,32 +351,73 @@ class SceneEntities {
}) })
return return
} }
const { selected } = args const { object } = args
const event = getEventForSegmentSelection(selected) const event = getEventForSegmentSelection(object)
if (!event) return if (!event) return
sceneInfra.modelingSend(event) sceneInfra.modelingSend(event)
}, },
...mouseEnterLeaveCallbacks(), onMouseEnter: ({ object }) => {
// TODO change the color of the segment to yellow?
// Give a few pixels grace around each of the segments
// for hover.
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
const obj = object as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(object)
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(object, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ object }) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(object)
const isSelected = parent?.userData?.isSelected
colorSegment(object, isSelected ? 0x0000ff : 0xffffff)
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
const obj = object as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}) })
} else { } else {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: () => {},
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.mouseEvent.which !== 1) return if (args.event.which !== 1) return
const { intersectionPoint } = args const { intersection2d } = args
let intersection2d = intersectionPoint?.twoD if (!intersection2d) return
const profileStart = args.intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const firstSeg = sketchGroup.value[0]
const isClosingSketch = compareVec2Epsilon2(
firstSeg.from,
[intersection2d.x, intersection2d.y],
1
)
let modifiedAst let modifiedAst
if (profileStart) { if (isClosingSketch) {
// TODO close needs a better UX
modifiedAst = addCloseToPipe({ modifiedAst = addCloseToPipe({
node: kclManager.ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}) })
} else if (intersection2d) { } else {
const lastSegment = sketchGroup.value.slice(-1)[0] const lastSegment = sketchGroup.value.slice(-1)[0]
modifiedAst = addNewSketchLn({ modifiedAst = addNewSketchLn({
node: kclManager.ast, node: kclManager.ast,
@ -446,9 +430,6 @@ class SceneEntities {
: 'line', : 'line',
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}).modifiedAst }).modifiedAst
} else {
// return early as we didn't modify the ast
return
} }
kclManager.executeAstMock(modifiedAst, { updates: 'code' }) kclManager.executeAstMock(modifiedAst, { updates: 'code' })
@ -457,9 +438,8 @@ class SceneEntities {
}, },
onMove: (args) => { onMove: (args) => {
this.onDragSegment({ this.onDragSegment({
intersection2d: args.intersectionPoint.twoD, ...args,
object: Object.values(this.activeSegments).slice(-1)[0], object: Object.values(this.activeSegments).slice(-1)[0],
intersects: args.intersects,
sketchPathToNode, sketchPathToNode,
draftInfo: { draftInfo: {
draftSegment, draftSegment,
@ -469,10 +449,9 @@ class SceneEntities {
}, },
}) })
}, },
...mouseEnterLeaveCallbacks(),
}) })
} }
sceneInfra.camControls.enableRotate = false sceneInfra.controls.enableRotate = false
} }
updateAstAndRejigSketch = async ( updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
@ -506,15 +485,17 @@ class SceneEntities {
) )
onDragSegment({ onDragSegment({
object, object,
intersection2d: _intersection2d, event,
intersectPoint,
intersection2d,
sketchPathToNode, sketchPathToNode,
draftInfo, draftInfo,
intersects,
}: { }: {
object: any object: any
event: any
intersectPoint: Vector3
intersection2d: Vector2 intersection2d: Vector2
sketchPathToNode: PathToNode sketchPathToNode: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: { draftInfo?: {
draftSegment: DraftSegment draftSegment: DraftSegment
truncatedAst: Program truncatedAst: Program
@ -522,20 +503,7 @@ class SceneEntities {
variableDeclarationName: string variableDeclarationName: string
} }
}) { }) {
const profileStart = const group = getParentGroup(object)
draftInfo &&
intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const intersection2d = profileStart
? new Vector2(profileStart.position.x, profileStart.position.y)
: _intersection2d
const group = getParentGroup(object, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (!group) return if (!group) return
const pathToNode: PathToNode = JSON.parse( const pathToNode: PathToNode = JSON.parse(
JSON.stringify(group.userData.pathToNode) JSON.stringify(group.userData.pathToNode)
@ -559,28 +527,13 @@ class SceneEntities {
).node ).node
if (node.type !== 'CallExpression') return if (node.type !== 'CallExpression') return
let modded: { const modded = changeSketchArguments(
modifiedAst: Program
pathToNode: PathToNode
}
if (group.name === PROFILE_START) {
modded = updateStartProfileAtArgs({
node: modifiedAst,
pathToNode,
to,
from,
previousProgramMemory: kclManager.programMemory,
})
} else {
modded = changeSketchArguments(
modifiedAst, modifiedAst,
kclManager.programMemory, kclManager.programMemory,
[node.start, node.end], [node.start, node.end],
to, to,
from from
) )
}
modifiedAst = modded.modifiedAst modifiedAst = modded.modifiedAst
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
draftInfo draftInfo
@ -599,16 +552,10 @@ class SceneEntities {
programMemoryOverride, programMemoryOverride,
}) })
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[ const sketchGroup = programMemory.root[variableDeclarationName]
variableDeclarationName .value as Path[]
] as SketchGroup const orthoFactor = orthoScale(sceneInfra.camera)
const sgPaths = sketchGroup.value sketchGroup.forEach((segment, index) => {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const updateSegment = (
segment: Path | SketchGroup['start'],
index: number
) => {
const segPathToNode = getNodePathFromSourceRange( const segPathToNode = getNodePathFromSourceRange(
modifiedAst, modifiedAst,
segment.__geoMeta.sourceRange segment.__geoMeta.sourceRange
@ -623,13 +570,12 @@ class SceneEntities {
// const prevSegment = sketchGroup.slice(index - 1)[0] // const prevSegment = sketchGroup.slice(index - 1)[0]
const type = group?.userData?.type const type = group?.userData?.type
const factor = const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera sceneInfra.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, group)) / : perspScale(sceneInfra.camera, group)
sceneInfra._baseUnitMultiplier
if (type === TANGENTIAL_ARC_TO_SEGMENT) { if (type === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({ this.updateTangentialArcToSegment({
prevSegment: sgPaths[index - 1], prevSegment: sketchGroup[index - 1],
from: segment.from, from: segment.from,
to: segment.to, to: segment.to,
group: group, group: group,
@ -642,13 +588,8 @@ class SceneEntities {
group: group, group: group,
scale: factor, scale: factor,
}) })
} else if (type === PROFILE_START) {
group.position.set(segment.from[0], segment.from[1], 0)
group.scale.set(factor, factor, factor)
} }
} })
updateSegment(sketchGroup.start, 0)
sgPaths.forEach(updateSegment)
})() })()
} }
@ -668,7 +609,9 @@ class SceneEntities {
group.userData.from = from group.userData.from = from
group.userData.to = to group.userData.to = to
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.children.find(
(child) => child.userData.type === ARROWHEAD
) as Group
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
@ -743,9 +686,10 @@ class SceneEntities {
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, -0.08 * scale) shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line shape.lineTo(0, 0.08 * scale) // The width of the line
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.children.find(
(child) => child.userData.type === ARROWHEAD
) as Group
if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3() const dir = new Vector3()
@ -756,7 +700,6 @@ class SceneEntities {
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale) arrowGroup.scale.set(scale, scale, scale)
}
const straightSegmentBody = group.children.find( const straightSegmentBody = group.children.find(
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY (child) => child.userData.type === STRAIGHT_SEGMENT_BODY
@ -786,10 +729,10 @@ class SceneEntities {
} }
async animateAfterSketch() { async animateAfterSketch() {
if (isReducedMotion()) { if (isReducedMotion()) {
sceneInfra.camControls.usePerspectiveCamera() sceneInfra.usePerspectiveCamera()
return } else {
await sceneInfra.animateToPerspective()
} }
await sceneInfra.camControls.animateToPerspective()
} }
removeSketchGrid() { removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup) if (this.axisGroup) this.scene.remove(this.axisGroup)
@ -821,7 +764,7 @@ class SceneEntities {
reject() reject()
} }
} }
sceneInfra.camControls.enableRotate = true sceneInfra.controls.enableRotate = true
this.activeSegments = {} this.activeSegments = {}
// maybe should reset onMove etc handlers // maybe should reset onMove etc handlers
if (shouldResolve) resolve(true) if (shouldResolve) resolve(true)
@ -841,24 +784,23 @@ class SceneEntities {
} }
setupDefaultPlaneHover() { setupDefaultPlaneHover() {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onMouseEnter: ({ selected }) => { onMouseEnter: ({ object }) => {
if (!(selected instanceof Mesh && selected.parent)) return if (object.parent.userData.type !== DEFAULT_PLANES) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return const type: DefaultPlane = object.userData.type
const type: DefaultPlane = selected.userData.type object.material.color = defaultPlaneColor(type, 0.5, 1)
selected.material.color = defaultPlaneColor(type, 0.5, 1)
}, },
onMouseLeave: ({ selected }) => { onMouseLeave: ({ object }) => {
if (!(selected instanceof Mesh && selected.parent)) return if (object.parent.userData.type !== DEFAULT_PLANES) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return const type: DefaultPlane = object.userData.type
const type: DefaultPlane = selected.userData.type object.material.color = defaultPlaneColor(type)
selected.material.color = defaultPlaneColor(type)
}, },
onClick: (args) => { onClick: (args) => {
if (!args || !args.intersects?.[0]) return if (!args || !args.object) return
if (args.mouseEvent.which !== 1) return if (args.event.which !== 1) return
const { intersects } = args const { object, intersection } = args
const type = intersects?.[0].object.name || '' const type = object?.userData?.type || ''
const posNorm = Number(intersects?.[0]?.normal?.z) > 0 console.log('intersection.normal?.z', intersection)
const posNorm = Number(intersection.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY' let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1] let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
if (type === YZ_PLANE) { if (type === YZ_PLANE) {
@ -1030,9 +972,9 @@ export function quaternionFromSketchGroup(
} }
function colorSegment(object: any, color: number) { function colorSegment(object: any, color: number) {
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START]) const arrowHead = getParentGroup(object, [ARROWHEAD])
if (segmentHead) { if (arrowHead) {
segmentHead.traverse((child) => { arrowHead.traverse((child) => {
if (child instanceof Mesh) { if (child instanceof Mesh) {
child.material.color.set(color) child.material.color.set(color)
} }
@ -1088,53 +1030,3 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2]) ? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z) : new Vector3(a.x, a.y, a.z)
} }
function mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}

View File

@ -1,5 +1,5 @@
import { Quaternion } from 'three' import { Quaternion } from 'three'
import { isQuaternionVertical } from './helpers' import { isQuaternionVertical } from './sceneInfra'
describe('isQuaternionVertical', () => { describe('isQuaternionVertical', () => {
it('should identify vertical quaternions', () => { it('should identify vertical quaternions', () => {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
import { Coords2d } from 'lang/std/sketch' import { Coords2d } from 'lang/std/sketch'
import { import {
BoxGeometry,
BufferGeometry, BufferGeometry,
CatmullRomCurve3, CatmullRomCurve3,
ConeGeometry, ConeGeometry,
@ -20,7 +19,6 @@ import {
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import { import {
PROFILE_START,
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY, STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH, STRAIGHT_SEGMENT_DASH,
@ -31,38 +29,6 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './sceneInfra' import { ARROWHEAD } from './sceneInfra'
export function profileStart({
from,
id,
pathToNode,
scale = 1,
}: {
from: Coords2d
id: string
pathToNode: PathToNode
scale?: number
}) {
const group = new Group()
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
group.add(mesh)
group.userData = {
type: PROFILE_START,
id,
from,
pathToNode,
isSelected: false,
}
group.name = PROFILE_START
group.position.set(from[0], from[1], 0)
group.scale.set(scale, scale, scale)
return group
}
export function straightSegment({ export function straightSegment({
from, from,
to, to,
@ -70,7 +36,6 @@ export function straightSegment({
pathToNode, pathToNode,
isDraftSegment, isDraftSegment,
scale = 1, scale = 1,
callExpName,
}: { }: {
from: Coords2d from: Coords2d
to: Coords2d to: Coords2d
@ -78,7 +43,6 @@ export function straightSegment({
pathToNode: PathToNode pathToNode: PathToNode
isDraftSegment?: boolean isDraftSegment?: boolean
scale?: number scale?: number
callExpName: string
}): Group { }): Group {
const group = new Group() const group = new Group()
@ -102,8 +66,7 @@ export function straightSegment({
}) })
} }
const baseColor = callExpName === 'close' ? 0x444444 : 0xffffff const body = new MeshBasicMaterial({ color: 0xffffff })
const body = new MeshBasicMaterial({ color: baseColor })
const mesh = new Mesh(geometry, body) const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment mesh.userData.type = isDraftSegment
? STRAIGHT_SEGMENT_DASH ? STRAIGHT_SEGMENT_DASH
@ -117,10 +80,7 @@ export function straightSegment({
to, to,
pathToNode, pathToNode,
isSelected: false, isSelected: false,
callExpName,
baseColor,
} }
group.name = STRAIGHT_SEGMENT
const arrowGroup = createArrowhead(scale) const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
@ -129,8 +89,7 @@ export function straightSegment({
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
group.add(mesh) group.add(mesh, arrowGroup)
if (callExpName !== 'close') group.add(arrowGroup)
return group return group
} }
@ -210,7 +169,6 @@ export function tangentialArcToSegment({
pathToNode, pathToNode,
isSelected: false, isSelected: false,
} }
group.name = TANGENTIAL_ARC_TO_SEGMENT
const arrowGroup = createArrowhead(scale) const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)

View File

@ -87,7 +87,7 @@ export function useCalc({
inputRef: React.RefObject<HTMLInputElement> inputRef: React.RefObject<HTMLInputElement>
valueNode: Value | null valueNode: Value | null
calcResult: string calcResult: string
prevVariables: PrevVariable<unknown>[] prevVariables: PrevVariable<any>[]
newVariableName: string newVariableName: string
isNewVariableNameUnique: boolean isNewVariableNameUnique: boolean
newVariableInsertIndex: number newVariableInsertIndex: number

View File

@ -4,7 +4,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils' import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle( const updateDollyZoom = throttle(
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov), (newFov: number) => sceneInfra.dollyZoom(newFov),
1000 / 15 1000 / 15
) )
@ -15,19 +15,19 @@ export const CamToggle = () => {
useEffect(() => { useEffect(() => {
engineCommandManager.waitForReady.then(async () => { engineCommandManager.waitForReady.then(async () => {
sceneInfra.camControls.dollyZoom(fov) sceneInfra.dollyZoom(fov)
}) })
}, []) }, [])
const toggleCamera = () => { const toggleCamera = () => {
if (isPerspective) { if (isPerspective) {
isReducedMotion() isReducedMotion()
? sceneInfra.camControls.useOrthographicCamera() ? sceneInfra.useOrthographicCamera()
: sceneInfra.camControls.animateToOrthographic() : sceneInfra.animateToOrthographic()
} else { } else {
isReducedMotion() isReducedMotion()
? sceneInfra.camControls.usePerspectiveCamera() ? sceneInfra.usePerspectiveCamera()
: sceneInfra.camControls.animateToPerspective() : sceneInfra.animateToPerspective()
} }
setIsPerspective(!isPerspective) setIsPerspective(!isPerspective)
} }
@ -60,9 +60,9 @@ export const CamToggle = () => {
<button <button
onClick={() => { onClick={() => {
if (enableRotate) { if (enableRotate) {
sceneInfra.camControls.enableRotate = false sceneInfra.controls.enableRotate = false
} else { } else {
sceneInfra.camControls.enableRotate = true sceneInfra.controls.enableRotate = true
} }
setEnableRotate(!enableRotate) setEnableRotate(!enableRotate)
}} }}

View File

@ -1,8 +1,8 @@
import { Combobox } from '@headlessui/react' import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes' import { CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
function CommandArgOptionInput({ function CommandArgOptionInput({
options, options,
@ -11,89 +11,51 @@ function CommandArgOptionInput({
onSubmit, onSubmit,
placeholder, placeholder,
}: { }: {
options: (CommandArgument<unknown> & { inputType: 'options' })['options'] options: CommandArgumentOption<unknown>[]
argName: string argName: string
stepBack: () => void stepBack: () => void
onSubmit: (data: unknown) => void onSubmit: (data: unknown) => void
placeholder?: string placeholder?: string
}) { }) {
const { commandBarSend, commandBarState } = useCommandsContext() const { commandBarSend, commandBarState } = useCommandsContext()
const resolvedOptions = useMemo(
() =>
typeof options === 'function'
? options(commandBarState.context)
: options,
[argName, options, commandBarState.context]
)
// The initial current option is either an already-input value or the configured default
const currentOption = useMemo(
() =>
resolvedOptions.find(
(o) => o.value === commandBarState.context.argumentsToSubmit[argName]
) || resolvedOptions.find((o) => o.isCurrent),
[commandBarState.context.argumentsToSubmit, argName, resolvedOptions]
)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const formRef = useRef<HTMLFormElement>(null) const formRef = useRef<HTMLFormElement>(null)
const [selectedOption, setSelectedOption] = useState< const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
CommandArgumentOption<unknown> options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
>(currentOption || resolvedOptions[0]) commandBarState.context.argumentsToSubmit[argName] ||
const initialQuery = useMemo(() => '', [options, argName]) options[0].value
const [query, setQuery] = useState(initialQuery) )
const [filteredOptions, setFilteredOptions] = const [query, setQuery] = useState('')
useState<typeof resolvedOptions>() const [filteredOptions, setFilteredOptions] = useState<typeof options>()
// Create a new Fuse instance when the options change const fuse = new Fuse(options, {
const fuse = useMemo(
() =>
new Fuse(resolvedOptions, {
keys: ['name', 'description'], keys: ['name', 'description'],
threshold: 0.3, threshold: 0.3,
}), })
[argName, resolvedOptions]
)
// Reset the query and selected option when the argName changes
useEffect(() => {
setQuery(initialQuery)
setSelectedOption(currentOption || resolvedOptions[0])
}, [argName])
// Auto focus and select the input when the component mounts
useEffect(() => { useEffect(() => {
inputRef.current?.focus() inputRef.current?.focus()
inputRef.current?.select() inputRef.current?.select()
}, [inputRef]) }, [inputRef])
// Filter the options based on the query,
// resetting the query when the options change
useEffect(() => { useEffect(() => {
const results = fuse.search(query).map((result) => result.item) const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : resolvedOptions) setFilteredOptions(query.length > 0 ? results : options)
}, [query, resolvedOptions, fuse]) }, [query])
function handleSelectOption(option: CommandArgumentOption<unknown>) { function handleSelectOption(option: CommandArgumentOption<unknown>) {
// We deal with the whole option object internally setArgValue(option)
setSelectedOption(option)
// But we only submit the value
onSubmit(option.value) onSubmit(option.value)
} }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault() e.preventDefault()
onSubmit(argValue)
// We submit the value of the selected option, not the whole object
onSubmit(selectedOption.value)
} }
return ( return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}> <form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
<Combobox <Combobox value={argValue} onChange={handleSelectOption} name="options">
value={selectedOption}
onChange={handleSelectOption}
name="options"
>
<div className="flex items-center mx-4 mt-4 mb-2"> <div className="flex items-center mx-4 mt-4 mb-2">
<label <label
htmlFor="option-input" htmlFor="option-input"
@ -113,12 +75,10 @@ function CommandArgOptionInput({
stepBack() stepBack()
} }
}} }}
value={query}
placeholder={ placeholder={
currentOption?.name || (argValue as CommandArgumentOption<unknown>)?.name ||
placeholder || placeholder ||
argName || 'Select an option for ' + argName
'Select an option'
} }
autoCapitalize="off" autoCapitalize="off"
autoComplete="off" autoComplete="off"
@ -138,7 +98,7 @@ function CommandArgOptionInput({
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90" className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
> >
<p className="flex-grow">{option.name} </p> <p className="flex-grow">{option.name} </p>
{option.value === currentOption?.value && ( {'isCurrent' in option && option.isCurrent && (
<small className="text-chalkboard-70 dark:text-chalkboard-50"> <small className="text-chalkboard-70 dark:text-chalkboard-50">
current current
</small> </small>

View File

@ -29,6 +29,12 @@ export const CommandBarProvider = ({
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, { const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
devTools: true, devTools: true,
guards: { guards: {
'Arguments are ready': (context, _) => {
return context.selectedCommand?.args
? context.argumentsToSubmit.length ===
Object.keys(context.selectedCommand.args)?.length
: false
},
'Command has no arguments': (context, _event) => { 'Command has no arguments': (context, _event) => {
return ( return (
!context.selectedCommand?.args || !context.selectedCommand?.args ||
@ -51,11 +57,12 @@ export const CommandBarProvider = ({
}} }}
> >
{children} {children}
<CommandBar />
</CommandsContext.Provider> </CommandsContext.Provider>
) )
} }
export const CommandBar = () => { const CommandBar = () => {
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { const {
context: { selectedCommand, currentArgument, commands }, context: { selectedCommand, currentArgument, commands },
@ -75,23 +82,17 @@ export const CommandBar = () => {
function stepBack() { function stepBack() {
if (!currentArgument) { if (!currentArgument) {
if (commandBarState.matches('Review')) { if (commandBarState.matches('Review')) {
const entries = Object.entries(selectedCommand?.args || {}).filter( const entries = Object.entries(selectedCommand?.args || {})
([_, argConfig]) =>
typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required
)
const currentArgName = entries[entries.length - 1][0]
const currentArg = {
name: currentArgName,
...entries[entries.length - 1][1],
}
commandBarSend({ commandBarSend({
type: 'Edit argument', type: commandBarState.matches('Review')
? 'Edit argument'
: 'Change current argument',
data: { data: {
arg: currentArg, arg: {
name: entries[entries.length - 1][0],
...entries[entries.length - 1][1],
},
}, },
}) })
} else { } else {

View File

@ -4,7 +4,6 @@ import CommandBarSelectionInput from './CommandBarSelectionInput'
import { CommandArgument } from 'lib/commandTypes' import { CommandArgument } from 'lib/commandTypes'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader' import CommandBarHeader from './CommandBarHeader'
import CommandBarKclInput from './CommandBarKclInput'
function CommandBarArgument({ stepBack }: { stepBack: () => void }) { function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
@ -18,7 +17,10 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
commandBarSend({ commandBarSend({
type: 'Submit argument', type: 'Submit argument',
data: { data: {
[currentArgument.name]: data, [currentArgument.name]:
currentArgument.inputType === 'number'
? parseFloat((data as string) || '0')
: data,
}, },
}) })
} }
@ -66,10 +68,6 @@ function ArgumentInput({
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
) )
case 'kcl':
return (
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
)
default: default:
return ( return (
<CommandBarBasicInput <CommandBarBasicInput

View File

@ -9,7 +9,7 @@ function CommandBarBasicInput({
onSubmit, onSubmit,
}: { }: {
arg: CommandArgument<unknown> & { arg: CommandArgument<unknown> & {
inputType: 'string' inputType: 'number' | 'string'
name: string name: string
} }
stepBack: () => void stepBack: () => void
@ -18,6 +18,7 @@ function CommandBarBasicInput({
const { commandBarSend, commandBarState } = useCommandsContext() const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const inputType = arg.inputType === 'number' ? 'number' : 'text'
useEffect(() => { useEffect(() => {
if (inputRef.current) { if (inputRef.current) {
@ -39,9 +40,9 @@ function CommandBarBasicInput({
</span> </span>
<input <input
id="arg-form" id="arg-form"
name={arg.inputType} name={inputType}
ref={inputRef} ref={inputRef}
type={arg.inputType} type={inputType}
required required
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none" className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
placeholder="Enter a value" placeholder="Enter a value"

View File

@ -4,9 +4,6 @@ import React, { ReactNode, useState } from 'react'
import { ActionButton } from '../ActionButton' import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections' import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
import Tooltip from 'components/Tooltip'
import { roundOff } from 'lib/utils'
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
@ -48,7 +45,6 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
parseInt(b.keys[0], 10) - 1 parseInt(b.keys[0], 10) - 1
] ]
const arg = selectedCommand?.args[argName] const arg = selectedCommand?.args[argName]
if (!argName || !arg) return
commandBarSend({ commandBarSend({
type: 'Change current argument', type: 'Change current argument',
data: { arg: { ...arg, name: argName } }, data: { arg: { ...arg, name: argName } },
@ -63,7 +59,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
selectedCommand && selectedCommand &&
argumentsToSubmit && ( argumentsToSubmit && (
<> <>
<div className="group px-4 text-sm flex gap-4 items-start"> <div className="px-4 text-sm flex gap-4 items-start">
<div className="flex flex-1 flex-wrap gap-2"> <div className="flex flex-1 flex-wrap gap-2">
<p <p
data-command-name={selectedCommand?.name} data-command-name={selectedCommand?.name}
@ -76,21 +72,8 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
)} )}
{selectedCommand?.name} {selectedCommand?.name}
</p> </p>
{Object.entries(selectedCommand?.args || {}) {Object.entries(selectedCommand?.args || {}).map(
.filter(([_, argConfig]) => ([argName, arg], i) => (
typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required
)
.map(([argName, arg], i) => {
const argValue =
(typeof argumentsToSubmit[argName] === 'function'
? (argumentsToSubmit[argName] as Function)(
commandBarState.context
)
: argumentsToSubmit[argName]) || ''
return (
<button <button
disabled={!isReviewing && currentArgument?.name === argName} disabled={!isReviewing && currentArgument?.name === argName}
onClick={() => { onClick={() => {
@ -108,50 +91,28 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80' : 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
}`} }`}
> >
<span className="capitalize">{argName}</span> {argumentsToSubmit[argName] ? (
{argValue ? (
arg.inputType === 'selection' ? ( arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections) getSelectionTypeDisplayText(
) : arg.inputType === 'kcl' ? ( argumentsToSubmit[argName] as Selections
roundOff(
Number((argValue as KclCommandValue).valueCalculated),
4
) )
) : typeof argValue === 'object' ? ( ) : typeof argumentsToSubmit[argName] === 'object' ? (
JSON.stringify(argValue) JSON.stringify(argumentsToSubmit[argName])
) : ( ) : (
<em>{argValue}</em> <em>{argumentsToSubmit[argName] as ReactNode}</em>
) )
) : null} ) : (
<em>{argName}</em>
)}
{showShortcuts && ( {showShortcuts && (
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100"> <small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
<span className="sr-only">Hotkey: </span> <span className="sr-only">Hotkey: </span>
{i + 1} {i + 1}
</small> </small>
)} )}
{arg.inputType === 'kcl' &&
!!argValue &&
'variableName' in (argValue as KclCommandValue) && (
<>
<CustomIcon
name="make-variable"
className="w-4 h-4"
/>
<Tooltip position="blockEnd">
New variable:{' '}
{
(
argumentsToSubmit[
argName
] as KclExpressionWithVariable
).variableName
}
</Tooltip>
</>
)}
</button> </button>
) )
})} )}
</div> </div>
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />} {isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
</div> </div>

View File

@ -1,17 +0,0 @@
.editor {
@apply text-base flex-1;
}
.editor :global(.cm-editor) {
@apply bg-transparent;
}
.editor :global(.cm-line)::selection {
@apply px-1;
@apply text-chalkboard-100;
@apply bg-energy-10/50;
}
:global(.dark) .editor :global(.cm-line)::selection {
@apply text-energy-10;
@apply bg-energy-10/20;
}

View File

@ -1,221 +0,0 @@
import { Completion } from '@codemirror/autocomplete'
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
import { CustomIcon } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
import { getSystemTheme } from 'lib/theme'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
import { roundOff } from 'lib/utils'
import { varMentions } from 'lib/varCompletionExtension'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
function CommandBarKclInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & {
inputType: 'kcl'
name: string
}
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
const previouslySetValue = commandBarState.context.argumentsToSubmit[
arg.name
] as KclCommandValue | undefined
const { settings } = useGlobalStateContext()
const defaultValue = (arg.defaultValue as string) || ''
const [value, setValue] = useState(
previouslySetValue?.valueText || defaultValue || ''
)
const [createNewVariable, setCreateNewVariable] = useState(
previouslySetValue && 'variableName' in previouslySetValue
)
const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null)
const {
prevVariables,
calcResult,
newVariableInsertIndex,
valueNode,
newVariableName,
setNewVariableName,
isNewVariableNameUnique,
} = useCalculateKclExpression({
value,
initialVariableName:
previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name,
})
const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key,
detail: String(roundOff(v.value as number)),
}))
const { setContainer } = useCodeMirror({
container: editorRef.current,
value,
indentWithTab: false,
basicSetup: false,
autoFocus: true,
selection: {
anchor: 0,
head:
previouslySetValue && 'valueText' in previouslySetValue
? previouslySetValue.valueText.length
: defaultValue.length,
},
accessKey: 'command-bar',
theme:
settings.context.theme === 'system'
? getSystemTheme()
: settings.context.theme,
extensions: [
EditorView.domEventHandlers({
keydown: (event) => {
if (event.key === 'Backspace' && value === '') {
event.preventDefault()
stepBack()
}
},
}),
varMentions(varMentionData),
EditorState.transactionFilter.of((tr) => {
if (tr.newDoc.lines > 1) {
handleSubmit()
return []
}
return tr
}),
],
onChange: (newValue) => setValue(newValue),
})
useEffect(() => {
if (editorRef.current) {
setContainer(editorRef.current)
}
}, [arg, editorRef])
useEffect(() => {
setCanSubmit(
calcResult !== 'NAN' && (!createNewVariable || isNewVariableNameUnique)
)
}, [calcResult, createNewVariable, isNewVariableNameUnique])
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
if (!canSubmit || valueNode === null) return
onSubmit(
createNewVariable
? ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
variableName: newVariableName,
insertIndex: newVariableInsertIndex,
variableIdentifierAst: createIdentifier(newVariableName),
variableDeclarationAst: createVariableDeclaration(
newVariableName,
valueNode
),
} satisfies KclCommandValue)
: ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
} satisfies KclCommandValue)
)
}
return (
<form id="arg-form" onSubmit={handleSubmit} data-can-submit={canSubmit}>
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
<span className="capitalize text-chalkboard-80 dark:text-chalkboard-20">
{arg.name}
</span>
<div ref={editorRef} className={styles.editor} />
<CustomIcon
name="equal"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
<span
className={
calcResult === 'NAN'
? 'text-destroy-80 dark:text-destroy-40'
: 'text-energy-60 dark:text-energy-20'
}
>
{calcResult === 'NAN'
? "Can't calculate"
: roundOff(Number(calcResult), 4)}
</span>
</label>
{createNewVariable ? (
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
<label
htmlFor="variable-name"
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
>
Variable name
</label>
<input
type="text"
id="variable-name"
name="variable-name"
className="flex-1 border-none bg-transparent"
placeholder="Variable name"
value={newVariableName}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
autoFocus
onChange={(e) => setNewVariableName(e.target.value)}
onKeyDown={(e) => {
if (e.currentTarget.value === '' && e.key === 'Backspace') {
setCreateNewVariable(false)
}
}}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSubmit()
}
}}
/>
<span
className={
isNewVariableNameUnique
? 'text-energy-60 dark:text-energy-20'
: 'text-destroy-60 dark:text-destroy-40'
}
>
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
</span>
</div>
) : (
<div className="flex justify-between gap-2 px-4">
<button
onClick={() => setCreateNewVariable(true)}
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
>
<CustomIcon name="plus" className="w-5 h-5" />
Create new variable
</button>
</div>
)}
</form>
)
}
export default CommandBarKclInput

View File

@ -14,18 +14,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
}) })
useHotkeys( useHotkeys(
[ '1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
'alt+1',
'alt+2',
'alt+3',
'alt+4',
'alt+5',
'alt+6',
'alt+7',
'alt+8',
'alt+9',
'alt+0',
],
(_, b) => { (_, b) => {
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) { if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
if (!selectedCommand?.args) return if (!selectedCommand?.args) return
@ -48,8 +37,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
if (!arg) return if (!arg) return
}) })
function submitCommand(e: React.FormEvent<HTMLFormElement>) { function submitCommand() {
e.preventDefault()
commandBarSend({ commandBarSend({
type: 'Submit command', type: 'Submit command',
data: argumentsToSubmit, data: argumentsToSubmit,

View File

@ -29,7 +29,7 @@ function CommandBarSelectionInput({
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false) const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector) const selection = useSelector(arg.actor, selectionSelector)
const [selectionsByType, setSelectionsByType] = useState< const [selectionsByType, setSelectionsByType] = useState<
'none' | ResolvedSelectionType[] 'none' | ResolvedSelectionType[]
>( >(

View File

@ -9,7 +9,6 @@ export type CustomIconName =
| 'clipboardCheckmark' | 'clipboardCheckmark'
| 'close' | 'close'
| 'equal' | 'equal'
| 'exportFile'
| 'extrude' | 'extrude'
| 'file' | 'file'
| 'filePlus' | 'filePlus'
@ -19,12 +18,10 @@ export type CustomIconName =
| 'horizontal' | 'horizontal'
| 'horizontalDash' | 'horizontalDash'
| 'line' | 'line'
| 'make-variable'
| 'move' | 'move'
| 'network' | 'network'
| 'networkCrossedOut' | 'networkCrossedOut'
| 'parallel' | 'parallel'
| 'plus'
| 'search' | 'search'
| 'settings' | 'settings'
| 'sketch' | 'sketch'
@ -195,22 +192,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'exportFile':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM16.3904 14.1877L14.3904 11.6877L13.6096 12.3124L14.9597 14H11V15H14.9597L13.6096 16.6877L14.3904 17.3124L16.3904 14.8124L16.6403 14.5L16.3904 14.1877Z"
fill="currentColor"
/>
</svg>
)
case 'extrude': case 'extrude':
return ( return (
<svg <svg
@ -355,22 +336,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'make-variable':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.07178 6.57735L9.99998 3.1547L15.9282 6.57735V13.4227L9.99998 16.8453L4.07178 13.4227V6.57735ZM9.99998 2L16.9282 6V14L9.99998 18L3.07178 14V6L9.99998 2ZM9.45068 6.854C9.20802 6.798 8.97468 6.78867 8.75068 6.826C8.39602 6.90067 8.06468 7.04533 7.75668 7.26C7.73802 7.26933 7.72402 7.27867 7.71468 7.288C7.45335 7.484 7.24802 7.694 7.09868 7.918C6.96802 8.09533 6.86068 8.282 6.77668 8.478C6.69268 8.65533 6.63668 8.814 6.60868 8.954C6.60868 9.00067 6.62268 9.038 6.65068 9.066L6.69268 9.108H6.95868C7.13602 9.108 7.23402 9.09867 7.25268 9.08C7.28068 9.052 7.30868 8.982 7.33668 8.87C7.45802 8.52467 7.65402 8.212 7.92468 7.932C8.13002 7.72667 8.36802 7.58667 8.63868 7.512C8.83468 7.456 9.02602 7.456 9.21268 7.512C9.40868 7.57733 9.53002 7.68467 9.57668 7.834C9.62335 7.96467 9.61402 8.198 9.54868 8.534L8.77868 11.614C8.65735 11.9593 8.47535 12.216 8.23268 12.384C8.10202 12.4587 7.97602 12.4913 7.85468 12.482C7.68668 12.482 7.53735 12.4307 7.40668 12.328L7.36468 12.286L7.42068 12.272C7.50468 12.244 7.57002 12.216 7.61668 12.188C7.93402 12.02 8.10668 11.7493 8.13468 11.376C8.15335 11.1053 8.05535 10.9187 7.84068 10.816C7.60735 10.6853 7.34135 10.69 7.04268 10.83C6.73468 10.9793 6.54802 11.2547 6.48268 11.656C6.45468 11.8893 6.47335 12.1087 6.53868 12.314C6.56668 12.4073 6.60868 12.4913 6.66468 12.566C6.92602 12.986 7.32268 13.182 7.85468 13.154C8.31202 13.126 8.72268 12.8787 9.08668 12.412L9.12868 12.37L9.21268 12.496C9.44602 12.8133 9.80068 13.0233 10.2767 13.126C10.5474 13.1633 10.79 13.1633 11.0047 13.126C11.6954 12.9767 12.2507 12.58 12.6707 11.936C12.6894 11.9173 12.7034 11.894 12.7127 11.866C12.9553 11.474 13.0767 11.18 13.0767 10.984C13.0767 10.9373 13.0674 10.9047 13.0487 10.886C13.0207 10.8673 12.918 10.858 12.7407 10.858C12.61 10.858 12.526 10.8627 12.4887 10.872C12.442 10.8813 12.4047 10.9327 12.3767 11.026C12.2834 11.3807 12.092 11.7073 11.8027 12.006C11.56 12.23 11.3174 12.3793 11.0747 12.454C11.0094 12.4727 10.9067 12.482 10.7667 12.482C10.6174 12.482 10.5194 12.4727 10.4727 12.454C10.314 12.398 10.1974 12.3 10.1227 12.16C10.0667 12.0573 10.062 11.8613 10.1087 11.572C10.1087 11.5347 10.132 11.4367 10.1787 11.278C10.58 9.542 10.8274 8.55733 10.9207 8.324C11.0887 7.88533 11.3127 7.61467 11.5927 7.512C11.6114 7.50267 11.63 7.498 11.6487 7.498C11.8914 7.43267 12.0967 7.47467 12.2647 7.624L12.3207 7.68L12.2087 7.722C11.8354 7.85267 11.6207 8.128 11.5647 8.548C11.5367 8.76267 11.5927 8.94 11.7327 9.08C11.77 9.11733 11.8167 9.15 11.8727 9.178C12.1714 9.32733 12.4887 9.28067 12.8247 9.038C12.9367 8.954 13.03 8.83267 13.1047 8.674C13.282 8.26333 13.2774 7.87133 13.0907 7.498C12.9787 7.26467 12.7874 7.078 12.5167 6.938C12.162 6.77933 11.8074 6.76533 11.4527 6.896C11.1447 7.01733 10.8787 7.20867 10.6547 7.47L10.5707 7.582C10.552 7.582 10.524 7.554 10.4867 7.498C10.2627 7.17133 9.91735 6.95667 9.45068 6.854Z"
fill="currentColor"
/>
</svg>
)
case 'move': case 'move':
return ( return (
<svg <svg
@ -435,22 +400,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'plus':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
fill="currentColor"
/>
</svg>
)
case 'search': case 'search':
return ( return (
<svg <svg

View File

@ -0,0 +1,238 @@
import { v4 as uuidv4 } from 'uuid'
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
import { ActionButton } from './ActionButton'
import Modal from 'react-modal'
import React from 'react'
import { useFormik } from 'formik'
import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
type StorageUnion = ExtractStorageTypes<OutputFormat>
interface ExportButtonProps extends React.PropsWithChildren {
className?: {
button?: string
icon?: string
bg?: string
}
}
export const ExportButton = ({ children, className }: ExportButtonProps) => {
const [modalIsOpen, setIsOpen] = React.useState(false)
const {
settings: {
state: {
context: { baseUnit },
},
},
} = useGlobalStateContext()
const defaultType = 'gltf'
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
const defaultStorage = 'embedded'
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
function openModal() {
setIsOpen(true)
}
function closeModal() {
setIsOpen(false)
}
// Default to gltf and embedded.
const initialValues: OutputFormat = {
type: defaultType,
storage: defaultStorage,
presentation: 'pretty',
}
const formik = useFormik({
initialValues,
onSubmit: (values: OutputFormat) => {
// Set the default coords.
if (
values.type === 'obj' ||
values.type === 'ply' ||
values.type === 'step' ||
values.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
values.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
if (
values.type === 'obj' ||
values.type === 'stl' ||
values.type === 'ply'
) {
values.units = baseUnit
}
if (
values.type === 'ply' ||
values.type === 'stl' ||
values.type === 'gltf'
) {
// Set the storage type.
values.storage = storage
}
if (values.type === 'ply' || values.type === 'stl') {
values.selection = { type: 'default_scene' }
}
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',
// By default let's leave this blank to export the whole scene.
// In the future we might want to let the user choose which entities
// in the scene to export. In that case, you'd pass the IDs thru here.
entity_ids: [],
format: values,
source_unit: baseUnit,
},
cmd_id: uuidv4(),
})
closeModal()
},
})
return (
<>
<ActionButton
onClick={openModal}
Element="button"
icon={{
icon: faFileExport,
className: 'p-1',
size: 'sm',
iconClassName: className?.icon,
bgClassName: className?.bg,
}}
className={className?.button}
>
{children || 'Export'}
</ActionButton>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Export"
overlayClassName="z-40 fixed inset-0 grid place-items-center"
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
>
<h1 className="text-2xl font-bold">Export your design</h1>
<form onSubmit={formik.handleSubmit}>
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
<label htmlFor="type" className="flex-1">
<p className="mb-2">Type</p>
<select
id="type"
name="type"
data-testid="export-type"
onChange={(e) => {
setType(e.target.value as OutputTypeKey)
if (e.target.value === 'gltf') {
// Set default to embedded.
setStorage('embedded')
} else if (e.target.value === 'ply') {
// Set default to ascii.
setStorage('ascii')
} else if (e.target.value === 'stl') {
// Set default to ascii.
setStorage('ascii')
}
formik.handleChange(e)
}}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
>
<option value="gltf">gltf</option>
<option value="obj">obj</option>
<option value="ply">ply</option>
<option value="step">step</option>
<option value="stl">stl</option>
</select>
</label>
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
<label htmlFor="storage" className="flex-1">
<p className="mb-2">Storage</p>
<select
id="storage"
name="storage"
data-testid="export-storage"
onChange={(e) => {
setStorage(e.target.value as StorageUnion)
formik.handleChange(e)
}}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
>
{type === 'gltf' && (
<>
<option value="embedded">embedded</option>
<option value="binary">binary</option>
<option value="standard">standard</option>
</>
)}
{type === 'stl' && (
<>
<option value="ascii">ascii</option>
<option value="binary">binary</option>
</>
)}
{type === 'ply' && (
<>
<option value="ascii">ascii</option>
<option value="binary_little_endian">
binary_little_endian
</option>
<option value="binary_big_endian">
binary_big_endian
</option>
</>
)}
</select>
</label>
)}
</div>
<div className="flex justify-between mt-6">
<ActionButton
Element="button"
onClick={closeModal}
icon={{
icon: faXmark,
className: 'p-1',
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="hover:border-destroy-40"
>
Close
</ActionButton>
<ActionButton
Element="button"
type="submit"
icon={{ icon: faFileExport, className: 'p-1' }}
>
Export
</ActionButton>
</div>
</form>
</Modal>
</>
)
}

View File

@ -18,7 +18,6 @@ import {
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig' import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { sceneInfra } from 'clientSideScene/sceneInfra'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -57,17 +56,15 @@ export const GlobalStateProvider = ({
> >
) )
const [settingsState, settingsSend, settingsActor] = useMachine( const [settingsState, settingsSend] = useMachine(settingsMachine, {
settingsMachine,
{
context: persistedSettings, context: persistedSettings,
actions: { actions: {
toastSuccess: (context, event) => { toastSuccess: (context, event) => {
const truncatedNewValue = const truncatedNewValue =
'data' in event && event.data instanceof Object 'data' in event && event.data instanceof Object
? (String( ? (context[Object.keys(event.data)[0] as keyof typeof context]
context[Object.keys(event.data)[0] as keyof typeof context] .toString()
).substring(0, 28) as any) .substring(0, 28) as any)
: undefined : undefined
toast.success( toast.success(
event.type + event.type +
@ -79,8 +76,7 @@ export const GlobalStateProvider = ({
) )
}, },
}, },
} })
)
settingsStateRef = settingsState.context settingsStateRef = settingsState.context
useStateMachineCommands({ useStateMachineCommands({
@ -88,7 +84,6 @@ export const GlobalStateProvider = ({
state: settingsState, state: settingsState,
send: settingsSend, send: settingsSend,
commandBarConfig: settingsCommandBarConfig, commandBarConfig: settingsCommandBarConfig,
actor: settingsActor,
}) })
// Listen for changes to the system theme and update the app theme accordingly // Listen for changes to the system theme and update the app theme accordingly
@ -102,14 +97,13 @@ export const GlobalStateProvider = ({
if (settingsState.context.theme !== 'system') return if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light) setThemeClass(e.matches ? Themes.Dark : Themes.Light)
} }
sceneInfra.baseUnit = settingsState?.context?.baseUnit || 'mm'
matcher.addEventListener('change', listener) matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener) return () => matcher.removeEventListener('change', listener)
}, [settingsState.context]) }, [settingsState.context])
// Auth machine setup // Auth machine setup
const [authState, authSend, authActor] = useMachine(authMachine, { const [authState, authSend] = useMachine(authMachine, {
actions: { actions: {
goToSignInPage: () => { goToSignInPage: () => {
navigate(paths.SIGN_IN) navigate(paths.SIGN_IN)
@ -129,7 +123,6 @@ export const GlobalStateProvider = ({
state: authState, state: authState,
send: authSend, send: authSend,
commandBarConfig: authCommandBarConfig, commandBarConfig: authCommandBarConfig,
actor: authActor,
}) })
return ( return (

View File

@ -25,7 +25,8 @@ describe('processMemory', () => {
|> lineTo([-3.35, 0.17], %) |> lineTo([-3.35, 0.17], %)
|> lineTo([0.98, 5.16], %) |> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)
show(theExtrude, theSketch)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast, { const programMemory = await enginelessExecutor(ast, {
root: {}, root: {},

View File

@ -38,10 +38,6 @@ import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
import { startSketchOnDefault } from 'lang/modifyAst' import { startSketchOnDefault } from 'lang/modifyAst'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'
import { isSingleCursorInPipe } from 'lang/queryAst' import { isSingleCursorInPipe } from 'lang/queryAst'
import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -58,12 +54,7 @@ export const ModelingMachineProvider = ({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const { const { auth } = useGlobalStateContext()
auth,
settings: {
context: { baseUnit },
},
} = useGlobalStateContext()
const { code } = useKclContext() const { code } = useKclContext()
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
@ -179,56 +170,6 @@ export const ModelingMachineProvider = ({
} }
return { selectionRangeTypeMap } return { selectionRangeTypeMap }
}), }),
'Engine export': (_, event) => {
if (event.type !== 'Export' || TEST) return
const format = {
...event.data,
} as Partial<Models['OutputFormat_type']>
// Set all the un-configurable defaults here.
if (format.type === 'gltf') {
format.presentation = 'pretty'
}
if (
format.type === 'obj' ||
format.type === 'ply' ||
format.type === 'step' ||
format.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
format.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
if (
format.type === 'obj' ||
format.type === 'stl' ||
format.type === 'ply'
) {
format.units = baseUnit
}
if (format.type === 'ply' || format.type === 'stl') {
format.selection = { type: 'default_scene' }
}
exportFromEngine({
source_unit: baseUnit,
format: format as Models['OutputFormat_type'],
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
},
}, },
guards: { guards: {
'has valid extrude selection': ({ selectionRanges }) => { 'has valid extrude selection': ({ selectionRanges }) => {
@ -251,8 +192,6 @@ export const ModelingMachineProvider = ({
selectionRanges selectionRanges
) )
}, },
'Has exportable geometry': () =>
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
}, },
services: { services: {
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => { 'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
@ -274,7 +213,7 @@ export const ModelingMachineProvider = ({
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal) const quaternion = getSketchQuaternion(pathToNode, normal)
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion) await sceneInfra.tweenCameraToQuaternion(quaternion)
return { return {
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
sketchNormalBackUp: normal, sketchNormalBackUp: normal,
@ -288,7 +227,7 @@ export const ModelingMachineProvider = ({
sketchPathToNode || [], sketchPathToNode || [],
sketchNormalBackUp sketchNormalBackUp
) )
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion) await sceneInfra.tweenCameraToQuaternion(quaternion)
}, },
'Get horizontal info': async ({ 'Get horizontal info': async ({
selectionRanges, selectionRanges,
@ -435,7 +374,6 @@ export const ModelingMachineProvider = ({
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineConfig,
allCommandsRequireNetwork: true,
onCancel: () => modelingSend({ type: 'Cancel' }), onCancel: () => modelingSend({ type: 'Cancel' }),
}) })

View File

@ -80,7 +80,7 @@ const overallConnectionStateIcon: Record<
[NetworkHealthState.Disconnected]: 'networkCrossedOut', [NetworkHealthState.Disconnected]: 'networkCrossedOut',
} }
export function useNetworkStatus() { export const NetworkHealthIndicator = () => {
const [steps, setSteps] = useState(initialConnectingTypeGroupState) const [steps, setSteps] = useState(initialConnectingTypeGroupState)
const [internetConnected, setInternetConnected] = useState<boolean>(true) const [internetConnected, setInternetConnected] = useState<boolean>(true)
const [overallState, setOverallState] = useState<NetworkHealthState>( const [overallState, setOverallState] = useState<NetworkHealthState>(
@ -118,18 +118,18 @@ export function useNetworkStatus() {
}, [hasIssues, internetConnected]) }, [hasIssues, internetConnected])
useEffect(() => { useEffect(() => {
const onlineCallback = () => { const cb1 = () => {
setSteps(initialConnectingTypeGroupState) setSteps(initialConnectingTypeGroupState)
setInternetConnected(true) setInternetConnected(true)
} }
const offlineCallback = () => { const cb2 = () => {
setInternetConnected(false) setInternetConnected(false)
} }
window.addEventListener('online', onlineCallback) window.addEventListener('online', cb1)
window.addEventListener('offline', offlineCallback) window.addEventListener('offline', cb2)
return () => { return () => {
window.removeEventListener('online', onlineCallback) window.removeEventListener('online', cb1)
window.removeEventListener('offline', offlineCallback) window.removeEventListener('offline', cb2)
} }
}, []) }, [])
@ -183,30 +183,6 @@ export function useNetworkStatus() {
) )
}, []) }, [])
return {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
}
}
export const NetworkHealthIndicator = () => {
const {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
} = useNetworkStatus()
return ( return (
<Popover className="relative"> <Popover className="relative">
<Popover.Button <Popover.Button

View File

@ -3,8 +3,8 @@ import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { GlobalStateProvider } from './GlobalStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { vi } from 'vitest'
const now = new Date() const now = new Date()
const projectWellFormed = { const projectWellFormed = {
@ -41,9 +41,11 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => { test('Renders the project name', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider> <GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} /> <ProjectSidebarMenu project={projectWellFormed} />
</GlobalStateProvider> </GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
@ -60,9 +62,11 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders app name if given no project', () => { test('Renders app name if given no project', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider> <GlobalStateProvider>
<ProjectSidebarMenu /> <ProjectSidebarMenu />
</GlobalStateProvider> </GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
@ -74,9 +78,14 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders as a link if set to do so', () => { test('Renders as a link if set to do so', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider> <GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} /> <ProjectSidebarMenu
project={projectWellFormed}
renderAsLink={true}
/>
</GlobalStateProvider> </GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )

View File

@ -5,12 +5,12 @@ import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton'
import { Fragment } from 'react' import { Fragment } from 'react'
import { FileTree } from './FileTree' import { FileTree } from './FileTree'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo' import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -21,8 +21,6 @@ const ProjectSidebarMenu = ({
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) => { }) => {
const { commandBarSend } = useCommandsContext()
return renderAsLink ? ( return renderAsLink ? (
<Link <Link
to={paths.HOME} to={paths.HOME}
@ -114,19 +112,13 @@ const ProjectSidebarMenu = ({
<div className="flex-1 overflow-hidden" /> <div className="flex-1 overflow-hidden" />
)} )}
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90"> <div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
<ActionButton <ExportButton
Element="button" className={{
icon={{ icon: 'exportFile', className: 'p-1' }} button: 'border-transparent dark:border-transparent',
className="border-transparent dark:border-transparent" }}
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Export', ownerMachine: 'modeling' },
})
}
> >
Export Part Export Model
</ActionButton> </ExportButton>
{isTauri() && ( {isTauri() && (
<ActionButton <ActionButton
Element="link" Element="link"

View File

@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
import { import {
AvailableVars, AvailableVars,
addToInputHelper, addToInputHelper,
useCalc,
CalcResult, CalcResult,
CreateNewVariable, CreateNewVariable,
} from './AvailableVarsHelpers' } from './AvailableVarsHelpers'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
type ModalResolve = { type ModalResolve = {
value: string value: string
@ -55,7 +55,7 @@ export const SetAngleLengthModal = ({
setNewVariableName, setNewVariableName,
inputRef, inputRef,
newVariableInsertIndex, newVariableInsertIndex,
} = useCalculateKclExpression({ } = useCalc({
value, value,
initialVariableName: valueName, initialVariableName: valueName,
}) })

View File

@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
import { import {
AvailableVars, AvailableVars,
addToInputHelper, addToInputHelper,
useCalc,
CalcResult, CalcResult,
CreateNewVariable, CreateNewVariable,
} from './AvailableVarsHelpers' } from './AvailableVarsHelpers'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
type ModalResolve = { type ModalResolve = {
value: string value: string
@ -59,7 +59,7 @@ export const GetInfoModal = ({
newVariableName, newVariableName,
isNewVariableNameUnique, isNewVariableNameUnique,
newVariableInsertIndex, newVariableInsertIndex,
} = useCalculateKclExpression({ value: value, initialVariableName }) } = useCalc({ value: value, initialVariableName })
return ( return (
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>

View File

@ -1,11 +1,10 @@
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react' import { Fragment } from 'react'
import { CreateNewVariable } from './AvailableVarsHelpers' import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faPlus } from '@fortawesome/free-solid-svg-icons' import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { type InstanceProps, create } from 'react-modal-promise' import { type InstanceProps, create } from 'react-modal-promise'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
type ModalResolve = { variableName: string } type ModalResolve = { variableName: string }
type ModalReject = boolean type ModalReject = boolean
@ -26,7 +25,7 @@ export const SetVarNameModal = ({
valueName, valueName,
}: SetVarNameModalProps) => { }: SetVarNameModalProps) => {
const { isNewVariableNameUnique, newVariableName, setNewVariableName } = const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
useCalculateKclExpression({ value: '', initialVariableName: valueName }) useCalc({ value: '', initialVariableName: valueName })
return ( return (
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>

View File

@ -1,15 +1,21 @@
import { MouseEventHandler, useEffect, useRef, useState } from 'react' import {
MouseEventHandler,
WheelEventHandler,
useEffect,
useRef,
useState,
} from 'react'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils' import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useKclContext } from 'lang/KclSingleton' import { useKclContext } from 'lang/KclSingleton'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
export const Stream = ({ className = '' }: { className?: string }) => { export const Stream = ({ className = '' }: { className?: string }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -29,10 +35,9 @@ export const Stream = ({ className = '' }: { className?: string }) => {
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls
const { state } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok
useEffect(() => { useEffect(() => {
if ( if (
@ -60,6 +65,19 @@ export const Stream = ({ className = '' }: { className?: string }) => {
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: e.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
}, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({ const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
clientX, clientX,
clientY, clientY,
@ -138,6 +156,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
muted muted
autoPlay autoPlay
controls={false} controls={false}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)} onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove} onMouseMoveCapture={handleMouseMove}
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`} className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
@ -145,13 +164,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }} style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/> />
<ClientSideScene cameraControls={settings.context.cameraControls} /> <ClientSideScene cameraControls={settings.context.cameraControls} />
{!isNetworkOkay && !isLoading && (
<div className="text-center absolute inset-0">
<Loading>
<span data-testid="loading-stream">Stream disconnected</span>
</Loading>
</div>
)}
{isLoading && ( {isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> <div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<Loading> <Loading>

View File

@ -1,4 +1,3 @@
import { undo, redo } from '@codemirror/commands'
import ReactCodeMirror, { import ReactCodeMirror, {
Extension, Extension,
ViewUpdate, ViewUpdate,
@ -12,7 +11,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { useEffect, useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { linter, lintGutter } from '@codemirror/lint' import { linter, lintGutter } from '@codemirror/lint'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections' import { processCodeMirrorRanges } from 'lib/selections'
@ -26,14 +25,11 @@ import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { useFileContext } from 'hooks/useFileContext'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { sceneInfra } from 'clientSideScene/sceneInfra' import { sceneInfra } from 'clientSideScene/sceneInfra'
import { copilotPlugin } from 'editor/plugins/lsp/copilot' import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import { useHotkeys } from 'react-hotkeys-hook'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -79,28 +75,6 @@ export const TextEditor = ({
})) }))
const { code, errors } = useKclContext() const { code, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() }) const lastEvent = useRef({ event: '', time: Date.now() })
const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok
useEffect(() => {
if (typeof window === 'undefined') return
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
window.addEventListener('online', onlineCallback)
return () => window.removeEventListener('online', onlineCallback)
}, [])
useHotkeys('mod+z', (e) => {
e.preventDefault()
if (editorView) {
undo(editorView)
}
})
useHotkeys('mod+shift+z', (e) => {
e.preventDefault()
if (editorView) {
redo(editorView)
}
})
const { const {
context: { selectionRanges, selectionRangeTypeMap }, context: { selectionRanges, selectionRangeTypeMap },
@ -111,9 +85,6 @@ export const TextEditor = ({
const { settings: { context: { textWrapping } = {} } = {}, auth } = const { settings: { context: { textWrapping } = {} } = {}, auth } =
useGlobalStateContext() useGlobalStateContext()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const {
context: { project },
} = useFileContext()
const { enable: convertEnabled, handleClick: convertCallback } = const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable() useConvertToVariable()
@ -136,7 +107,7 @@ export const TextEditor = ({
}, [setIsKclLspServerReady]) }, [setIsKclLspServerReady])
// Here we initialize the plugin which will start the client. // Here we initialize the plugin which will start the client.
// Now that we have multi-file support the name of the file is a dep of // When we have multi-file support the name of the file will be a dep of
// this use memo, as well as the directory structure, which I think is // this use memo, as well as the directory structure, which I think is
// a good setup because it will restart the client but not the server :) // a good setup because it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
@ -192,12 +163,11 @@ export const TextEditor = ({
plugin = lsp plugin = lsp
} }
return plugin return plugin
}, [copilotLspClient, isCopilotLspServerReady, project]) }, [copilotLspClient, isCopilotLspServerReady])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => { // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = async (newCode: string) => { const onChange = (newCode: string) => {
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode) kclManager.setCodeAndExecute(newCode)
else kclManager.setCode(newCode)
} //, []); } //, []);
const onUpdate = (viewUpdate: ViewUpdate) => { const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) { if (!editorView) {

View File

@ -27,8 +27,6 @@ describe('UserSidebarMenu tests', () => {
phone: '555-555-5555', phone: '555-555-5555',
first_name: 'Test', first_name: 'Test',
last_name: 'User', last_name: 'User',
can_train_on_data: false,
is_service_account: false,
} }
render( render(
@ -59,8 +57,6 @@ describe('UserSidebarMenu tests', () => {
first_name: '', first_name: '',
last_name: '', last_name: '',
name: '', name: '',
can_train_on_data: false,
is_service_account: false,
} }
render( render(
@ -88,8 +84,6 @@ describe('UserSidebarMenu tests', () => {
first_name: 'Test', first_name: 'Test',
last_name: 'User', last_name: 'User',
image: '', image: '',
can_train_on_data: false,
is_service_account: false,
} }
render( render(

View File

@ -142,7 +142,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/issues/new/choose" to="https://github.com/KittyCAD/modeling-app/issues/new"
icon={{ icon: faBug, className: 'p-1', size: 'sm' }} icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent" className="border-transparent dark:border-transparent"
> >

View File

@ -4,7 +4,6 @@ import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol' import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { offsetToPos } from 'editor/plugins/lsp/util' import { offsetToPos } from 'editor/plugins/lsp/util'
import { LanguageServerOptions } from 'editor/plugins/lsp' import { LanguageServerOptions } from 'editor/plugins/lsp'
import { syntaxTree } from '@codemirror/language'
import { import {
LanguageServerPlugin, LanguageServerPlugin,
documentUri, documentUri,
@ -41,14 +40,6 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
if (plugin == null) return null if (plugin == null) return null
const { state, pos, explicit } = context const { state, pos, explicit } = context
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos) const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined let trigChar: string | undefined
@ -69,7 +60,6 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
) { ) {
return null return null
} }
return await plugin.requestCompletion( return await plugin.requestCompletion(
context, context,
offsetToPos(state.doc, pos), offsetToPos(state.doc, pos),

View File

@ -7,5 +7,6 @@ export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS .VITE_KC_CONNECTION_TIMEOUT_MS
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
export const TEST = import.meta.env.TEST export const TEST = import.meta.env.TEST
export const DEV = import.meta.env.DEV export const DEV = import.meta.env.DEV

View File

@ -7,12 +7,6 @@ import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes' import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useKclContext } from 'lang/KclSingleton'
import { useStore } from 'useStore'
// This might not be necessary, AnyStateMachine from xstate is working // This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines = export type AllMachines =
@ -28,9 +22,8 @@ interface UseStateMachineCommandsArgs<
machineId: T['id'] machineId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor?: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: CommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean
onCancel?: () => void onCancel?: () => void
} }
@ -43,21 +36,12 @@ export default function useStateMachineCommands<
send, send,
actor, actor,
commandBarConfig, commandBarConfig,
allCommandsRequireNetwork = false,
onCancel, onCancel,
}: UseStateMachineCommandsArgs<T, S>) { }: UseStateMachineCommandsArgs<T, S>) {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { overallState } = useNetworkStatus()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
useEffect(() => { useEffect(() => {
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
const newCommands = state.nextEvents const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) => .map((type) =>
createMachineCommand<T, S>({ createMachineCommand<T, S>({
@ -80,5 +64,5 @@ export default function useStateMachineCommands<
data: { commands: newCommands }, data: { commands: newCommands },
}) })
} }
}, [state, overallState, isExecuting, isStreamReady]) }, [state])
} }

View File

@ -93,7 +93,7 @@ class KclManager {
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
this._params.id && this._params.id &&
writeTextFile(this._params.id, code).catch((err) => { writeTextFile(this._params.id, code).catch((err) => {
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254) // TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')
}) })
@ -239,8 +239,8 @@ class KclManager {
const currentExecutionId = executionId || Date.now() const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
this.isExecuting = true
await this.ensureWasmInit() await this.ensureWasmInit()
this.isExecuting = true
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast, ast,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,

View File

@ -11,11 +11,16 @@ const mySketch001 = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %)` // |> rx(45, %)
show(mySketch001)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.root?.mySketch001 const shown = programMemory?.return?.map(
expect(sketch001).toEqual({ // @ts-ignore
(a) => programMemory?.root?.[a.name]
)
expect(shown).toEqual([
{
type: 'SketchGroup', type: 'SketchGroup',
on: expect.any(Object), on: expect.any(Object),
start: { start: {
@ -57,7 +62,8 @@ const mySketch001 = startSketchOn('XY')
id: expect.any(String), id: expect.any(String),
entityId: expect.any(String), entityId: expect.any(String),
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
}) },
])
}) })
test('extrude artifacts', async () => { test('extrude artifacts', async () => {
// Enable rotations #152 // Enable rotations #152
@ -67,11 +73,16 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
|> extrude(2, %)` |> extrude(2, %)
show(mySketch001)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.root?.mySketch001 const shown = programMemory?.return?.map(
expect(sketch001).toEqual({ // @ts-ignore
(a) => programMemory?.root?.[a.name]
)
expect(shown).toEqual([
{
type: 'ExtrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [],
@ -84,7 +95,8 @@ const mySketch001 = startSketchOn('XY')
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
}) },
])
}) })
test('sketch extrude and sketch on one of the faces', async () => { test('sketch extrude and sketch on one of the faces', async () => {
// Enable rotations #152 // Enable rotations #152
@ -108,10 +120,14 @@ const sk2 = startSketchOn('XY')
// |> transform(theTransf, %) // |> transform(theTransf, %)
|> extrude(2, %) |> extrude(2, %)
`
show(theExtrude, sk2)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2] const geos = programMemory?.return?.map(
// @ts-ignore
({ name }) => programMemory?.root?.[name]
)
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'ExtrudeGroup', type: 'ExtrudeGroup',

View File

@ -47,8 +47,9 @@ const newVar = myVar + 1`
|> lineTo([2,3], %) |> lineTo([2,3], %)
|> lineTo({ to: [5,-1], tag: "rightPath" }, %) |> lineTo({ to: [5,-1], tag: "rightPath" }, %)
// |> close(%) // |> close(%)
show(mySketch)
` `
const { root } = await exe(code) const { root, return: _return } = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests // geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = root.mySketch.value const minusGeo = root.mySketch.value
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
@ -83,6 +84,15 @@ const newVar = myVar + 1`
name: 'rightPath', name: 'rightPath',
}, },
]) ])
// expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath)
expect(_return).toEqual([
{
type: 'Identifier',
start: 203,
end: 211,
name: 'mySketch',
},
])
}) })
it('pipe binary expression into call expression', async () => { it('pipe binary expression into call expression', async () => {
@ -347,6 +357,7 @@ describe('testing math operators', () => {
` -legLen(segLen('seg01', %), myVar)`, ` -legLen(segLen('seg01', %), myVar)`,
`], %)`, `], %)`,
``, ``,
`show(part001)`,
].join('\n') ].join('\n')
const { root } = await exe(code) const { root } = await exe(code)
const sketch = root.part001 const sketch = root.part001
@ -381,7 +392,8 @@ const theExtrude = startSketchOn('XY')
|> line([-0.76], myVarZ, %) |> line([-0.76], myVarZ, %)
|> line([5,5], %) |> line([5,5], %)
|> close(%) |> close(%)
|> extrude(4, %)` |> extrude(4, %)
show(theExtrude)`
await expect(exe(code)).rejects.toEqual( await expect(exe(code)).rejects.toEqual(
new KCLError( new KCLError(
'undefined_value', 'undefined_value',

View File

@ -1,4 +1,4 @@
import { parse, recast, initPromise, Identifier } from './wasm' import { parse, recast, initPromise } from './wasm'
import { import {
createLiteral, createLiteral,
createIdentifier, createIdentifier,
@ -90,17 +90,7 @@ describe('Testing createPipeExpression', () => {
describe('Testing findUniqueName', () => { describe('Testing findUniqueName', () => {
it('should find a unique name', () => { it('should find a unique name', () => {
const result = findUniqueName( const result = findUniqueName(
JSON.stringify([ 'yo01 yo02 yo03 yo04 yo05 yo06 yo07 yo08 yo09',
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
] satisfies Identifier[]),
'yo', 'yo',
2 2
) )
@ -122,6 +112,7 @@ describe('Testing addSketchTo', () => {
expect(str).toBe(`const part001 = startSketchOn('YZ') expect(str).toBe(`const part001 = startSketchOn('YZ')
|> startProfileAt('default', %) |> startProfileAt('default', %)
|> line('default', %) |> line('default', %)
show(part001)
`) `)
}) })
}) })
@ -146,7 +137,8 @@ describe('Testing giveSketchFnCallTag', () => {
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([-2.57, -0.13], %) |> line([-2.57, -0.13], %)
|> line([0, 0.83], %) |> line([0, 0.83], %)
|> line([0.82, 0.34], %)` |> line([0.82, 0.34], %)
show(part001)`
it('Should add tag to a sketch function call', () => { it('Should add tag to a sketch function call', () => {
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper( const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
code, code,
@ -202,7 +194,8 @@ const part001 = startSketchOn('XY')
|> angledLine([def(yo), 3.09], %) |> angledLine([def(yo), 3.09], %)
|> angledLine([ghi(%), 3.09], %) |> angledLine([ghi(%), 3.09], %)
|> angledLine([jkl(yo) + 2, 3.09], %) |> angledLine([jkl(yo) + 2, 3.09], %)
const yo2 = hmm([identifierGuy + 5])` const yo2 = hmm([identifierGuy + 5])
show(part001)`
it('should move a binary expression into a new variable', async () => { it('should move a binary expression into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)

View File

@ -6,6 +6,7 @@ import {
PipeExpression, PipeExpression,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
ExpressionStatement,
Value, Value,
Literal, Literal,
PipeSubstitution, PipeSubstitution,
@ -127,8 +128,16 @@ export function addSketchTo(
createPipeExpression(pipeBody) createPipeExpression(pipeBody)
) )
const showCallIndex = getShowIndex(_node)
let sketchIndex = showCallIndex
if (showCallIndex === -1) {
_node.body = [...node.body, variableDeclaration] _node.body = [...node.body, variableDeclaration]
let sketchIndex = _node.body.length - 1 sketchIndex = _node.body.length - 1
} else {
const newBody = [...node.body]
newBody.splice(showCallIndex, 0, variableDeclaration)
_node.body = newBody
}
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
@ -141,7 +150,7 @@ export function addSketchTo(
} }
return { return {
modifiedAst: _node, modifiedAst: addToShow(_node, _name),
id: _name, id: _name,
pathToNode, pathToNode,
} }
@ -153,33 +162,57 @@ export function findUniqueName(
pad = 3, pad = 3,
index = 1 index = 1
): string { ): string {
let searchStr: string = typeof ast === 'string' ? ast : JSON.stringify(ast) let searchStr = ''
const indexStr = String(index).padStart(pad, '0') if (typeof ast === 'string') {
searchStr = ast
const endingDigitsMatcher = /\d+$/ } else {
const nameEndsInDigits = name.match(endingDigitsMatcher) searchStr = JSON.stringify(ast)
let nameIsInString = searchStr.includes(`:"${name}"`) }
const indexStr = `${index}`.padStart(pad, '0')
if (nameEndsInDigits !== null) { const newName = `${name}${indexStr}`
// base case: name is unique and ends in digits const isInString = searchStr.includes(newName)
if (!nameIsInString) return name if (!isInString) {
return newName
// recursive case: name is not unique and ends in digits }
const newPad = nameEndsInDigits[1].length return findUniqueName(searchStr, name, pad, index + 1)
const newIndex = parseInt(nameEndsInDigits[1]) + 1
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
} }
const newName = `${name}${indexStr}` function addToShow(node: Program, name: string): Program {
nameIsInString = searchStr.includes(`:"${newName}"`) const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const showCallIndex = getShowIndex(_node)
if (showCallIndex === -1) {
const showCall = createCallExpressionStdLib('show', [
createIdentifier(name),
])
const showExpressionStatement: ExpressionStatement = {
type: 'ExpressionStatement',
...dumbyStartend,
expression: showCall,
}
_node.body = [..._node.body, showExpressionStatement]
return _node
}
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
const showCallArgs = (showCall.expression as CallExpression).arguments
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
// base case: name is unique and does not end in digits _node.body[showCallIndex] = {
if (!nameIsInString) return newName ...showCall,
expression: newShowExpression,
}
return _node
}
// recursive case: name is not unique and does not end in digits function getShowIndex(node: Program): number {
return findUniqueName(searchStr, name, pad, index + 1) return node.body.findIndex(
(statement) =>
statement.type === 'ExpressionStatement' &&
statement.expression.type === 'CallExpression' &&
statement.expression.callee.type === 'Identifier' &&
statement.expression.callee.name === 'show'
)
} }
export function mutateArrExp( export function mutateArrExp(
@ -240,7 +273,7 @@ export function extrudeSketch(
node: Program, node: Program,
pathToNode: PathToNode, pathToNode: PathToNode,
shouldPipe = true, shouldPipe = true,
distance = createLiteral(4) as Value distance = 4
): { ): {
modifiedAst: Program modifiedAst: Program
pathToNode: PathToNode pathToNode: PathToNode
@ -266,7 +299,7 @@ export function extrudeSketch(
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator') getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
const extrudeCall = createCallExpressionStdLib('extrude', [ const extrudeCall = createCallExpressionStdLib('extrude', [
distance, createLiteral(distance),
shouldPipe shouldPipe
? createPipeSubstitution() ? createPipeSubstitution()
: { : {
@ -301,10 +334,15 @@ export function extrudeSketch(
} }
const name = findUniqueName(node, 'part') const name = findUniqueName(node, 'part')
const VariableDeclaration = createVariableDeclaration(name, extrudeCall) const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
_node.body.splice(_node.body.length, 0, VariableDeclaration) let showCallIndex = getShowIndex(_node)
if (showCallIndex === -1) {
// We didn't find a show, so let's just append everything
showCallIndex = _node.body.length
}
_node.body.splice(showCallIndex, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
['body', ''], ['body', ''],
[_node.body.length, 'index'], [showCallIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'], [0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
@ -313,7 +351,7 @@ export function extrudeSketch(
] ]
return { return {
modifiedAst: node, modifiedAst: node,
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']], pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
pathToExtrudeArg, pathToExtrudeArg,
} }
} }
@ -373,7 +411,7 @@ export function sketchOnExtrudedFace(
_node.body.splice(expressionIndex + 1, 0, newSketch) _node.body.splice(expressionIndex + 1, 0, newSketch)
return { return {
modifiedAst: _node, modifiedAst: addToShow(_node, newSketchName),
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']], pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
} }
} }

View File

@ -34,7 +34,8 @@ const part001 = startSketchOn('XY')
|> xLine(3.84, %) // selection-range-7ish-before-this |> xLine(3.84, %) // selection-range-7ish-before-this
const variableBelowShouldNotBeIncluded = 3 const variableBelowShouldNotBeIncluded = 3
`
show(part001)`
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7 const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
@ -68,7 +69,8 @@ describe('testing argIsNotIdentifier', () => {
|> angledLine([ghi(%), 3.09], %) |> angledLine([ghi(%), 3.09], %)
|> angledLine([jkl('yo') + 2, 3.09], %) |> angledLine([jkl('yo') + 2, 3.09], %)
const yo = 5 + 6 const yo = 5 + 6
const yo2 = hmm([identifierGuy + 5])` const yo2 = hmm([identifierGuy + 5])
show(part001)`
it('find a safe binaryExpression', () => { it('find a safe binaryExpression', () => {
const ast = parse(code) const ast = parse(code)
const rangeStart = code.indexOf('100 + 100') + 2 const rangeStart = code.indexOf('100 + 100') + 2
@ -199,7 +201,8 @@ describe('testing getNodePathFromSourceRange', () => {
const code = `const part001 = startSketchOn('XY') const code = `const part001 = startSketchOn('XY')
|> startProfileAt([0.39, -0.05], %) |> startProfileAt([0.39, -0.05], %)
|> line([0.94, 2.61], %) |> line([0.94, 2.61], %)
|> line([-0.21, -1.4], %)` |> line([-0.21, -1.4], %)
show(part001)`
it('finds the second line when cursor is put at the end', () => { it('finds the second line when cursor is put at the end', () => {
const searchLn = `line([0.94, 2.61], %)` const searchLn = `line([0.94, 2.61], %)`
const sourceIndex = code.indexOf(searchLn) + searchLn.length const sourceIndex = code.indexOf(searchLn) + searchLn.length

View File

@ -68,6 +68,8 @@ log(5, myVar)
|> lineTo([1, 1], %) |> lineTo([1, 1], %)
|> lineTo({ to: [1, 0], tag: "rightPath" }, %) |> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|> close(%) |> close(%)
show(mySketch)
` `
const { ast } = code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
@ -329,6 +331,7 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
intersectTag: 'seg01' intersectTag: 'seg01'
}, %) }, %)
|> line([-0.42, -1.72], %) |> line([-0.42, -1.72], %)
show(part001)
` `
const { ast } = code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)

View File

@ -3,6 +3,7 @@ import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { sceneInfra } from 'clientSideScene/sceneInfra' import { sceneInfra } from 'clientSideScene/sceneInfra'
@ -289,6 +290,12 @@ class EngineConnection {
} }
} }
// shouldTrace will return true when Sentry should be used to instrument
// the Engine.
shouldTrace() {
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
}
// connect will attempt to connect to the Engine over a WebSocket, and // connect will attempt to connect to the Engine over a WebSocket, and
// establish the WebRTC connections. // establish the WebRTC connections.
// //
@ -301,6 +308,41 @@ class EngineConnection {
// Information on the connect transaction // Information on the connect transaction
class SpanPromise {
span: Sentry.Span
promise: Promise<void>
resolve?: (v: void) => void
constructor(span: Sentry.Span) {
this.span = span
this.promise = new Promise((resolve) => {
this.resolve = (v: void) => {
// here we're going to invoke finish before resolving the
// promise so that a `.then()` will order strictly after
// all spans have -- for sure -- been resolved, rather than
// doing a `then` on this promise.
this.span.finish()
resolve(v)
}
})
}
}
let webrtcMediaTransaction: Sentry.Transaction
let websocketSpan: SpanPromise
let mediaTrackSpan: SpanPromise
let dataChannelSpan: SpanPromise
let handshakeSpan: SpanPromise
let iceSpan: SpanPromise
const spanStart = (op: string) =>
new SpanPromise(webrtcMediaTransaction.startChild({ op }))
if (this.shouldTrace()) {
webrtcMediaTransaction = Sentry.startTransaction({ name: 'webrtc-media' })
websocketSpan = spanStart('websocket')
}
const createPeerConnection = () => { const createPeerConnection = () => {
this.pc = new RTCPeerConnection() this.pc = new RTCPeerConnection()
@ -351,6 +393,10 @@ class EngineConnection {
// From what I understand, only after have we done the ICE song and // From what I understand, only after have we done the ICE song and
// dance is it safest to connect the video tracks / stream // dance is it safest to connect the video tracks / stream
case 'connected': case 'connected':
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
// Let the browser attach to the video stream now // Let the browser attach to the video stream now
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! }) this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
break break
@ -383,6 +429,17 @@ class EngineConnection {
}, },
} }
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
// let settings = mediaStreamTrack.getSettings()
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
// mediaTrackSpan.span.setTag("width", settings.width)
// mediaTrackSpan.span.setTag("height", settings.height)
mediaTrackSpan.resolve?.()
})
}
this.webrtcStatsCollector = (): Promise<ClientMetrics> => { this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (mediaStream.getVideoTracks().length !== 1) { if (mediaStream.getVideoTracks().length !== 1) {
@ -465,6 +522,10 @@ class EngineConnection {
}, },
} }
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
// Everything is now connected. // Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished } this.state = { type: EngineConnectionStateType.ConnectionEstablished }
@ -516,6 +577,27 @@ class EngineConnection {
if (this.token) { if (this.token) {
this.send({ headers: { Authorization: `Bearer ${this.token}` } }) this.send({ headers: { Authorization: `Bearer ${this.token}` } })
} }
if (this.shouldTrace()) {
websocketSpan.resolve?.()
handshakeSpan = spanStart('handshake')
iceSpan = spanStart('ice')
dataChannelSpan = spanStart('data-channel')
mediaTrackSpan = spanStart('media-track')
}
if (this.shouldTrace()) {
void Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
}
}) })
this.websocket.addEventListener('close', (event) => { this.websocket.addEventListener('close', (event) => {
@ -704,6 +786,13 @@ failed cmd type was ${artifactThatFailed?.commandType}`
type: ConnectingType.WebRTCConnecting, type: ConnectingType.WebRTCConnecting,
}, },
} }
if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.resolve?.()
}
break break
case 'trickle_ice': case 'trickle_ice':
@ -796,7 +885,7 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
callback: (data: Extract<UnreliableResponses, { type: T }>) => void callback: (data: Extract<UnreliableResponses, { type: T }>) => void
} }
export interface Subscription<T extends ModelTypes> { interface Subscription<T extends ModelTypes> {
event: T event: T
callback: ( callback: (
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }> data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
@ -907,6 +996,9 @@ export class EngineCommandManager {
} }
}, },
onEngineConnectionOpen: () => { onEngineConnectionOpen: () => {
this.resolveReady()
setIsStreamReady(true)
// Make the axis gizmo. // Make the axis gizmo.
// We do this after the connection opened to avoid a race condition. // We do this after the connection opened to avoid a race condition.
// Connected opened is the last thing that happens when the stream // Connected opened is the last thing that happens when the stream
@ -925,20 +1017,9 @@ export class EngineCommandManager {
gizmo_mode: true, gizmo_mode: true,
}, },
}) })
sceneInfra.camControls.onCameraChange() sceneInfra.onStreamStart()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
this.initPlanes().then(() => { this.initPlanes().then(() => {
this.resolveReady()
setIsStreamReady(true)
executeCode(undefined, true) executeCode(undefined, true)
}) })
}, },

View File

@ -101,6 +101,7 @@ describe('testing changeSketchArguments', () => {
|> ${line} |> ${line}
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
show(mySketch001)
` `
const code = genCode(lineToChange) const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange) const expectedCode = genCode(lineAfterChange)
@ -127,7 +128,8 @@ const mySketch001 = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
// |> rx(45, %) // |> rx(45, %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)` |> lineTo([0.46, -5.82], %)
show(mySketch001)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
@ -153,6 +155,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %) |> lineTo([2, 3], %)
show(mySketch001)
` `
expect(recast(modifiedAst)).toBe(expectedCode) expect(recast(modifiedAst)).toBe(expectedCode)
@ -174,6 +177,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
|> close(%) |> close(%)
show(mySketch001)
` `
expect(recast(modifiedAst)).toBe(expectedCode) expect(recast(modifiedAst)).toBe(expectedCode)
}) })
@ -188,6 +192,7 @@ describe('testing addTagForSketchOnFace', () => {
// |> rx(45, %) // |> rx(45, %)
|> ${line} |> ${line}
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
show(mySketch001)
` `
const code = genCode(originalLine) const code = genCode(originalLine)
const ast = parse(code) const ast = parse(code)

View File

@ -91,6 +91,12 @@ export function createFirstArg(
throw new Error('all sketch line types should have been covered') throw new Error('all sketch line types should have been covered')
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type LineData = {
from: [number, number, number]
to: [number, number, number]
}
export const lineTo: SketchLineHelper = { export const lineTo: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -960,30 +966,6 @@ export const angledLineThatIntersects: SketchLineHelper = {
addTag: addTagWithTo('angleTo'), // TODO might be wrong addTag: addTagWithTo('angleTo'), // TODO might be wrong
} }
export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
node,
pathToNode,
to,
}) => {
const _node = { ...node }
const { node: callExpression } = getNodeFromPath<CallExpression>(
_node,
pathToNode
)
const toArrExp = createArrayExpression([
createLiteral(roundOff(to[0])),
createLiteral(roundOff(to[1])),
])
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
return {
modifiedAst: _node,
pathToNode,
}
}
export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
line, line,
lineTo, lineTo,

View File

@ -88,6 +88,7 @@ describe('testing swapping out sketch calls with xLine/xLineTo', () => {
` |> yLine(-1.07, %)`, ` |> yLine(-1.07, %)`,
` |> xLineTo(3.27, %)`, ` |> xLineTo(3.27, %)`,
` |> yLineTo(2.14, %)`, ` |> yLineTo(2.14, %)`,
`show(part001)`,
] ]
const bigExample = bigExampleArr.join('\n') const bigExample = bigExampleArr.join('\n')
it('line with tag converts to xLine', async () => { it('line with tag converts to xLine', async () => {
@ -289,6 +290,7 @@ describe('testing swapping out sketch calls with xLine/xLineTo while keeping var
` |> angledLineToX([330, angledLineToXx], %)`, ` |> angledLineToX([330, angledLineToXx], %)`,
` |> angledLineToY([217, angledLineToYy], %)`, ` |> angledLineToY([217, angledLineToYy], %)`,
` |> line([0.89, -0.1], %)`, ` |> line([0.89, -0.1], %)`,
`show(part001)`,
] ]
const varExample = variablesExampleArr.join('\n') const varExample = variablesExampleArr.join('\n')
it('line keeps variable when converted to xLine', async () => { it('line keeps variable when converted to xLine', async () => {
@ -376,7 +378,8 @@ const part001 = startSketchOn('XY')
|> line([0, 0.4], %) |> line([0, 0.4], %)
|> xLine(3.48, %) |> xLine(3.48, %)
|> line([2.14, 1.35], %) // normal-segment |> line([2.14, 1.35], %) // normal-segment
|> xLine(3.54, %)` |> xLine(3.54, %)
show(part001)`
it('normal case works', async () => { it('normal case works', async () => {
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7 const index = code.indexOf('// normal-segment') - 7

View File

@ -123,6 +123,7 @@ const part001 = startSketchOn('XY')
|> yLine(1.04, %) // ln-yLine-free should sub in segLen |> yLine(1.04, %) // ln-yLine-free should sub in segLen
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine |> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine |> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
show(part001)
` `
const expectModifiedScript = `const myVar = 3 const expectModifiedScript = `const myVar = 3
const myVar2 = 5 const myVar2 = 5
@ -195,6 +196,7 @@ const part001 = startSketchOn('XY')
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen |> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine |> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine |> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
show(part001)
` `
it('should transform the ast', async () => { it('should transform the ast', async () => {
const ast = parse(inputScript) const ast = parse(inputScript)
@ -255,6 +257,7 @@ const part001 = startSketchOn('XY')
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9 |> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10 |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10 |> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)
` `
it('should transform horizontal lines the ast', async () => { it('should transform horizontal lines the ast', async () => {
const expectModifiedScript = `const myVar = 2 const expectModifiedScript = `const myVar = 2
@ -283,6 +286,7 @@ const part001 = startSketchOn('XY')
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9 |> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> xLineTo(myVar3, %) // select for horizontal constraint 10 |> xLineTo(myVar3, %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10 |> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)
` `
const ast = parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
@ -341,6 +345,7 @@ const part001 = startSketchOn('XY')
|> yLineTo(7.68, %) // select for vertical constraint 9 |> yLineTo(7.68, %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10 |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> yLineTo(myVar, %) // select for vertical constraint 10 |> yLineTo(myVar, %) // select for vertical constraint 10
show(part001)
` `
const ast = parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
@ -384,6 +389,7 @@ const part001 = startSketchOn('XY')
|> line([0.45, 1.46], %) // free |> line([0.45, 1.46], %) // free
|> line([myVar, 0.01], %) // xRelative |> line([myVar, 0.01], %) // xRelative
|> line([0.7, myVar], %) // yRelative |> line([0.7, myVar], %) // yRelative
show(part001)
` `
it('testing for free to horizontal and vertical distance', async () => { it('testing for free to horizontal and vertical distance', async () => {
const expectedHorizontalCode = await helperThing( const expectedHorizontalCode = await helperThing(
@ -495,7 +501,8 @@ const part001 = startSketchOn('XY')
|> xLine(3.36, %) // partial |> xLine(3.36, %) // partial
|> line([-1.49, 1.06], %) // free |> line([-1.49, 1.06], %) // free
|> xLine(-3.43 + 0, %) // full |> xLine(-3.43 + 0, %) // full
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full` |> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full
show(part001)`
const ast = parse(code) const ast = parse(code)
const constraintLevels: ReturnType< const constraintLevels: ReturnType<
typeof getConstraintLevelFromSourceRange typeof getConstraintLevelFromSourceRange

View File

@ -15,7 +15,8 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset}, offset: ${offset},
tag: "yo2" tag: "yo2"
}, %) }, %)
const intersect = segEndX('yo2', part001)` const intersect = segEndX('yo2', part001)
show(part001)`
const { root } = await enginelessExecutor(parse(code('-1'))) const { root } = await enginelessExecutor(parse(code('-1')))
expect(root.intersect.value).toBe(1 + Math.sqrt(2)) expect(root.intersect.value).toBe(1 + Math.sqrt(2))
const { root: noOffset } = await enginelessExecutor(parse(code('0'))) const { root: noOffset } = await enginelessExecutor(parse(code('0')))

View File

@ -39,36 +39,30 @@ export interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
const butName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1) || e.button === 0,
})
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = { export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: { KittyCAD: {
pan: { pan: {
description: 'Right click + Shift + drag or middle click + drag', description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).middle && noModifiersPressed(e)) || (e.button === 1 && noModifiersPressed(e)) ||
(butName(e).right && e.shiftKey), (e.button === 2 && e.shiftKey),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey, dragCallback: (e) => e.button === 2 && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click + drag',
callback: (e) => butName(e).right && noModifiersPressed(e), callback: (e) => e.button === 2 && noModifiersPressed(e),
}, },
}, },
OnShape: { OnShape: {
pan: { pan: {
description: 'Right click + Ctrl + drag or middle click + drag', description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).right && e.ctrlKey) || (e.button === 2 && e.ctrlKey) ||
(butName(e).middle && noModifiersPressed(e)), (e.button === 1 && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -77,77 +71,77 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click + drag',
callback: (e) => butName(e).right && noModifiersPressed(e), callback: (e) => e.button === 2 && noModifiersPressed(e),
}, },
}, },
'Trackpad Friendly': { 'Trackpad Friendly': {
pan: { pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag', description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).left && e.altKey && e.shiftKey && !e.metaKey) || (e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
(butName(e).middle && noModifiersPressed(e)), (e.button === 1 && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag', description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => butName(e).left && e.altKey && e.metaKey, dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Left click + Alt + drag', description: 'Left click + Alt + drag',
callback: (e) => butName(e).left && e.altKey && !e.shiftKey && !e.metaKey, callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0, lenientDragStartButton: 0,
}, },
}, },
Solidworks: { Solidworks: {
pan: { pan: {
description: 'Right click + Ctrl + drag', description: 'Right click + Ctrl + drag',
callback: (e) => butName(e).right && e.ctrlKey, callback: (e) => e.button === 2 && e.ctrlKey,
lenientDragStartButton: 2, lenientDragStartButton: 2,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Shift + drag', description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => butName(e).middle && e.shiftKey, dragCallback: (e) => e.button === 1 && e.shiftKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
}, },
NX: { NX: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => e.button === 1 && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => butName(e).middle && e.ctrlKey, dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
}, },
Creo: { Creo: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => e.button === 1 && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => butName(e).middle && e.ctrlKey, dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
}, },
AutoCAD: { AutoCAD: {
pan: { pan: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => e.button === 1 && noModifiersPressed(e),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -156,7 +150,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => e.button === 1 && e.shiftKey,
}, },
}, },
} }

View File

@ -28,8 +28,7 @@ export const homeCommandBarConfig: CommandSetConfig<
name: { name: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: [], options: (context) =>
optionsFromContext: (context) =>
context.projects.map((p) => ({ context.projects.map((p) => ({
name: p.name!, name: p.name!,
value: p.name!, value: p.name!,
@ -44,7 +43,7 @@ export const homeCommandBarConfig: CommandSetConfig<
name: { name: {
inputType: 'string', inputType: 'string',
required: true, required: true,
defaultValueFromContext: (context) => context.defaultProjectName, defaultValue: (context) => context.defaultProjectName,
}, },
}, },
}, },
@ -56,8 +55,7 @@ export const homeCommandBarConfig: CommandSetConfig<
name: { name: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: [], options: (context) =>
optionsFromContext: (context) =>
context.projects.map((p) => ({ context.projects.map((p) => ({
name: p.name!, name: p.name!,
value: p.name!, value: p.name!,
@ -73,8 +71,7 @@ export const homeCommandBarConfig: CommandSetConfig<
oldName: { oldName: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: [], options: (context) =>
optionsFromContext: (context) =>
context.projects.map((p) => ({ context.projects.map((p) => ({
name: p.name!, name: p.name!,
value: p.name!, value: p.name!,
@ -83,7 +80,7 @@ export const homeCommandBarConfig: CommandSetConfig<
newName: { newName: {
inputType: 'string', inputType: 'string',
required: true, required: true,
defaultValueFromContext: (context) => context.defaultProjectName, defaultValue: (context) => context.defaultProjectName,
}, },
}, },
}, },

View File

@ -1,13 +1,7 @@
import { Models } from '@kittycad/lib' import { CommandSetConfig } from 'lib/commandTypes'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
type StorageUnion = ExtractStorageTypes<OutputFormat>
export const EXTRUSION_RESULTS = [ export const EXTRUSION_RESULTS = [
'new', 'new',
'add', 'add',
@ -17,14 +11,10 @@ export const EXTRUSION_RESULTS = [
export type ModelingCommandSchema = { export type ModelingCommandSchema = {
'Enter sketch': {} 'Enter sketch': {}
Export: {
type: OutputTypeKey
storage?: StorageUnion
}
Extrude: { Extrude: {
selection: Selections // & { type: 'face' } would be cool to lock that down selection: Selections // & { type: 'face' } would be cool to lock that down
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: number
} }
} }
@ -36,80 +26,6 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.', description: 'Enter sketch mode.',
icon: 'sketch', icon: 'sketch',
}, },
Export: {
description: 'Export the current model.',
icon: 'exportFile',
needsReview: true,
args: {
type: {
inputType: 'options',
defaultValue: 'gltf',
required: true,
options: [
{ name: 'gLTF', isCurrent: true, value: 'gltf' },
{ name: 'OBJ', isCurrent: false, value: 'obj' },
{ name: 'STL', isCurrent: false, value: 'stl' },
{ name: 'STEP', isCurrent: false, value: 'step' },
{ name: 'PLY', isCurrent: false, value: 'ply' },
],
},
storage: {
inputType: 'options',
defaultValue: (c) => {
switch (c.argumentsToSubmit.type) {
case 'gltf':
return 'embedded'
case 'stl':
return 'ascii'
case 'ply':
return 'ascii'
default:
return undefined
}
},
skip: true,
required: (commandContext) =>
['gltf', 'stl', 'ply'].includes(
commandContext.argumentsToSubmit.type as string
),
options: (commandContext) => {
const type = commandContext.argumentsToSubmit.type as
| OutputTypeKey
| undefined
switch (type) {
case 'gltf':
return [
{ name: 'embedded', isCurrent: true, value: 'embedded' },
{ name: 'binary', isCurrent: false, value: 'binary' },
{ name: 'standard', isCurrent: false, value: 'standard' },
]
case 'stl':
return [
{ name: 'binary', isCurrent: false, value: 'binary' },
{ name: 'ascii', isCurrent: true, value: 'ascii' },
]
case 'ply':
return [
{ name: 'ascii', isCurrent: true, value: 'ascii' },
{
name: 'binary_big_endian',
isCurrent: false,
value: 'binary_big_endian',
},
{
name: 'binary_little_endian',
isCurrent: false,
value: 'binary_little_endian',
},
]
default:
return []
}
},
},
},
},
Extrude: { Extrude: {
description: 'Pull a sketch into 3D along its normal or perpendicular.', description: 'Pull a sketch into 3D along its normal or perpendicular.',
icon: 'extrude', icon: 'extrude',
@ -134,8 +50,8 @@ export const modelingMachineConfig: CommandSetConfig<
// })), // })),
// }, // },
distance: { distance: {
inputType: 'kcl', inputType: 'number',
defaultValue: '5 + 7', defaultValue: 5,
required: true, required: true,
}, },
}, },

View File

@ -41,9 +41,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
baseUnit: { baseUnit: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValueFromContext: (context) => context.baseUnit, defaultValue: (context) => context.baseUnit,
options: [], options: (context) =>
optionsFromContext: (context) =>
Object.values(baseUnitsUnion).map((v) => ({ Object.values(baseUnitsUnion).map((v) => ({
name: v, name: v,
value: v, value: v,
@ -58,9 +57,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
cameraControls: { cameraControls: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValueFromContext: (context) => context.cameraControls, defaultValue: (context) => context.cameraControls,
options: [], options: (context) =>
optionsFromContext: (context) =>
Object.values(cameraSystems).map((v) => ({ Object.values(cameraSystems).map((v) => ({
name: v, name: v,
value: v, value: v,
@ -76,7 +74,7 @@ export const settingsCommandBarConfig: CommandSetConfig<
defaultProjectName: { defaultProjectName: {
inputType: 'string', inputType: 'string',
required: true, required: true,
defaultValueFromContext: (context) => context.defaultProjectName, defaultValue: (context) => context.defaultProjectName,
}, },
}, },
}, },
@ -86,9 +84,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
textWrapping: { textWrapping: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValueFromContext: (context) => context.textWrapping, defaultValue: (context) => context.textWrapping,
options: [], options: (context) => [
optionsFromContext: (context) => [
{ {
name: 'On', name: 'On',
value: 'On' as Toggle, value: 'On' as Toggle,
@ -109,9 +106,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
theme: { theme: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValueFromContext: (context) => context.theme, defaultValue: (context) => context.theme,
options: [], options: (context) =>
optionsFromContext: (context) =>
Object.values(Themes).map((v) => ({ Object.values(Themes).map((v) => ({
name: v, name: v,
value: v, value: v,
@ -126,9 +122,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
unitSystem: { unitSystem: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValueFromContext: (context) => context.unitSystem, defaultValue: (context) => context.unitSystem,
options: [], options: (context) => [
optionsFromContext: (context) => [
{ {
name: 'Imperial', name: 'Imperial',
value: 'imperial' as UnitSystem, value: 'imperial' as UnitSystem,

View File

@ -7,24 +7,10 @@ import {
InterpreterFrom, InterpreterFrom,
} from 'xstate' } from 'xstate'
import { Selection } from './selections' import { Selection } from './selections'
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine'
type Icon = CustomIconName type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const const PLATFORMS = ['both', 'web', 'desktop'] as const
const INPUT_TYPES = ['options', 'string', 'kcl', 'selection'] as const const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
export interface KclExpression {
valueAst: Value
valueText: string
valueCalculated: string
}
export interface KclExpressionWithVariable extends KclExpression {
variableName: string
variableDeclarationAst: VariableDeclaration
variableIdentifierAst: Identifier
insertIndex: number
}
export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = (typeof INPUT_TYPES)[number] export type CommandInputType = (typeof INPUT_TYPES)[number]
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{ export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
@ -94,47 +80,22 @@ export type CommandArgumentConfig<
> = > =
| { | {
description?: string description?: string
required: required: boolean
| boolean skip?: true
| (( defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
) => boolean)
skip?: boolean
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>
options: options:
| CommandArgumentOption<OutputType>[] | CommandArgumentOption<OutputType>[]
| (( | ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
commandBarContext: {
argumentsToSubmit: Record<string, unknown>
} // Should be the commandbarMachine's context, but it creates a circular dependency
) => CommandArgumentOption<OutputType>[])
optionsFromContext?: (
context: ContextFrom<T>
) => CommandArgumentOption<OutputType>[]
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
} }
| { | {
inputType: Extract<CommandInputType, 'selection'> inputType: Extract<CommandInputType, 'selection'>
selectionTypes: Selection['type'][] selectionTypes: Selection['type'][]
multiple: boolean multiple: boolean
} }
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values | { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
| {
inputType: Extract<CommandInputType, 'string'>
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
}
) )
export type CommandArgument< export type CommandArgument<
@ -143,43 +104,21 @@ export type CommandArgument<
> = > =
| { | {
description?: string description?: string
required: required: boolean
| boolean skip?: true
| (( defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
) => boolean)
skip?: boolean
machineActor: InterpreterFrom<T>
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>
options: options: CommandArgumentOption<OutputType>[]
| CommandArgumentOption<OutputType>[]
| ((
commandBarContext: {
argumentsToSubmit: Record<string, unknown>
} // Should be the commandbarMachine's context, but it creates a circular dependency
) => CommandArgumentOption<OutputType>[])
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
} }
| { | {
inputType: Extract<CommandInputType, 'selection'> inputType: Extract<CommandInputType, 'selection'>
selectionTypes: Selection['type'][] selectionTypes: Selection['type'][]
actor: InterpreterFrom<T>
multiple: boolean multiple: boolean
} }
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values | { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
| {
inputType: Extract<CommandInputType, 'string'>
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
}
) )
export type CommandArgumentWithName< export type CommandArgumentWithName<

View File

@ -1,15 +0,0 @@
// Some command argument payloads are objects with a value field that is a KCL expression.
// That object also contains some metadata about what to do with the KCL expression,
// such as whether we need to create a new variable for it.
// This function extracts the value field from those arg payloads and returns
// The arg object with all its field as natural values that the command to be executed will expect.
export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) {
return Object.fromEntries(
Object.entries(args).map(([key, value]) => {
if (value !== null && typeof value === 'object' && 'value' in value) {
return [key, value.value]
}
return [key, value]
})
)
}

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