Client sketch scene (#1271)

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make tsc happ

* better error msg

* fix control point issue

* basic code gen working for tangentialArc

* partical fix for move with arcs

* tangential arc move

* fix

* make eslint rules less annoying

* inital refactor of some xstate stuff

* more old tangential arc clean up stuff

* more tweaks

* add testing

* tweak xstate inspect

* temp remove test

* update formating for less conflicts

* fix state machine layout after merge

* shrug, something weird with xstate typegen

* renaming some xstate events

* tweak numbers to make CI playwright happ

* CI hacks

* more CI hacks

* more CI hacks

* new hack strategy

* run tests agian

* make cmd bar less flaky

* ci hacks

* CI hacks

* CI hacks

* CI hacks

* clean up

* fix

* still have constraint stuff to deal with

* progress on move rules

* update source ranges after no execute code-mod

* typo

* mvp working

* hide show sketch overlay

* match scaling

* update arrow head style

* animate line tool

* bypass xstate for animations, much smoother

* add new segment working with refactor needed for setup paper sketch

* refactor setup paper sketch

* tangantialArcTo drag animations working

* tangential arc polish

* cargo fmt

* clippy

* more clippy

* mock canvas

* last of clippy?

* typo

* more clippy stuff

* move util function so they are shareable with typescript

* migrate a bunch to rust and only rust

* add arc center point for draft tangential ac

* clippy tweak

* delete uneeded test

* Rough start to scaling arrow heads.

The tangent arrow heads are basically nuked and replaced while the
straight line sections are just rotated and repositioned, this means they
miss out on updating scaling number after a screen size changes.
Needs fixing

* fix bug with tool tips

* fix draft line start position

Having drag the end of teh path before selecting a tooltip would result in the draft line starting where the path used to end, stale data

* some progress with pan maybe

* fmt

* inital camera sync working

For perspective camera at least

* change three.js to use z-up

* add grid

* orthographic camera working with polish items TODO

* fix zoom level when swapping camera

* fix up camera/orbit changing on cam change (pan wasn't being respected)

* tidy up

* use orbit target instead of assuming scene center

* dynamic fov working

* animate orthographic to perspective and reverse

* fix import

* temp fix for batch commands

* initial client side scene sketch working

* remove hover log

* FOV adjust fix

* fix comment

* tear down sketch and small tweaks

* some progress with camera tweening

* combine dollyZoom engine commands

see
https://github.com/KittyCAD/modeling-api/compare/kurt-perspective-settings?expand=1
and
https://github.com/KittyCAD/engine/compare/kurt-perspective-settings?expand=1

* make tests happy (mocks)

* fix tween to vertical/camera-up bug

* tween to each axis with hacky solutions in there

* fix startSketchOn planes

* tidy startSketchOn

* tweening okay for now I think

* get sketching on default planes working

* allow editing on all default planes

* clean up enter and exit sketch logic

* tidy

* tidy

* remove more default plane stuff

* start of draft line

* remove some annoying parts of the paper.js implementation

* fix drag than equip line bug

* comment

* don't animate on skech tear down since it's used for draft line

* remove more default plane shit

* style draft line

* refine dashed line

* draft line set up and tear down mostly happy

* add on click logic ready for draft lines

* sketch mode with drag and draft mode working solidly now, straight segments only

* default planes match colors, hover and select still TODO

* hover and click logic working for default planes

Now just need the code mode to fire to 'startSketchOn(...)'

* select default planes

* remove some logs

* fix update infinite loop

* start of orbitControls port to Franks control guards

* hiding scenes at different times

* scene hide on camera move should be respected by scroll zoom

* basic hover working

* Hook up user camera settings to ClientSideScene (#1334)

* Refactor to not import utilities from Router.tsx

* Stop tracking changes or formatting *.typegen.ts

* Hook up cameraControls to ClientSideScene

* Remove camera controls toggle from temp debug panel

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>

* add select segment moves cursor

* highlight segments yellow on hover

* cursor ranges effect 2d line colors

* fix constrainst i.e. make sure the sketch is rejiged

* selecting nothing should remove selections

* remove hardcoded strings

* update get_tangential_arc_to_info rust util

* initial drawing of tangential arcs in client scene

* fix tangentialArc arrow head direction

* correct userData types for tangential arcs

* get tangential arc updates working

Doesn't include draging the head of the tangential arc itself yet

* spot of clean up

* make selections work with tangential arcs

* get draft tangential segment animated

* fix initial click weirdness for adding new tangential line

* couple tweaks

* add grace pixels /threshold to raycast

* redo arc dashes so that they spawn from the ccenter of the arc

* fix multi drag bug

* fmt

* add temp solution for close

* add default axis hover colors, still needs select logic

* selection of axis works, just with out selection color

* get axis selection colors working

* fix outdate source ranges after drag problem

* update moreNodePathFromSourceRange

* fix ts-rs issue/workaround

* fix default plane weirdness

* fix tangential arc rounding issue

* review clean up part 1

* review clean up part 2

Big state-diagram cull

* clippy

* typo

* clippy

* fix xstate types with typegen

* fix types

* clippy

* catch error

* fix test import issue

Not sure exactly what was happening but guessing circular import that vite didn't like

* add axis/plane info to sketch group tests

* case changes because of rs-ts bug, can probably revert this later

* start of playwright test fixes

* reduce geo complexity for straight segments

* fix cam adjust tests

* Revert "Clean up vite build warnings (#1332)"

This reverts commit c1f661ab52.

* selection e2e test fixed<

* remove camToggle to allow playwright tests to pass

* remove drag test

too brittle and needs to be redone from the ground up anyway

* trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* fix last test

* clean up part 3

* clean up part 4

* clean up part 5

* clean up sketch enter exit logic

* fix engine side selections

* default plane should not be selected form 'onDragEnd'
i.e. rotating the camera should not mean the user acidently selects a plane

* clean up state diagram around animating to sketch mode

Embracing that the animation is async and puting the interdiate steps in the state diagram clean up some logic and solved some bugs at the same time

* add test for multiple sketches

* typo

* make highlight more robust

* type tweak

* scale segmenst with distance from camera so they have a consistent pixel size/ screenspace size

* Jess's advice

* tsc and fmt

* clean up part 6

remove integer from xstate names

* clean up part 7

* integrate sequency in to camera moves

* fix tests

* update snapshot e2e

* small snapshot change

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* trigger ci

* Fix HomeLoaderData types

* update std stuff

* update kittycad rs client lib

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kurt Hutten
2024-02-11 12:59:00 +11:00
committed by GitHub
parent 64398381a9
commit f640f7a5e0
131 changed files with 12618 additions and 2652 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime ignore-words-list: crate,everytime,inout
skip: **/target,node_modules,build,**/Cargo.lock skip: **/target,node_modules,build,**/Cargo.lock

View File

@ -1 +1,2 @@
src/wasm-lib/* src/wasm-lib/*
*.typegen.ts

View File

@ -17,12 +17,12 @@
"never" "never"
], ],
"react-hooks/exhaustive-deps": "off", "react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-floating-promises": "warn"
}, },
"overrides": [ "overrides": [
{ {
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "warn",
"testing-library/prefer-screen-queries": "off" "testing-library/prefer-screen-queries": "off"
} }
} }

View File

@ -46,6 +46,7 @@ jobs:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- run: yarn build:wasm - run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc - run: yarn tsc

4
.gitignore vendored
View File

@ -50,3 +50,7 @@ e2e/playwright/export-snapshots/*embedded.gltf
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
## generated files
src/**/*.typegen.ts

View File

@ -10,4 +10,4 @@ src/wasm-lib/kcl/bindings
e2e/playwright/export-snapshots e2e/playwright/export-snapshots
# XState generated files # XState generated files
src/machines/modelingMachine.typegen.ts src/machines/**.typegen.ts

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets' import { secrets } from './secrets'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
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'
@ -53,40 +51,38 @@ test('Basic sketch', async ({ page }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
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 Promise.all([ await u.doAndWaitForImageDiff(
u.doAndWaitForImageDiff( () => page.getByRole('button', { name: 'Start Sketch' }).click(),
() => page.getByRole('button', { name: 'Start Sketch' }).click(), 200
200 )
),
u.waitForDefaultPlanesVisibilityChange(),
])
// select a plane // select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter') await page.mouse.click(700, 200)
await u.waitForCmdReceive('set_tool')
await u.doAndWaitForCmd( await expect(page.locator('.cm-content')).toHaveText(
() => page.getByRole('button', { name: 'Line' }).click(), `const part001 = startSketchOn('-XZ')`
'set_tool'
) )
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600 const startXPx = 600
await u.doAndWaitForCmd( await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10), const startAt = '[23.89, -32.23]'
'mouse_click', await expect(page.locator('.cm-content'))
false .toHaveText(`const part001 = startSketchOn('-XZ')
) |> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100)
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)
const startAt = '[18.26, -24.63]' const num = 24.11
const num = '18.43'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
@ -97,27 +93,22 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num}], %)`) |> line([0, ${num + 0.01}], %)`)
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(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num}], %) |> line([0, ${num + 0.01}], %)
|> line([-36.69, 0], %)`) |> line([-48, 0], %)`)
// deselect line tool // deselect line tool
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Line' }).click()
() => page.getByRole('button', { name: 'Line' }).click(), await page.waitForTimeout(100)
'set_tool'
)
// click between first two clicks to get center of the line // click between first two clicks to get center of the line
await u.doAndWaitForCmd( await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10), await page.waitForTimeout(100)
'select_with_point'
)
await u.closeDebugPanel()
// hold down shift // hold down shift
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
@ -133,7 +124,7 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line({ to: [${num}, 0], tag: 'seg01' }, %) |> line({ to: [${num}, 0], tag: 'seg01' }, %)
|> line([0, ${num}], %) |> line([0, ${num + 0.01}], %)
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
@ -271,59 +262,41 @@ test('Can create sketches on all planes and their back sides', async ({
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const camCmd: EngineCommand = { const camPos: [number, number, number] = [100, 100, 100]
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 15, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: 30, y: 30, z: 30 },
},
}
const TestSinglePlane = async ({ const TestSinglePlane = async ({
viewCmd, viewCmd,
expectedCode, expectedCode,
clickCoords, clickCoords,
}: { }: {
viewCmd: EngineCommand viewCmd: [number, number, number]
expectedCode: string expectedCode: string
clickCoords: { x: number; y: number } clickCoords: { x: number; y: number }
}) => { }) => {
await u.openDebugPanel() await u.openDebugPanel()
await u.sendCustomCmd(viewCmd)
await u.updateCamPosition(viewCmd)
await u.clearCommandLogs() await u.clearCommandLogs()
// await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y) await page.mouse.click(clickCoords.x, clickCoords.y)
await u.openDebugPanel() await page.waitForTimeout(300) // wait for animation
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible() await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
// draw a line // draw a line
const startXPx = 600 const startXPx = 600
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Line' }).click()
await u.waitForCmdReceive('set_tool')
await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await u.openDebugPanel()
await u.waitForCmdReceive('mouse_click')
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await u.openDebugPanel()
await expect(page.locator('.cm-content')).toHaveText(expectedCode) await expect(page.locator('.cm-content')).toHaveText(expectedCode)
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
await u.clearCommandLogs() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -333,51 +306,40 @@ test('Can create sketches on all planes and their back sides', async ({
const codeTemplate = ( const codeTemplate = (
plane = 'XY', plane = 'XY',
sign = '' rounded = false
) => `const part001 = startSketchOn('${plane}') ) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([${sign}6.88, -9.29], %) |> startProfileAt([28.91, -39${rounded ? '' : '.01'}], %)`
|> line([${sign}6.95, 0], %)`
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmd, viewCmd: camPos,
expectedCode: codeTemplate('XY'), expectedCode: codeTemplate('XY'),
clickCoords: { x: 700, y: 350 }, // red plane clickCoords: { x: 600, y: 388 }, // red plane
// clickCoords: { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmd, viewCmd: camPos,
expectedCode: codeTemplate('YZ'), expectedCode: codeTemplate('YZ', true),
clickCoords: { x: 1000, y: 200 }, // green plane clickCoords: { x: 700, y: 300 }, // green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmd, viewCmd: camPos,
expectedCode: codeTemplate('XZ', '-'), expectedCode: codeTemplate('XZ'),
clickCoords: { x: 630, y: 130 }, // blue plane clickCoords: { x: 700, y: 80 }, // blue plane
}) })
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
// new camera angle to click the back side of all three planes
const camCmdBackSide: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: -15, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: -30, y: -30, z: -30 },
},
}
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY', '-'), expectedCode: codeTemplate('-XY', true),
clickCoords: { x: 705, y: 136 }, // back of red plane clickCoords: { x: 601, y: 118 }, // back of red plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-YZ', '-'), expectedCode: codeTemplate('-YZ'),
clickCoords: { x: 1000, y: 350 }, // back of green plane clickCoords: { x: 730, y: 219 }, // back of green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ'), expectedCode: codeTemplate('-XZ', true),
clickCoords: { x: 600, y: 400 }, // back of blue plane clickCoords: { x: 680, y: 427 }, // back of blue plane
}) })
}) })
@ -387,7 +349,6 @@ test('Auto complete works', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.waitForDefaultPlanesVisibilityChange()
// this test might be brittle as we add and remove functions // this test might be brittle as we add and remove functions
// but should also be easy to update. // but should also be easy to update.
@ -478,38 +439,36 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const xAxisClick = () => page.mouse.click(700, 250) const xAxisClick = () =>
const emptySpaceClick = () => page.mouse.click(700, 300) page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () => page.mouse.click(700, 285) const emptySpaceClick = () =>
const bottomHorzSegmentClick = () => page.mouse.click(750, 393) page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () =>
page.mouse.click(709, 289).then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () =>
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
// select a plane // select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter') await page.mouse.click(700, 200)
await u.waitForCmdReceive('set_tool') await page.waitForTimeout(700) // wait for animation
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
const startXPx = 600 const startXPx = 600
await u.doAndWaitForCmd( await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10), const startAt = '[23.89, -32.23]'
'mouse_click', await expect(page.locator('.cm-content'))
false .toHaveText(`const part001 = startSketchOn('-XZ')
) |> startProfileAt(${startAt}, %)`)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const startAt = '[18.26, -24.63]' const num = 24.11
const num = '18.43' const num2 = '48'
const num2 = '36.69'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
@ -520,20 +479,17 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num}], %)`) |> line([0, ${num + 0.01}], %)`)
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(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num}], %) |> line([0, ${num + 0.01}], %)
|> line([-${num2}, 0], %)`) |> line([-${num2}, 0], %)`)
// deselect line tool // deselect line tool
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Line' }).click()
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await u.closeDebugPanel() await u.closeDebugPanel()
const selectionSequence = async () => { const selectionSequence = async () => {
@ -555,79 +511,72 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// now check clicking works including axis // now check clicking works including axis
// click a segment hold shift and click an axis, see that a relevant constraint is enabled // click a segment hold shift and click an axis, see that a relevant constraint is enabled
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false) await topHorzSegmentClick()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
const absYButton = page.getByRole('button', { name: 'ABS Y' }) const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false) await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await absYButton.and(page.locator(':not([disabled])')).waitFor() await absYButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) await emptySpaceClick()
// same selection but click the axis first // same selection but click the axis first
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false) await xAxisClick()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false) await topHorzSegmentClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) 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 u.doAndWaitForCmd( await page.getByText(` |> line([-${num2}, 0], %)`).click()
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
'select_clear',
false
)
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false) await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) 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 u.doAndWaitForCmd( await page.getByText(` |> line([-${num2}, 0], %)`).click()
() => page.getByText(` |> line([-${num2}, 0], %)`).click(), await page.waitForTimeout(300)
'select_clear',
false
)
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)
await u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one await bottomHorzSegmentClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
// clear selection by clicking on nothing // clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) await emptySpaceClick()
} }
await selectionSequence() await selectionSequence()
// hovering in fresh sketch worked, lets try exiting and re-entering // hovering in fresh sketch worked, lets try exiting and re-entering
await u.doAndWaitForCmd( await u.openAndClearDebugPanel()
() => page.getByRole('button', { name: 'Exit Sketch' }).click(), await page.getByRole('button', { name: 'Exit Sketch' }).click()
'edit_mode_exit' await page.waitForTimeout(200)
)
// wait for execution done // wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// select a line // select a line
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false) await topHorzSegmentClick()
await page.waitForTimeout(200)
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Start Sketch' }).click()
() => page.getByRole('button', { name: 'Start Sketch' }).click(), await page.waitForTimeout(700) // wait for animation
'edit_mode_enter',
false
)
// hover again and check it works // hover again and check it works
await selectionSequence() await selectionSequence()
@ -697,6 +646,8 @@ test('Can extrude from the command bar', async ({ page, context }) => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
@ -735,3 +686,127 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|> extrude(5, %)` |> extrude(5, %)`
) )
}) })
test('Can add multiple sketches', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(page.getByRole('button', { name: 'Start Sketch' })).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(500) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)
|> line([-48, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
// exit the sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([0, 100, 100])
// start a new sketch
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await page.mouse.click(673, 384)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await u.clearAndCloseDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[23.61, -31.85]'
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num2 = 23.83
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '')
)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${num2}], %)`.replace(/\s/g, '')
)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${num2}], %)
|> line([-47.44, 0], %)`.replace(/\s/g, '')
)
})

View File

@ -40,19 +40,8 @@ test('change camera, show planes', async ({ page, context }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
const camCmd: EngineCommand = { const camPos: [number, number, number] = [0, 85, 85]
type: 'modeling_cmd_req', await u.updateCamPosition(camPos)
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: 0, y: 85, z: 85 },
},
}
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
// rotate // rotate
await u.closeDebugPanel() await u.closeDebugPanel()
@ -62,13 +51,11 @@ test('change camera, show planes', async ({ page, context }) => {
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(500) await page.waitForTimeout(500)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -77,10 +64,8 @@ test('change camera, show planes', async ({ page, context }) => {
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.sendCustomCmd(camCmd) await u.updateCamPosition(camPos)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
@ -93,12 +78,10 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300) await page.waitForTimeout(300)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -107,10 +90,8 @@ test('change camera, show planes', async ({ page, context }) => {
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.sendCustomCmd(camCmd) await u.updateCamPosition(camPos)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
@ -119,17 +100,15 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.mouse.move(700, 400) await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' }) await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 350) await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control') await page.keyboard.up('Control')
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300) await page.waitForTimeout(300)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -191,7 +170,6 @@ const part001 = startSketchOn('-XZ')
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -80,10 +80,21 @@ export function getUtils(page: Page) {
waitForAuthSkipAppStart: () => waitForPageLoad(page), waitForAuthSkipAppStart: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page), removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => {
const fillInput = async () => {
await page.fill('[data-testid="cam-x-position"]', String(xyz[0]))
await page.fill('[data-testid="cam-y-position"]', String(xyz[1]))
await page.fill('[data-testid="cam-z-position"]', String(xyz[2]))
}
await fillInput()
await page.waitForTimeout(100)
await fillInput()
await page.waitForTimeout(100)
await fillInput()
await page.waitForTimeout(100)
},
clearCommandLogs: () => clearCommandLogs(page), clearCommandLogs: () => clearCommandLogs(page),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr), expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
waitForDefaultPlanesVisibilityChange: () =>
waitForDefaultPlanesToBeVisible(page),
openDebugPanel: () => openDebugPanel(page), openDebugPanel: () => openDebugPanel(page),
closeDebugPanel: () => closeDebugPanel(page), closeDebugPanel: () => closeDebugPanel(page),
openAndClearDebugPanel: async () => { openAndClearDebugPanel: async () => {

View File

@ -10,7 +10,7 @@
"@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.46", "@kittycad/lib": "^0.0.50",
"@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",
@ -21,6 +21,7 @@
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@types/node": "^16.7.13", "@types/node": "^16.7.13",
"@types/react": "^18.2.41", "@types/react": "^18.2.41",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
@ -45,6 +46,7 @@
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"swr": "^2.2.2", "swr": "^2.2.2",
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1", "tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
"three": "^0.160.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
@ -82,7 +84,9 @@
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", "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": "patch-package && yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
}, },
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
@ -113,6 +117,7 @@
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.160.0",
"@types/uuid": "^9.0.4", "@types/uuid": "^9.0.4",
"@types/wait-on": "^5.3.4", "@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2020.9.6", "@types/wicg-file-system-access": "^2020.9.6",
@ -124,15 +129,18 @@
"@wdio/local-runner": "^8.24.3", "@wdio/local-runner": "^8.24.3",
"@wdio/mocha-framework": "^8.24.3", "@wdio/mocha-framework": "^8.24.3",
"@wdio/spec-reporter": "^8.24.2", "@wdio/spec-reporter": "^8.24.2",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"eslint": "^8.53.0", "eslint": "^8.53.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"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",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
@ -140,6 +148,7 @@
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.2.1", "vite-tsconfig-paths": "^4.2.1",
"vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0", "wait-on": "^7.2.0",
"yarn": "^1.22.19" "yarn": "^1.22.19"
} }

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 ) {

99
src-tauri/Cargo.lock generated
View File

@ -82,6 +82,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-driver",
"tauri-plugin-fs-extra", "tauri-plugin-fs-extra",
"tokio", "tokio",
"toml 0.8.2", "toml 0.8.2",
@ -1307,6 +1308,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.25.2" version = "0.25.2"
@ -2538,6 +2548,12 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "pico-args"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.3" version = "1.1.3"
@ -3419,6 +3435,37 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "signal-hook-tokio"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e"
dependencies = [
"futures-core",
"libc",
"signal-hook",
"tokio",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.5" version = "0.3.5"
@ -3855,6 +3902,25 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-driver"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0a46011e3018146c2a189bf1fa0339f10ef6be065ab8a5e718018121e3a03bf"
dependencies = [
"anyhow",
"futures",
"futures-util",
"hyper",
"pico-args",
"serde",
"serde_json",
"signal-hook",
"signal-hook-tokio",
"tokio",
"which",
]
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "1.4.2" version = "1.4.2"
@ -4062,9 +4128,21 @@ dependencies = [
"num_cpus", "num_cpus",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.5", "socket2 0.5.5",
"tokio-macros",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "tokio-macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-native-tls"
version = "0.3.1" version = "0.3.1"
@ -4617,6 +4695,18 @@ dependencies = [
"windows-metadata", "windows-metadata",
] ]
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.21",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -4743,6 +4833,15 @@ dependencies = [
"windows-targets 0.48.0", "windows-targets 0.48.0",
] ]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.0",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.42.2" version = "0.42.2"

View File

@ -1,73 +0,0 @@
import { render, screen } from '@testing-library/react'
import { App } from './App'
import { describe, test, vi } from 'vitest'
import {
Route,
RouterProvider,
createMemoryRouter,
createRoutesFromElements,
} from 'react-router-dom'
import { GlobalStateProvider } from './components/GlobalStateProvider'
import CommandBarProvider from 'components/CommandBar/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { BROWSER_FILE_NAME } from 'Router'
let listener: ((rect: any) => void) | undefined = undefined
;(global as any).ResizeObserver = class ResizeObserver {
constructor(ls: ((rect: any) => void) | undefined) {
listener = ls
}
observe() {}
unobserve() {}
disconnect() {}
}
describe('App tests', () => {
test('Renders the modeling app screen, including "Variables" pane.', () => {
vi.mock('react-router-dom', async () => {
const actual = (await vi.importActual('react-router-dom')) as Record<
string,
any
>
return {
...actual,
useParams: () => ({ id: BROWSER_FILE_NAME }),
useLoaderData: () => ({ code: null }),
}
})
render(
<TestWrap>
<App />
</TestWrap>
)
const linkElement = screen.getByText(/Variables/i)
expect(linkElement).toBeInTheDocument()
vi.restoreAllMocks()
})
})
function TestWrap({ children }: { children: React.ReactNode }) {
// We have to use a memory router in the testing environment,
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
const router = createMemoryRouter(
createRoutesFromElements(
<Route
path="/file/:id"
element={
<CommandBarProvider>
<GlobalStateProvider>
<ModelingMachineProvider>{children}</ModelingMachineProvider>
</GlobalStateProvider>
</CommandBarProvider>
}
/>
),
{
initialEntries: ['/file/new'],
initialIndex: 0,
}
)
return <RouterProvider router={router} />
}

View File

@ -20,9 +20,9 @@ import {
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { getNormalisedCoordinates } from './lib/utils' import { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { IndexLoaderData } from './Router' import { IndexLoaderData } from 'lib/types'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding' import { onboardingPaths } from 'routes/Onboarding/paths'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu' import { CodeMenu } from 'components/CodeMenu'
@ -31,6 +31,8 @@ import { Themes, getSystemTheme } from 'lib/theme'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from './lang/std/engineConnection' import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { ClientSideScene } from 'clientSideScene/setup'
// import { CamToggle } from 'components/CamToggle'
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
@ -83,10 +85,12 @@ export function App() {
useEngineConnectionSubscriptions() useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
void engineCommandManager.sendSceneCommand(message) engineCommandManager.sendSceneCommand(message)
}, 16) }, 1000 / 15)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault() if (state.matches('Sketch')) {
return
}
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
@ -97,37 +101,15 @@ export function App() {
const newCmdId = uuidv4() const newCmdId = uuidv4()
if (buttonDownInStream === undefined) { if (buttonDownInStream === undefined) {
if (state.matches('Sketch.Line Tool')) { debounceSocketSend({
debounceSocketSend({ type: 'modeling_cmd_req',
type: 'modeling_cmd_req', cmd: {
cmd_id: newCmdId, type: 'highlight_set_entity',
cmd: { selected_at_window: { x, y },
type: 'mouse_move', },
window: { x, y }, cmd_id: newCmdId,
}, })
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
} else { } else {
if (state.matches('Sketch.Move Tool')) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
return
}
const interactionGuards = cameraMouseDragGuards[cameraControls] const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type let interaction: CameraDragInteractionType_type
@ -238,6 +220,7 @@ export function App() {
open={openPanes.includes('debug')} open={openPanes.includes('debug')}
/> />
)} )}
{/* <CamToggle /> */}
</div> </div>
) )
} }

View File

@ -14,10 +14,7 @@ import {
import { useEffect } from 'react' 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, { import Onboarding, { onboardingRoutes } from './routes/Onboarding'
onboardingRoutes,
onboardingPaths,
} from './routes/Onboarding'
import SignIn from './routes/SignIn' import SignIn from './routes/SignIn'
import { Auth } from './Auth' import { Auth } from './Auth'
import { isTauri } from './lib/isTauri' import { isTauri } from './lib/isTauri'
@ -29,7 +26,7 @@ import {
isProjectDirectory, isProjectDirectory,
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
} from './lib/tauriFS' } from './lib/tauriFS'
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api' import { metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner' import DownloadAppBanner from './components/DownloadAppBanner'
import { WasmErrBanner } from './components/WasmErrBanner' import { WasmErrBanner } from './components/WasmErrBanner'
import { GlobalStateProvider } from './components/GlobalStateProvider' import { GlobalStateProvider } from './components/GlobalStateProvider'
@ -42,9 +39,11 @@ import CommandBarProvider from 'components/CommandBar/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env' import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider' import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSinglton' import { KclContextProvider, kclManager } from 'lang/KclSingleton'
import FileMachineProvider from 'components/FileMachineProvider' import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
if (VITE_KC_SENTRY_DSN && !TEST) { if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({ Sentry.init({
@ -78,43 +77,8 @@ if (VITE_KC_SENTRY_DSN && !TEST) {
}) })
} }
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
return Object.fromEntries(
Object.entries(routesObject).map(([constName, path]) => [
constName,
prepend + path,
])
)
}
export const paths = {
INDEX: '/',
HOME: '/home',
FILE: '/file',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding'
) as typeof onboardingPaths,
}
export const BROWSER_FILE_NAME = 'new' export const BROWSER_FILE_NAME = 'new'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
file?: FileEntry
}
export type ProjectWithEntryPointMetadata = FileEntry & {
entrypointMetadata: Metadata
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0] type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
const addGlobalContextToElements = ( const addGlobalContextToElements = (
@ -146,18 +110,18 @@ const router = createBrowserRouter(
{ {
path: paths.FILE + '/:id', path: paths.FILE + '/:id',
element: ( element: (
<Auth> <KclContextProvider>
<FileMachineProvider> <Auth>
<KclContextProvider> <FileMachineProvider>
<ModelingMachineProvider> <ModelingMachineProvider>
<Outlet /> <Outlet />
<App /> <App />
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner /> <WasmErrBanner />
</KclContextProvider> </FileMachineProvider>
</FileMachineProvider> {!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />} </Auth>
</Auth> </KclContextProvider>
), ),
id: paths.FILE, id: paths.FILE,
loader: async ({ loader: async ({
@ -249,7 +213,7 @@ const router = createBrowserRouter(
<Home /> <Home />
</Auth> </Auth>
), ),
loader: async () => { loader: async (): Promise<HomeLoaderData | Response> => {
if (!isTauri()) { if (!isTauri()) {
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME) return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
} }

View File

@ -89,44 +89,48 @@ export const Toolbar = () => {
</li> </li>
)} )}
{state.matches('Sketch') && !state.matches('idle') && ( {state.matches('Sketch') && !state.matches('idle') && (
<li className="contents"> <>
<ActionButton <li className="contents" key="line-button">
Element="button" <ActionButton
onClick={() => Element="button"
state.matches('Sketch.Line Tool') onClick={() =>
? send('CancelSketch') state?.matches('Sketch.Line tool')
: send('Equip tool') ? send('CancelSketch')
} : send('Equip Line tool')
aria-pressed={state.matches('Sketch.Line Tool')} }
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80" aria-pressed={state?.matches('Sketch.Line tool')}
icon={{ className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon: 'line', icon={{
bgClassName, icon: 'line',
}} bgClassName,
> }}
Line >
</ActionButton> Line
</li> </ActionButton>
)} </li>
{state.matches('Sketch') && ( <li className="contents" key="tangential-arc-button">
<li className="contents"> <ActionButton
<ActionButton Element="button"
Element="button" onClick={() =>
onClick={() => state.matches('Sketch.Tangential arc to')
state.matches('Sketch.Move Tool') ? send('CancelSketch')
? send('CancelSketch') : send('Equip tangential arc to')
: send('Equip move tool') }
} aria-pressed={state.matches('Sketch.Tangential arc to')}
aria-pressed={state.matches('Sketch.Move Tool')} className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80" icon={{
icon={{ icon: 'line',
icon: 'move', bgClassName,
bgClassName, }}
}} disabled={
> !state.can('Equip tangential arc to') &&
Move !state.matches('Sketch.Tangential arc to')
</ActionButton> }
</li> >
Tangential Arc
</ActionButton>
</li>
</>
)} )}
{state.matches('Sketch.SketchIdle') && {state.matches('Sketch.SketchIdle') &&
state.nextEvents state.nextEvents
@ -151,7 +155,7 @@ export const Toolbar = () => {
return 0 return 0
}) })
.map((eventName) => ( .map((eventName) => (
<li className="contents"> <li className="contents" key={eventName}>
<ActionButton <ActionButton
Element="button" Element="button"
className="text-sm" className="text-sm"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
import {
GridHelper,
LineBasicMaterial,
OrthographicCamera,
PerspectiveCamera,
Group,
Mesh,
} from 'three'
export function createGridHelper({
size,
divisions,
}: {
size: number
divisions: number
}) {
const gridHelperMaterial = new LineBasicMaterial({
color: 0xaaaaaa,
transparent: true,
opacity: 0.5,
depthTest: false,
})
const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff)
gridHelper.material = gridHelperMaterial
gridHelper.rotation.x = Math.PI / 2
return gridHelper
}
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
0.55 / cam.zoom
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov) / 4000

View File

@ -0,0 +1,358 @@
import { Coords2d } from 'lang/std/sketch'
import {
BufferGeometry,
CatmullRomCurve3,
ConeGeometry,
CurvePath,
EllipseCurve,
ExtrudeGeometry,
Group,
LineCurve3,
Mesh,
MeshBasicMaterial,
NormalBufferAttributes,
Shape,
SphereGeometry,
Vector2,
Vector3,
} from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import {
STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH,
TANGENTIAL_ARC_TO_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT_BODY,
TANGENTIAL_ARC_TO__SEGMENT_DASH,
} from './clientSideScene'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './setup'
export function straightSegment({
from,
to,
id,
pathToNode,
isDraftSegment,
scale = 1,
}: {
from: Coords2d
to: Coords2d
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
}): Group {
const group = new Group()
const shape = new Shape()
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
let geometry
if (isDraftSegment) {
geometry = dashedStraight(from, to, shape, scale)
} else {
const line = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)
)
geometry = new ExtrudeGeometry(shape, {
steps: 2,
bevelEnabled: false,
extrudePath: line,
})
}
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment
? STRAIGHT_SEGMENT_DASH
: STRAIGHT_SEGMENT_BODY
mesh.name = STRAIGHT_SEGMENT_BODY
group.userData = {
type: STRAIGHT_SEGMENT,
id,
from,
to,
pathToNode,
isSelected: false,
}
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3()
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
group.add(mesh, arrowGroup)
return group
}
function createArrowhead(scale = 1): Group {
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
arrowheadMesh.position.set(0, -0.6, 0)
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
const arrowGroup = new Group()
arrowGroup.userData.type = ARROWHEAD
arrowGroup.name = ARROWHEAD
arrowGroup.add(arrowheadMesh, sphereMesh)
arrowGroup.lookAt(new Vector3(0, 1, 0))
arrowGroup.scale.set(scale, scale, scale)
return arrowGroup
}
export function tangentialArcToSegment({
prevSegment,
from,
to,
id,
pathToNode,
isDraftSegment,
scale = 1,
}: {
prevSegment: SketchGroup['value'][number]
from: Coords2d
to: Coords2d
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
}): Group {
const group = new Group()
const previousPoint =
prevSegment?.type === 'TangentialArcTo'
? getTangentPointFromPreviousArc(
prevSegment.center,
prevSegment.ccw,
prevSegment.to
)
: prevSegment.from
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
arcStartPoint: from,
arcEndPoint: to,
tanPreviousPoint: previousPoint,
obtuse: true,
})
const geometry = createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw,
isDashed: isDraftSegment,
scale,
})
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment
? TANGENTIAL_ARC_TO__SEGMENT_DASH
: TANGENTIAL_ARC_TO_SEGMENT_BODY
group.userData = {
type: TANGENTIAL_ARC_TO_SEGMENT,
id,
from,
to,
prevSegment,
pathToNode,
isSelected: false,
}
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)
const arrowheadAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
group.add(mesh, arrowGroup)
return group
}
export function createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw,
isDashed = false,
scale = 1,
}: {
center: Coords2d
radius: number
startAngle: number
endAngle: number
ccw: boolean
isDashed?: boolean
scale?: number
}): BufferGeometry {
const dashSize = 1.2 * scale
const gapSize = 1.2 * scale
const arcStart = new EllipseCurve(
center[0],
center[1],
radius,
radius,
startAngle,
endAngle,
!ccw,
0
)
const arcEnd = new EllipseCurve(
center[0],
center[1],
radius,
radius,
endAngle,
startAngle,
ccw,
0
)
const shape = new Shape()
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
if (!isDashed) {
const points = arcStart.getPoints(50)
const path = new CurvePath<Vector3>()
path.add(new CatmullRomCurve3(points.map((p) => new Vector3(p.x, p.y, 0))))
return new ExtrudeGeometry(shape, {
steps: 100,
bevelEnabled: false,
extrudePath: path,
})
}
const length = arcStart.getLength()
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
const dashGeometries = []
// Function to create a dash at a specific t value (0 to 1 along the curve)
const createDashAt = (t: number, curve: EllipseCurve) => {
const startVec = curve.getPoint(t)
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
const dashCurve = new CurvePath<Vector3>()
dashCurve.add(
new CatmullRomCurve3([
new Vector3(startVec.x, startVec.y, 0),
new Vector3(midVec.x, midVec.y, 0),
new Vector3(endVec.x, endVec.y, 0),
])
)
return new ExtrudeGeometry(shape, {
steps: 3,
bevelEnabled: false,
extrudePath: dashCurve,
})
}
// Create dashes at the start of the arc
for (let i = 0; i < dashesAtEachEnd; i++) {
const t = i / totalDashes
dashGeometries.push(createDashAt(t, arcStart))
dashGeometries.push(createDashAt(t, arcEnd))
}
// fill in the remaining arc
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
if (remainingArcLength > 0) {
const remainingArcStartT = dashesAtEachEnd / totalDashes
const remainingArcEndT = 1 - remainingArcStartT
const centerVec = new Vector2(center[0], center[1])
const remainingArcStartVec = arcStart.getPoint(remainingArcStartT)
const remainingArcEndVec = arcStart.getPoint(remainingArcEndT)
const remainingArcCurve = new EllipseCurve(
arcStart.aX,
arcStart.aY,
arcStart.xRadius,
arcStart.yRadius,
new Vector2().subVectors(centerVec, remainingArcStartVec).angle() +
Math.PI,
new Vector2().subVectors(centerVec, remainingArcEndVec).angle() + Math.PI,
!ccw
)
const remainingArcPoints = remainingArcCurve.getPoints(50)
const remainingArcPath = new CurvePath<Vector3>()
remainingArcPath.add(
new CatmullRomCurve3(
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
)
)
const remainingArcGeometry = new ExtrudeGeometry(shape, {
steps: 50,
bevelEnabled: false,
extrudePath: remainingArcPath,
})
dashGeometries.push(remainingArcGeometry)
}
const geo = dashGeometries.length
? mergeGeometries(dashGeometries)
: new BufferGeometry()
geo.userData.type = 'dashed'
return geo
}
export function dashedStraight(
from: Coords2d,
to: Coords2d,
shape: Shape,
scale = 1
): BufferGeometry<NormalBufferAttributes> {
const dashSize = 1.2 * scale
const gapSize = 1.2 * scale // todo: gabSize is not respected
const dashLine = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)
)
const length = dashLine.getLength()
const numberOfPoints = (length / (dashSize + gapSize)) * 2
const startOfLine = new Vector3(from[0], from[1], 0)
const endOfLine = new Vector3(to[0], to[1], 0)
const dashGeometries = []
const dashComponent = (xOrY: number, pointIndex: number) =>
((to[xOrY] - from[xOrY]) / numberOfPoints) * pointIndex + from[xOrY]
for (let i = 0; i < numberOfPoints; i += 2) {
const dashStart = new Vector3(dashComponent(0, i), dashComponent(1, i), 0)
let dashEnd = new Vector3(
dashComponent(0, i + 1),
dashComponent(1, i + 1),
0
)
if (startOfLine.distanceTo(dashEnd) > startOfLine.distanceTo(endOfLine))
dashEnd = endOfLine
if (dashEnd) {
const dashCurve = new LineCurve3(dashStart, dashEnd)
const dashGeometry = new ExtrudeGeometry(shape, {
steps: 1,
bevelEnabled: false,
extrudePath: dashCurve,
})
dashGeometries.push(dashGeometry)
}
}
const geo = dashGeometries.length
? mergeGeometries(dashGeometries)
: new BufferGeometry()
geo.userData.type = 'dashed'
return geo
}

View File

@ -0,0 +1,28 @@
import { Quaternion } from 'three'
import { isQuaternionVertical } from './setup'
describe('isQuaternionVertical', () => {
it('should identify vertical quaternions', () => {
const verticalQuaternions = [
new Quaternion(1, 0, 0, 0).normalize(), // bottom
new Quaternion(-0.7, 0.7, 0, 0).normalize(), // bottom 2
new Quaternion(0, 1, 0, 0).normalize(), // bottom 3
new Quaternion(0, 0, 0, 1).normalize(), // look from top
]
verticalQuaternions.forEach((quaternion) => {
expect(isQuaternionVertical(quaternion)).toBe(true)
})
})
it('should identify non-vertical quaternions', () => {
const nonVerticalQuaternions = [
new Quaternion(0.7, 0, 0, 0.7).normalize(), // front
new Quaternion(0, 0.7, 0.7, 0).normalize(), // back
new Quaternion(-0.5, 0.5, 0.5, -0.5).normalize(), // left side
new Quaternion(0.5, 0.5, 0.5, 0.5).normalize(), // right side
]
nonVerticalQuaternions.forEach((quaternion) => {
expect(isQuaternionVertical(quaternion)).toBe(false)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import React from 'react' import React from 'react'
import { paths } from '../Router' import { paths } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import type { LinkProps } from 'react-router-dom' import type { LinkProps } from 'react-router-dom'

View File

@ -1,6 +1,6 @@
import { Toolbar } from '../Toolbar' import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { IndexLoaderData } from '../Router' import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'

View File

@ -1,5 +1,5 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore' import { useStore } from 'useStore'

View File

@ -8,7 +8,7 @@ import {
} from '../lang/modifyAst' } from '../lang/modifyAst'
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst' import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSinglton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'useStore' import { executeAst } from 'useStore'
@ -138,38 +138,37 @@ export function useCalc({
}, [kclManager.ast, kclManager.programMemory, selectionRange]) }, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(() => { useEffect(() => {
const execAstAndSetResult = async () => { try {
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
const ast = parse(code) const ast = parse(code)
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => { availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } _programMem.root[key] = { type: 'userVal', value, __meta: [] }
}) })
const { programMemory } = await executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
defaultPlanes: kclManager.defaultPlanes,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory) JSON.stringify(kclManager.programMemory)
), ),
}).then(({ programMemory }) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}) })
const resultDeclaration = ast.body.find( } catch (e) {
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}
execAstAndSetResult().catch(() => {
setCalcResult('NAN') setCalcResult('NAN')
setValueNode(null) setValueNode(null)
}) }
}, [value, availableVarInfo]) }, [value, availableVarInfo])
return { return {

View File

@ -0,0 +1,75 @@
import { useState, useEffect } from 'react'
import { setupSingleton } from '../clientSideScene/setup'
import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle(
(newFov: number) => setupSingleton.dollyZoom(newFov),
1000 / 15
)
export const CamToggle = () => {
const [isPerspective, setIsPerspective] = useState(true)
const [fov, setFov] = useState(40)
const [enableRotate, setEnableRotate] = useState(true)
useEffect(() => {
engineCommandManager.waitForReady.then(async () => {
setupSingleton.dollyZoom(fov)
})
}, [])
const toggleCamera = () => {
if (isPerspective) {
isReducedMotion()
? setupSingleton.useOrthographicCamera()
: setupSingleton.animateToOrthographic()
} else {
isReducedMotion()
? setupSingleton.usePerspectiveCamera()
: setupSingleton.animateToPerspective()
}
setIsPerspective(!isPerspective)
}
const handleFovChange = (newFov: number) => {
setFov(newFov)
updateDollyZoom(newFov)
}
return (
<div className="absolute right-14 bottom-3">
{isPerspective && (
<div className="">
<input
type="range"
min="4"
max="90"
step={0.5}
value={fov}
onChange={(e) => handleFovChange(Number(e.target.value))}
className="w-full cursor-pointer pointer-events-auto"
/>
</div>
)}
<button onClick={toggleCamera} className="">
{isPerspective
? 'Switch to Orthographic Camera'
: 'Switch to Perspective Camera'}
</button>
<button
onClick={() => {
if (enableRotate) {
setupSingleton.controls.enableRotate = false
} else {
setupSingleton.controls.enableRotate = true
}
setEnableRotate(!enableRotate)
}}
className=""
>
{enableRotate ? 'Disable Rotation' : 'Enable Rotation'}
</button>
</div>
)
}

View File

@ -9,7 +9,7 @@ import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor' import { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export const CodeMenu = ({ children }: PropsWithChildren) => { export const CodeMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =

View File

@ -1,6 +1,6 @@
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
import { CommandArgument } from 'lib/commandTypes' import { CommandArgument } from 'lib/commandTypes'
import { import {
ResolvedSelectionType, ResolvedSelectionType,

View File

@ -1,6 +1,7 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer' import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands' import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/setup'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => { export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return ( return (
@ -15,6 +16,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
> >
<section className="p-4 flex flex-col gap-4"> <section className="p-4 flex flex-col gap-4">
<EngineCommands /> <EngineCommands />
<CamDebugSettings />
<div style={{ height: '400px' }} className="overflow-y-auto"> <div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer /> <AstExplorer />
</div> </div>

View File

@ -0,0 +1,43 @@
import { MoveDesc } from 'machines/modelingMachine'
export const DragWarningToast = (moveDescs: MoveDesc[]) => {
if (moveDescs.length === 1) {
return (
<div className="flex items-center">
<div>🔒</div>
<div className="dark:bg-slate-950/50 bg-slate-400/50 p-1 px-3 rounded-xl text-sm">
move disabled: line{' '}
<span className="dark:text-energy-20 text-lime-600">
{moveDescs[0].line}
</span>
:{' '}
<pre>
<code className="dark:text-energy-20 text-lime-600">
{moveDescs[0].snippet}
</code>
</pre>{' '}
is fully constrained
</div>
</div>
)
} else if (moveDescs.length > 1) {
return (
<div className="dark:bg-slate-950/50 bg-slate-400/50 p-1 px-3 rounded-xl text-sm">
<div>Move disabled as The following lines are constrained</div>
{moveDescs.map((desc, i) => {
return (
<div key={i}>
line {desc.line}:{' '}
<pre className="inline-block">
<code className="dark:text-energy-20 text-lime-600">
{moveDescs[0].snippet}
</code>
</pre>{' '}
</div>
)
})}
</div>
)
}
return null
}

View File

@ -93,7 +93,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
if (values.type === 'ply' || values.type === 'stl') { if (values.type === 'ply' || values.type === 'stl') {
values.selection = { type: 'default_scene' } values.selection = { type: 'default_scene' }
} }
void engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'export', type: 'export',

View File

@ -1,6 +1,7 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { IndexLoaderData, paths } from '../Router' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import React, { createContext } from 'react' import React, { createContext } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {

View File

@ -1,4 +1,5 @@
import { IndexLoaderData, paths } from 'Router' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { FileEntry } from '@tauri-apps/api/fs' import { FileEntry } from '@tauri-apps/api/fs'

View File

@ -1,6 +1,6 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { paths } from '../Router' import { paths } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL' import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react' import React, { createContext, useEffect, useRef } from 'react'
@ -101,7 +101,7 @@ export const GlobalStateProvider = ({
goToSignInPage: () => { goToSignInPage: () => {
navigate(paths.SIGN_IN) navigate(paths.SIGN_IN)
void logout() logout()
}, },
goToIndexPage: () => { goToIndexPage: () => {
if (window.location.pathname.includes(paths.SIGN_IN)) { if (window.location.pathname.includes(paths.SIGN_IN)) {

View File

@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
import { useEffect } from 'react' import { useEffect } from 'react'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme' import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
const ReactJsonTypeHack = ReactJson as any const ReactJsonTypeHack = ReactJson as any

View File

@ -41,9 +41,9 @@ describe('processMemory', () => {
otherVar: 3, otherVar: 3,
theExtrude: [], theExtrude: [],
theSketch: [ theSketch: [
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' }, { type: 'ToPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' }, { type: 'ToPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' }, { type: 'ToPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
], ],
}) })
}) })

View File

@ -3,7 +3,7 @@ import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useMemo } from 'react' import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm' import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
import { Themes } from '../lib/theme' import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
interface MemoryPanelProps extends CollapsiblePanelProps { interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System> theme?: Exclude<Themes, Themes.System>

View File

@ -13,23 +13,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager } from 'lang/std/engineConnection' import { engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { addStartSketch } from 'lang/modifyAst'
import { roundOff } from 'lib/utils'
import {
recast,
parse,
Program,
PipeExpression,
CallExpression,
} from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst'
import {
addCloseToPipe,
addNewSketchLn,
compareVec2Epsilon,
} from 'lang/std/sketch'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
angleBetweenInfo, angleBetweenInfo,
@ -49,6 +33,9 @@ import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { setupSingleton } from 'clientSideScene/setup'
import { getSketchQuaternion } from 'clientSideScene/clientSideScene'
import { startSketchOnDefault } from 'lang/modifyAst'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -92,181 +79,10 @@ export const ModelingMachineProvider = ({
const [modelingState, modelingSend, modelingActor] = useMachine( const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine, modelingMachine,
{ {
// context: persistedSettings,
actions: { actions: {
'Modify AST': () => {},
'Update code selection cursors': () => {},
'show default planes': () => {
void kclManager.showPlanes()
},
'create path': assign({
sketchEnginePathId: () => {
const sketchUuid = uuidv4()
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
return sketchUuid
},
}),
'AST start new sketch': assign(
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
if (!axis) {
// Something really weird must have happened for this to happen.
console.error('axis is undefined for starting a new sketch')
return {}
}
if (!segmentId) {
// Something really weird must have happened for this to happen.
console.error('segmentId is undefined for starting a new sketch')
return {}
}
const _addStartSketch = addStartSketch(
kclManager.ast,
axis,
[roundOff(coords[0].x), roundOff(coords[0].y)],
[
roundOff(coords[1].x - coords[0].x),
roundOff(coords[1].y - coords[0].y),
]
)
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
const newCode = recast(_modifiedAst)
const astWithUpdatedSource = parse(newCode)
const updatedPipeNode = getNodeFromPath<PipeExpression>(
astWithUpdatedSource,
_pathToNode
).node
const startProfileAtCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startProfileAt'
)
if (startProfileAtCallExp)
engineCommandManager.artifactMap[sketchEnginePathId] = {
type: 'result',
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
commandType: 'start_path',
data: null,
raw: {} as any,
}
const lineCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' && exp.callee.name === 'line'
)
if (lineCallExp)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
void kclManager.executeAstMock(astWithUpdatedSource, true)
return {
sketchPathToNode: _pathToNode,
}
}
),
'AST add line segment': async (
{ sketchPathToNode, sketchEnginePathId },
{ data: { coords, segmentId } }
) => {
if (!sketchPathToNode) return
const lastCoord = coords[coords.length - 1]
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: sketchEnginePathId,
},
})
const firstSegment = pathInfo?.data?.data?.segments.find(
(seg: any) => seg.command === 'line_to'
)
const firstSegCoords = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: firstSegment.command_id,
},
})
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
const isClose = compareVec2Epsilon(
[startPathCoord.x, startPathCoord.y],
[lastCoord.x, lastCoord.y]
)
let _modifiedAst: Program
if (!isClose) {
const newSketchLn = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [lastCoord.x, lastCoord.y],
from: [coords[0].x, coords[0].y],
fnName: 'line',
pathToNode: sketchPathToNode,
})
const _modifiedAst = newSketchLn.modifiedAst
void kclManager.executeAstMock(_modifiedAst, true).then(() => {
const lineCallExp = getNodeFromPath<CallExpression>(
kclManager.ast,
newSketchLn.pathToNode
).node
if (segmentId)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
})
} else {
_modifiedAst = addCloseToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
void kclManager.executeAstMock(_modifiedAst, true)
// updateAst(_modifiedAst, true)
}
},
'sketch exit execute': () => { 'sketch exit execute': () => {
void kclManager.executeAst() kclManager.executeAst()
}, },
'set tool': () => {}, // TODO
'Set selection': assign(({ selectionRanges }, event) => { 'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data const setSelections = event.data
@ -274,36 +90,6 @@ export const ModelingMachineProvider = ({
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
return { selectionRanges: setSelections.selection } return { selectionRanges: setSelections.selection }
else if (setSelections.selectionType === 'otherSelection') { else if (setSelections.selectionType === 'otherSelection') {
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
// const axisAddCmd: EngineCommand = {
// type: 'modeling_cmd_req',
// cmd: {
// type: 'highlight_set_entities',
// entities: [
// setSelections.selection === 'x-axis'
// ? X_AXIS_UUID
// : Y_AXIS_UUID,
// ],
// },
// cmd_id: uuidv4(),
// }
// if (!isShiftDown) {
// engineCommandManager
// .sendSceneCommand({
// type: 'modeling_cmd_req',
// cmd: {
// type: 'select_clear',
// },
// cmd_id: uuidv4(),
// })
// .then(() => {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// })
// } else {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// }
const { const {
codeMirrorSelection, codeMirrorSelection,
selectionRangeTypeMap, selectionRangeTypeMap,
@ -384,12 +170,6 @@ export const ModelingMachineProvider = ({
}), }),
}, },
guards: { guards: {
'Selection contains axis': () => true,
'Selection contains edge': () => true,
'Selection contains face': () => true,
'Selection contains line': () => true,
'Selection contains point': () => true,
'Selection is not empty': () => true,
'has valid extrude selection': ({ selectionRanges }) => { 'has valid extrude selection': ({ selectionRanges }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected // A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time // TODO: I believe this guard only allows for extruding a single face at a time
@ -409,6 +189,29 @@ export const ModelingMachineProvider = ({
}, },
}, },
services: { services: {
'animate-to-face': async (_, { data: { plane, normal } }) => {
const { modifiedAst, pathToNode } = startSketchOnDefault(
kclManager.ast,
plane
)
await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal)
await setupSingleton.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
}
},
'animate-to-sketch': async ({
sketchPathToNode,
sketchNormalBackUp,
}) => {
const quaternion = getSketchQuaternion(
sketchPathToNode || [],
sketchNormalBackUp
)
await setupSingleton.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({ 'Get horizontal info': async ({
selectionRanges, selectionRanges,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
@ -542,17 +345,6 @@ export const ModelingMachineProvider = ({
} }
) )
useEffect(() => {
engineCommandManager.onPlaneSelected((plane_id: string) => {
if (modelingState.nextEvents.includes('Select default plane')) {
modelingSend({
type: 'Select default plane',
data: { planeId: plane_id },
})
}
})
}, [modelingSend, modelingState.nextEvents])
useEffect(() => { useEffect(() => {
kclManager.registerExecuteCallback(() => { kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' }) modelingSend({ type: 'Re-execute' })
@ -565,10 +357,7 @@ export const ModelingMachineProvider = ({
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineConfig,
onCancel: () => { onCancel: () => modelingSend({ type: 'Cancel' }),
console.log('firing onCancel!!')
modelingSend({ type: 'Cancel' })
},
}) })
return ( return (

View File

@ -1,5 +1,6 @@
import { FormEvent, useEffect, useRef, useState } from 'react' import { FormEvent, useEffect, useRef, useState } from 'react'
import { type ProjectWithEntryPointMetadata, paths } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { import {

View File

@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { GlobalStateProvider } from './GlobalStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar' import CommandBarProvider from './CommandBar/CommandBar'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'

View File

@ -1,7 +1,8 @@
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons' import { faHome } from '@fortawesome/free-solid-svg-icons'
import { IndexLoaderData, paths } from '../Router' import { type IndexLoaderData } from 'lib/types'
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 { ExportButton } from './ExportButton'

View File

@ -11,16 +11,13 @@ import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodeFromPath } from 'lang/queryAst'
import { VariableDeclarator, recast, CallExpression } from 'lang/wasm'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager, useKclContext } from 'lang/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
import { changeSketchArguments } from 'lang/std/sketch' import { ClientSideScene } from 'clientSideScene/setup'
export const Stream = ({ className = '' }) => { export const Stream = ({ className = '' }: { className?: string }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>() const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
@ -39,7 +36,7 @@ export const Stream = ({ className = '' }) => {
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls const cameraControls = settings?.context?.cameraControls
const { send, state, context } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
useEffect(() => { useEffect(() => {
@ -53,8 +50,10 @@ export const Stream = ({ className = '' }) => {
videoRef.current.srcObject = mediaStream videoRef.current.srcObject = mediaStream
}, [mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!videoRef.current) return if (!videoRef.current) return
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
@ -62,55 +61,6 @@ export const Stream = ({ className = '' }) => {
...streamDimensions, ...streamDimensions,
}) })
const newId = uuidv4()
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type = 'rotate'
if (
interactionGuards.pan.callback(e) ||
interactionGuards.pan.lenientDragStartButton === e.button
) {
interaction = 'pan'
} else if (
interactionGuards.rotate.callback(e) ||
interactionGuards.rotate.lenientDragStartButton === e.button
) {
interaction = 'rotate'
} else if (
interactionGuards.zoom.dragCallback(e) ||
interactionGuards.zoom.lenientDragStartButton === e.button
) {
interaction = 'zoom'
}
if (state.matches('Sketch.Move Tool')) {
if (
state.matches('Sketch.Move Tool.No move') ||
state.matches('Sketch.Move Tool.Move with execute')
) {
return
}
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'handle_mouse_drag_start',
window: { x, y },
},
cmd_id: newId,
})
} else if (!state.matches('Sketch.Line Tool')) {
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
}
setButtonDownInStream(e.button) setButtonDownInStream(e.button)
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
@ -118,7 +68,7 @@ export const Stream = ({ className = '' }) => {
const fps = 60 const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => { const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
void engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'default_camera_zoom', type: 'default_camera_zoom',
@ -128,13 +78,15 @@ export const Stream = ({ className = '' }) => {
}) })
}, Math.round(1000 / fps)) }, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({ const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
clientX, clientX,
clientY, clientY,
ctrlKey, ctrlKey,
}) => { }) => {
if (!videoRef.current) return if (!videoRef.current) return
setButtonDownInStream(undefined) setButtonDownInStream(undefined)
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX, clientX,
clientY, clientY,
@ -155,208 +107,21 @@ export const Stream = ({ className = '' }) => {
cmd_id: newCmdId, cmd_id: newCmdId,
} }
if (!didDragInStream && state.matches('Sketch no face')) { if (!didDragInStream) {
command.cmd = {
type: 'select_with_point',
selection_type: 'add',
selected_at_window: { x, y },
}
void engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
command.cmd = {
type: 'mouse_click',
window: { x, y },
}
void engineCommandManager.sendSceneCommand(command).then(async (resp) => {
const entities_modified = resp?.data?.data?.entities_modified
if (!entities_modified) return
if (state.matches('Sketch.Line Tool.No Points')) {
send('Add point')
} else if (state.matches('Sketch.Line Tool.Point Added')) {
const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
// We need the normal for the plane we are on.
const plane = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'get_sketch_mode_plane',
},
})
const z_axis = plane.data.data.z_axis
// Get the current axis.
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
null
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
if (z_axis.z === -1) {
currentAxis = '-xy'
} else {
currentAxis = 'xy'
}
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
if (z_axis.x === -1) {
currentAxis = '-yz'
} else {
currentAxis = 'yz'
}
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
if (z_axis.y === -1) {
currentAxis = '-xz'
} else {
currentAxis = 'xz'
}
}
send({
type: 'Add point',
data: {
coords,
axis: currentAxis,
segmentId: entities_modified[0],
},
})
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
send({
type: 'Add point',
data: { coords, axis: null, segmentId: entities_modified[0] },
})
}
})
} else if (
!didDragInStream &&
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
) {
command.cmd = { command.cmd = {
type: 'select_with_point', type: 'select_with_point',
selected_at_window: { x, y }, selected_at_window: { x, y },
selection_type: 'add', selection_type: 'add',
} }
engineCommandManager.sendSceneCommand(command)
void engineCommandManager.sendSceneCommand(command) } else if (didDragInStream) {
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
void engineCommandManager.sendSceneCommand(command)
} else if (didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = { command.cmd = {
type: 'handle_mouse_drag_end', type: 'handle_mouse_drag_end',
window: { x, y }, window: { x, y },
} }
void engineCommandManager.sendSceneCommand(command).then(async () => {
if (!context.sketchPathToNode) return
getNodeFromPath<VariableDeclarator>(
kclManager.ast,
context.sketchPathToNode,
'VariableDeclarator'
)
// Get the current plane string for plane we are on.
let currentPlaneString = ''
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
currentPlaneString = 'XY'
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
currentPlaneString = 'YZ'
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
currentPlaneString = 'XZ'
}
// Do not supporting editing/moving lines on a non-default plane.
// Eventually we can support this but for now we will just throw an
// error.
if (currentPlaneString === '') return
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: context.sketchEnginePathId,
},
})
const segmentsWithMappings = (
pathInfo?.data?.data?.segments as { command_id: string }[]
)
.filter(({ command_id }) => {
return command_id && engineCommandManager.artifactMap[command_id]
})
.map(({ command_id }) => command_id)
const segment2dInfo = await Promise.all(
segmentsWithMappings.map(async (segmentId) => {
const response = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: segmentId,
},
})
const controlPoints: [
{ x: number; y: number },
{ x: number; y: number }
] = response.data.data.control_points
return {
controlPoints,
segmentId,
}
})
)
let modifiedAst = { ...kclManager.ast }
let code = kclManager.code
for (const controlPoint of segment2dInfo) {
const range =
engineCommandManager.artifactMap[controlPoint.segmentId].range
if (!range) continue
const from = controlPoint.controlPoints[0]
const to = controlPoint.controlPoints[1]
const modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
range,
[to.x, to.y],
[from.x, from.y]
)
modifiedAst = modded.modifiedAst
// update artifact map ranges now that we have updated the ast.
code = recast(modded.modifiedAst)
const astWithCurrentRanges = kclManager.safeParse(code)
if (!astWithCurrentRanges) return
const updateNode = getNodeFromPath<CallExpression>(
astWithCurrentRanges,
modded.pathToNode
).node
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
updateNode.start,
updateNode.end,
]
}
void kclManager.executeAstMock(modifiedAst, true)
})
} else {
void engineCommandManager.sendSceneCommand(command) void engineCommandManager.sendSceneCommand(command)
} else {
engineCommandManager.sendSceneCommand(command)
} }
setDidDragInStream(false) setDidDragInStream(false)
@ -364,6 +129,8 @@ export const Stream = ({ className = '' }) => {
} }
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
if (!clickCoords) return if (!clickCoords) return
const delta = const delta =
@ -376,16 +143,19 @@ export const Stream = ({ className = '' }) => {
} }
return ( return (
<div id="stream" className={className}> <div
id="stream"
className={className}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
>
<video <video
ref={videoRef} ref={videoRef}
muted muted
autoPlay autoPlay
controls={false} controls={false}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
onWheel={handleScroll} onWheel={handleScroll}
onPlay={() => setIsLoading(false)} onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove} onMouseMoveCapture={handleMouseMove}
@ -393,6 +163,7 @@ export const Stream = ({ className = '' }) => {
disablePictureInPicture disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }} style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/> />
<ClientSideScene cameraControls={settings.context.cameraControls} />
{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

@ -11,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 { useMemo } 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'
@ -24,7 +24,9 @@ import { CSSRuleObject } from 'tailwindcss/types/config'
import { useModelingContext } from 'hooks/useModelingContext' 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/KclSinglton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { setupSingleton } from 'clientSideScene/setup'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -56,10 +58,12 @@ export const TextEditor = ({
isShiftDown: s.isShiftDown, isShiftDown: s.isShiftDown,
})) }))
const { code, errors } = useKclContext() const { code, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const { const {
context: { selectionRanges, selectionRangeTypeMap }, context: { selectionRanges, selectionRangeTypeMap },
send, send,
state,
} = useModelingContext() } = useModelingContext()
const { settings: { context: { textWrapping } = {} } = {} } = const { settings: { context: { textWrapping } = {} } = {} } =
@ -76,12 +80,10 @@ export const TextEditor = ({
const fromServer: FromServer = FromServer.create() const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer)
if (!TEST) { if (!TEST) {
Server.initialize(intoServer, fromServer) Server.initialize(intoServer, fromServer).then((lspServer) => {
.then((lspServer) => { lspServer.start()
void lspServer.start() setIsLSPServerReady(true)
setIsLSPServerReady(true) })
})
.catch((e) => console.log(e))
} }
const lspClient = new LanguageServerClient({ client }) const lspClient = new LanguageServerClient({ client })
@ -117,6 +119,12 @@ export const TextEditor = ({
if (!editorView) { if (!editorView) {
setEditorView(viewUpdate.view) setEditorView(viewUpdate.view)
} }
if (setupSingleton.selected) return // mid drag
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (ignoreEvents.includes(state.event.type)) return
const eventInfo = processCodeMirrorRanges({ const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges, codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges, selectionRanges,
@ -124,7 +132,20 @@ export const TextEditor = ({
isShiftDown, isShiftDown,
}) })
if (!eventInfo) return if (!eventInfo) return
const deterministicEventInfo = {
...eventInfo,
engineEvents: eventInfo.engineEvents.map((e) => ({
...e,
cmd_id: 'static',
})),
}
const stringEvent = JSON.stringify(deterministicEventInfo)
if (
stringEvent === lastEvent.current.event &&
Date.now() - lastEvent.current.time < 500
)
return // don't repeat events
lastEvent.current = { event: stringEvent, time: Date.now() }
send(eventInfo.modelingEvent) send(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) => eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event) engineCommandManager.sendSceneCommand(event)
@ -153,7 +174,7 @@ export const TextEditor = ({
key: editorShortcutMeta.convertToVariable.codeMirror, key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => { run: () => {
if (convertEnabled) { if (convertEnabled) {
void convertCallback() convertCallback()
return true return true
} }
return false return false

View File

@ -11,7 +11,7 @@ import {
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export function equalAngleInfo({ export function equalAngleInfo({
selectionRanges, selectionRanges,

View File

@ -11,7 +11,7 @@ import {
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export function setEqualLengthInfo({ export function setEqualLengthInfo({
selectionRanges, selectionRanges,

View File

@ -10,7 +10,7 @@ import {
getTransformInfos, getTransformInfos,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export function horzVertInfo( export function horzVertInfo(
selectionRanges: Selections, selectionRanges: Selections,

View File

@ -15,7 +15,7 @@ import {
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst' import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)

View File

@ -10,7 +10,7 @@ import {
getRemoveConstraintsTransforms, getRemoveConstraintsTransforms,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export function removeConstrainingValuesInfo({ export function removeConstrainingValuesInfo({
selectionRanges, selectionRanges,

View File

@ -19,7 +19,7 @@ import {
createVariableDeclaration, createVariableDeclaration,
} from '../../lang/modifyAst' } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)

View File

@ -14,7 +14,7 @@ import {
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst' import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)

View File

@ -13,7 +13,7 @@ import {
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst' import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)

View File

@ -21,7 +21,7 @@ import {
} from '../../lang/modifyAst' } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils' import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)

View File

@ -4,7 +4,7 @@ import { faBars, faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { paths } from '../Router' import { paths } from 'lib/paths'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'

View File

@ -1,7 +1,7 @@
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useState } from 'react' import { useState } from 'react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { useKclContext } from 'lang/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
export function WasmErrBanner() { export function WasmErrBanner() {
const [isBannerDismissed, setBannerDismissed] = useState(false) const [isBannerDismissed, setBannerDismissed] = useState(false)

View File

@ -67,7 +67,7 @@ export class LanguageServerClient {
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
await this.client.start() this.client.start()
this.ready = true this.ready = true
} }
@ -81,12 +81,12 @@ export class LanguageServerClient {
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
this.notify('textDocument/didOpen', params) this.notify('textDocument/didOpen', params)
void this.updateSemanticTokens(params.textDocument.uri) this.updateSemanticTokens(params.textDocument.uri)
} }
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) { textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
this.notify('textDocument/didChange', params) this.notify('textDocument/didChange', params)
void this.updateSemanticTokens(params.textDocument.uri) this.updateSemanticTokens(params.textDocument.uri)
} }
async updateSemanticTokens(uri: string) { async updateSemanticTokens(uri: string) {

View File

@ -62,7 +62,7 @@ export class LanguageServerPlugin implements PluginValue {
this.client.attachPlugin(this) this.client.attachPlugin(this)
void this.initialize({ this.initialize({
documentText: this.view.state.doc.toString(), documentText: this.view.state.doc.toString(),
}) })
} }
@ -70,7 +70,7 @@ export class LanguageServerPlugin implements PluginValue {
update({ docChanged }: ViewUpdate) { update({ docChanged }: ViewUpdate) {
if (!docChanged) return if (!docChanged) return
void this.sendChange({ this.sendChange({
documentText: this.view.state.doc.toString(), documentText: this.view.state.doc.toString(),
}) })
} }
@ -127,7 +127,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
requestDiagnostics(view: EditorView) { requestDiagnostics(view: EditorView) {
void this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: view.state.doc.toString() })
} }
async requestHoverTooltip( async requestHoverTooltip(
@ -140,7 +140,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
await this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: view.state.doc.toString() })
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.documentUri },
position: { line, character }, position: { line, character },
@ -178,7 +178,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
await this.sendChange({ this.sendChange({
documentText: context.state.doc.toString(), documentText: context.state.doc.toString(),
}) })

View File

@ -1,4 +1,6 @@
import { BROWSER_FILE_NAME, IndexLoaderData, paths } from 'Router' import { BROWSER_FILE_NAME } from 'Router'
import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { useRouteLoaderData } from 'react-router-dom' import { useRouteLoaderData } from 'react-router-dom'
export function useAbsoluteFilePath() { export function useAbsoluteFilePath() {

View File

@ -46,6 +46,6 @@ export function useEngineConnectionSubscriptions() {
engineCommandManager, engineCommandManager,
setHighlightRange, setHighlightRange,
highlightRange, highlightRange,
context.sketchEnginePathId, context?.sketchEnginePathId,
]) ])
} }

View File

@ -3,7 +3,7 @@ import { parse } from '../lang/wasm'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,

View File

@ -2,7 +2,7 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -39,7 +39,7 @@ export function useConvertToVariable() {
variableName variableName
) )
void kclManager.updateAst(_modifiedAst, true) kclManager.updateAst(_modifiedAst, true)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }

View File

@ -6,6 +6,8 @@ import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook' import { HotkeysProvider } from 'react-hotkeys-hook'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env'
// import { inspect } from '@xstate/inspect'
// if (DEV) // if (DEV)
// inspect({ // inspect({
// iframe: false, // iframe: false,

View File

@ -7,6 +7,7 @@ import {
} from './std/engineConnection' } from './std/engineConnection'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { import {
CallExpression,
initPromise, initPromise,
parse, parse,
PathToNode, PathToNode,
@ -17,7 +18,7 @@ import {
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { IndexLoaderData } from 'Router' import { type IndexLoaderData } from 'lib/types'
import { Params, useLoaderData } from 'react-router-dom' import { Params, useLoaderData } from 'react-router-dom'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/api/fs' import { writeTextFile } from '@tauri-apps/api/fs'
@ -59,7 +60,7 @@ class KclManager {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
void this.executeAst(ast) this.executeAst(ast)
}, 600) }, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {} private _isExecutingCallback: (arg: boolean) => void = () => {}
@ -98,7 +99,7 @@ class KclManager {
}) })
}) })
} else { } else {
localStorage.setItem(PERSIST_CODE_TOKEN, code) localStorage?.setItem(PERSIST_CODE_TOKEN, code)
} }
} }
@ -110,10 +111,6 @@ class KclManager {
this._programMemoryCallBack(programMemory) this._programMemoryCallBack(programMemory)
} }
get defaultPlanes() {
return this?.engineCommandManager?.defaultPlanes
}
get logs() { get logs() {
return this._logs return this._logs
} }
@ -168,11 +165,13 @@ class KclManager {
zustandStore.state.code = '' zustandStore.state.code = ''
localStorage.setItem('store', JSON.stringify(zustandStore)) localStorage.setItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) { } else if (storedCode === null) {
console.log('stored brack thing')
this.code = bracket this.code = bracket
} else { } else {
this.code = storedCode this.code = storedCode
} }
this.ensureWasmInit().then(() => {
this.ast = this.safeParse(this.code) || this.ast
})
} }
registerCallBacks({ registerCallBacks({
setCode, setCode,
@ -235,7 +234,6 @@ class KclManager {
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast, ast,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
defaultPlanes: this.defaultPlanes,
}) })
this.isExecuting = false this.isExecuting = false
this.logs = logs this.logs = logs
@ -251,13 +249,20 @@ class KclManager {
data: null, data: null,
}) })
} }
async executeAstMock(ast: Program = this._ast, updateCode = false) { async executeAstMock(
ast: Program = this._ast,
{
updates,
}: {
updates: 'none' | 'code' | 'codeAndArtifactRanges'
} = { updates: 'none' }
) {
await this.ensureWasmInit() await this.ensureWasmInit()
const newCode = recast(ast) const newCode = recast(ast)
const newAst = this.safeParse(newCode) const newAst = this.safeParse(newCode)
if (!newAst) return if (!newAst) return
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
if (updateCode) { if (updates !== 'none') {
this.setCode(recast(ast)) this.setCode(recast(ast))
} }
this._ast = { ...newAst } this._ast = { ...newAst }
@ -265,22 +270,38 @@ class KclManager {
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast: newAst, ast: newAst,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
defaultPlanes: this.defaultPlanes,
useFakeExecutor: true, useFakeExecutor: true,
}) })
this._logs = logs this._logs = logs
this._kclErrors = errors this._kclErrors = errors
this._programMemory = programMemory this._programMemory = programMemory
if (updates !== 'codeAndArtifactRanges') return
Object.entries(engineCommandManager.artifactMap).forEach(
([commandId, artifact]) => {
if (!artifact.pathToNode) return
const node = getNodeFromPath<CallExpression>(
kclManager.ast,
artifact.pathToNode,
'CallExpression'
).node
if (node.type !== 'CallExpression') return
const [oldStart, oldEnd] = artifact.range
if (oldStart === 0 && oldEnd === 0) return
if (oldStart === node.start && oldEnd === node.end) return
engineCommandManager.artifactMap[commandId].range = [
node.start,
node.end,
]
}
)
} }
async executeCode(code?: string) { async executeCode(code?: string) {
await this.ensureWasmInit() await this.ensureWasmInit()
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
if (!this?.engineCommandManager?.planesInitialized()) return
const result = await executeCode({ const result = await executeCode({
engineCommandManager, engineCommandManager,
code: code || this._code, code: code || this._code,
lastAst: this._ast, lastAst: this._ast,
defaultPlanes: this.defaultPlanes,
force: false, force: false,
}) })
if (!result.isChange) return if (!result.isChange) return
@ -366,26 +387,10 @@ class KclManager {
// When we don't re-execute, we still want to update the program // When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor // memory with the new ast. So we will hit the mock executor
// instead. // instead.
await this.executeAstMock(astWithUpdatedSource, true) await this.executeAstMock(astWithUpdatedSource, { updates: 'code' })
} }
return returnVal return returnVal
} }
getPlaneId(axis: 'xy' | 'xz' | 'yz'): string {
return this.defaultPlanes[axis]
}
showPlanes() {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
}
hidePlanes() {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
}
} }
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)

View File

@ -33,7 +33,7 @@ show(mySketch001)`
}, },
value: [ value: [
{ {
type: 'toPoint', type: 'ToPoint',
name: '', name: '',
to: [-1.59, -1.54], to: [-1.59, -1.54],
from: [0, 0], from: [0, 0],
@ -43,7 +43,7 @@ show(mySketch001)`
}, },
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [0.46, -5.82], to: [0.46, -5.82],
from: [-1.59, -1.54], from: [-1.59, -1.54],
name: '', name: '',
@ -55,6 +55,9 @@ show(mySketch001)`
], ],
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
xAxis: [1, 0, 0],
yAxis: [0, 1, 0],
zAxis: [0, 0, 1],
id: expect.any(String), id: expect.any(String),
planeId: expect.any(String), planeId: expect.any(String),
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],

View File

@ -54,7 +54,7 @@ show(mySketch)
const minusGeo = root.mySketch.value const minusGeo = root.mySketch.value
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
{ {
type: 'toPoint', type: 'ToPoint',
to: [0, 2], to: [0, 2],
from: [0, 0], from: [0, 0],
__geoMeta: { __geoMeta: {
@ -64,7 +64,7 @@ show(mySketch)
name: 'myPath', name: 'myPath',
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [2, 3], to: [2, 3],
from: [0, 2], from: [0, 2],
name: '', name: '',
@ -74,7 +74,7 @@ show(mySketch)
}, },
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [5, -1], to: [5, -1],
from: [2, 3], from: [2, 3],
__geoMeta: { __geoMeta: {
@ -154,7 +154,7 @@ show(mySketch)
}, },
value: [ value: [
{ {
type: 'toPoint', type: 'ToPoint',
to: [1, 1], to: [1, 1],
from: [0, 0], from: [0, 0],
name: '', name: '',
@ -164,7 +164,7 @@ show(mySketch)
}, },
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [0, 1], to: [0, 1],
from: [1, 1], from: [1, 1],
__geoMeta: { __geoMeta: {
@ -174,7 +174,7 @@ show(mySketch)
name: 'myPath', name: 'myPath',
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [1, 1], to: [1, 1],
from: [0, 1], from: [0, 1],
name: '', name: '',
@ -186,6 +186,9 @@ show(mySketch)
], ],
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
xAxis: [1, 0, 0],
yAxis: [0, 1, 0],
zAxis: [0, 0, 1],
id: expect.any(String), id: expect.any(String),
planeId: expect.any(String), planeId: expect.any(String),
__meta: [{ sourceRange: [39, 63] }], __meta: [{ sourceRange: [39, 63] }],

View File

@ -30,41 +30,28 @@ import {
createFirstArg, createFirstArg,
} from './std/sketch' } from './std/sketch'
import { isLiteralArrayOrStatic } from './std/sketchcombos' import { isLiteralArrayOrStatic } from './std/sketchcombos'
import { DefaultPlaneStr } from 'clientSideScene/clientSideScene'
import { roundOff } from 'lib/utils'
export function addStartSketch( export function startSketchOnDefault(
node: Program, node: Program,
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz', axis: DefaultPlaneStr,
start: [number, number], name = ''
end: [number, number]
): { modifiedAst: Program; id: string; pathToNode: PathToNode } { ): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
const _node = { ...node } const _node = { ...node }
const _name = findUniqueName(node, 'part') const _name = name || findUniqueName(node, 'part')
const startSketchOn = createCallExpressionStdLib('startSketchOn', [ const startSketchOn = createCallExpressionStdLib('startSketchOn', [
createLiteral(axis.toUpperCase()), createLiteral(axis),
])
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
createPipeSubstitution(),
])
const initialLineTo = createCallExpression('line', [
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
createPipeSubstitution(),
]) ])
const pipeBody = [startSketchOn, startProfileAt, initialLineTo] const variableDeclaration = createVariableDeclaration(_name, startSketchOn)
const variableDeclaration = createVariableDeclaration(
_name,
createPipeExpression(pipeBody)
)
const newIndex = node.body.length
_node.body = [...node.body, variableDeclaration] _node.body = [...node.body, variableDeclaration]
const sketchIndex = _node.body.length - 1
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[newIndex.toString(10), 'index'], [sketchIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'], ['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
@ -77,6 +64,43 @@ export function addStartSketch(
} }
} }
export function addStartProfileAt(
node: Program,
pathToNode: PathToNode,
at: [number, number]
): { modifiedAst: Program; pathToNode: PathToNode } {
console.log('addStartProfileAt called')
const variableDeclaration = getNodeFromPath<VariableDeclaration>(
node,
pathToNode,
'VariableDeclaration'
).node
if (variableDeclaration.type !== 'VariableDeclaration') {
throw new Error('variableDeclaration.init.type !== PipeExpression')
}
const _node = { ...node }
const init = variableDeclaration.declarations[0].init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([
createLiteral(roundOff(at[0])),
createLiteral(roundOff(at[1])),
]),
createPipeSubstitution(),
])
if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt)
} else {
variableDeclaration.declarations[0].init = createPipeExpression([
init,
startProfileAt,
])
}
return {
modifiedAst: _node,
pathToNode,
}
}
export function addSketchTo( export function addSketchTo(
node: Program, node: Program,
axis: 'xy' | 'xz' | 'yz', axis: 'xy' | 'xz' | 'yz',

View File

@ -65,13 +65,13 @@ export function getNodeFromPath<T>(
} }
} }
} catch (e) { } catch (e) {
console.error( // console.error(
`Could not find path ${pathItem} in node ${JSON.stringify( // `Could not find path ${pathItem} in node ${JSON.stringify(
currentNode, // currentNode,
null, // null,
2 // 2
)}, successful path was ${successfulPaths}` // )}, successful path was ${successfulPaths}`
) // )
} }
} }
return { return {
@ -266,6 +266,7 @@ function moreNodePathFromSourceRange(
} }
} }
} }
if (_node.type === 'PipeSubstitution' && isInRange) return path
console.error('not implemented: ' + node.type) console.error('not implemented: ' + node.type)
return path return path
} }
@ -489,7 +490,7 @@ export function isLinesParallelAndConstrained(
const constraintLevel = getConstraintLevelFromSourceRange( const constraintLevel = getConstraintLevelFromSourceRange(
secondaryLine.range, secondaryLine.range,
ast ast
) ).level
const isConstrained = const isConstrained =
constraintType === 'angle' || constraintLevel === 'full' constraintType === 'angle' || constraintLevel === 'full'

View File

@ -1,16 +1,18 @@
import { SourceRange } from 'lang/wasm' import { PathToNode, Program, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env' 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 * as Sentry from '@sentry/react'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { setupSingleton } from 'clientSideScene/setup'
let lastMessage = '' let lastMessage = ''
interface CommandInfo { interface CommandInfo {
commandType: CommandTypes commandType: CommandTypes
range: SourceRange range: SourceRange
pathToNode: PathToNode
parentId?: string parentId?: string
} }
@ -601,9 +603,14 @@ class EngineConnection {
}) })
.join('\n') .join('\n')
if (message.request_id) { if (message.request_id) {
const artifactThatFailed =
engineCommandManager.artifactMap[message.request_id] ||
engineCommandManager.lastArtifactMap[message.request_id]
console.error( console.error(
`Error in response to request ${message.request_id}:\n${errorsString}` `Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.commandType}`
) )
console.log(artifactThatFailed)
} else { } else {
console.error(`Error from server:\n${errorsString}`) console.error(`Error from server:\n${errorsString}`)
} }
@ -618,8 +625,6 @@ class EngineConnection {
return return
} }
console.log('received', resp)
switch (resp.type) { switch (resp.type) {
case 'ice_server_info': case 'ice_server_info':
let ice_servers = resp.data?.ice_servers let ice_servers = resp.data?.ice_servers
@ -863,10 +868,10 @@ export type CommandLog =
export class EngineCommandManager { export class EngineCommandManager {
artifactMap: ArtifactMap = {} artifactMap: ArtifactMap = {}
lastArtifactMap: ArtifactMap = {} lastArtifactMap: ArtifactMap = {}
private getAst: () => Program = () => ({ start: 0, end: 0, body: [] } as any)
outSequence = 1 outSequence = 1
inSequence = 1 inSequence = 1
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes = { xy: '', yz: '', xz: '' }
_commandLogs: CommandLog[] = [] _commandLogs: CommandLog[] = []
_commandLogCallBack: (command: CommandLog[]) => void = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
// Folks should realize that wait for ready does not get called _everytime_ // Folks should realize that wait for ready does not get called _everytime_
@ -890,6 +895,11 @@ export class EngineCommandManager {
constructor() { constructor() {
this.engineConnection = undefined this.engineConnection = undefined
;(async () => {
// circular dependency needs one to be lazy loaded
const { kclManager } = await import('lang/KclSingleton')
this.getAst = () => kclManager.ast
})()
} }
start({ start({
@ -946,16 +956,9 @@ export class EngineCommandManager {
gizmo_mode: true, gizmo_mode: true,
}, },
}) })
setupSingleton.onStreamStart()
// Initialize the planes. executeCode(undefined, true)
void this.initPlanes().then(() => {
// We execute the code here to make sure if the stream was to
// restart in a session, we want to make sure to execute the code.
// We force it to re-execute the code because we want to make sure
// the code is executed everytime the stream is restarted.
// We pass undefined for the code so it reads from the current state.
executeCode(undefined, true)
})
}, },
onClose: () => { onClose: () => {
setIsStreamReady(false) setIsStreamReady(false)
@ -1075,6 +1078,7 @@ export class EngineCommandManager {
this.artifactMap[id] = { this.artifactMap[id] = {
type: 'result', type: 'result',
range: command.range, range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType, commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse, data: modelingResponse,
@ -1092,6 +1096,7 @@ export class EngineCommandManager {
type: 'result', type: 'result',
commandType: command?.commandType, commandType: command?.commandType,
range: command?.range, range: command?.range,
pathToNode: command?.pathToNode,
data: modelingResponse, data: modelingResponse,
raw: message, raw: message,
} }
@ -1109,6 +1114,7 @@ export class EngineCommandManager {
this.artifactMap[id] = { this.artifactMap[id] = {
type: 'failed', type: 'failed',
range: command.range, range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType, commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
errors, errors,
@ -1123,6 +1129,7 @@ export class EngineCommandManager {
this.artifactMap[id] = { this.artifactMap[id] = {
type: 'failed', type: 'failed',
range: command.range, range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType, commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
errors, errors,
@ -1218,7 +1225,10 @@ export class EngineCommandManager {
registerCommandLogCallback(callback: (command: CommandLog[]) => void) { registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
this._commandLogCallBack = callback this._commandLogCallBack = callback
} }
sendSceneCommand(command: EngineCommand): Promise<any> { sendSceneCommand(
command: EngineCommand,
forceWebsocket = false
): Promise<any> {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
} }
@ -1232,7 +1242,9 @@ export class EngineCommandManager {
command.type === 'modeling_cmd_req' && command.type === 'modeling_cmd_req' &&
(command.cmd.type === 'highlight_set_entity' || (command.cmd.type === 'highlight_set_entity' ||
command.cmd.type === 'mouse_move' || command.cmd.type === 'mouse_move' ||
command.cmd.type === 'camera_drag_move') command.cmd.type === 'camera_drag_move' ||
command.cmd.type === 'default_camera_look_at' ||
command.cmd.type === ('default_camera_perspective_settings' as any))
) )
) { ) {
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy // highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
@ -1249,14 +1261,23 @@ export class EngineCommandManager {
console.log('sending command', command.cmd.type) console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type lastMessage = command.cmd.type
} }
if (command.type === 'modeling_cmd_batch_req') {
this.engineConnection?.send(command)
// TODO - handlePendingCommands does not handle batch commands
// return this.handlePendingCommand(command.requests[0].cmd_id, command.cmd)
return Promise.resolve()
}
if (command.type !== 'modeling_cmd_req') return Promise.resolve() if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd const cmd = command.cmd
if ( if (
(cmd.type === 'camera_drag_move' || (cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') && cmd.type === 'handle_mouse_drag_move' ||
this.engineConnection?.unreliableDataChannel cmd.type === 'default_camera_look_at' ||
cmd.type === ('default_camera_perspective_settings' as any)) &&
this.engineConnection?.unreliableDataChannel &&
!forceWebsocket
) { ) {
cmd.sequence = this.outSequence ;(cmd as any).sequence = this.outSequence
this.outSequence++ this.outSequence++
this.engineConnection?.unreliableSend(command) this.engineConnection?.unreliableSend(command)
return Promise.resolve() return Promise.resolve()
@ -1277,6 +1298,12 @@ export class EngineCommandManager {
this.engineConnection?.unreliableSend(command) this.engineConnection?.unreliableSend(command)
return Promise.resolve() return Promise.resolve()
} }
if (
command.cmd.type === 'default_camera_look_at' ||
command.cmd.type === ('default_camera_perspective_settings' as any)
) {
;(cmd as any).sequence = this.outSequence++
}
// since it's not mouse drag or highlighting send over TCP and keep track of the command // since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command) this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id, command.cmd) return this.handlePendingCommand(command.cmd_id, command.cmd)
@ -1285,10 +1312,12 @@ export class EngineCommandManager {
id, id,
range, range,
command, command,
ast,
}: { }: {
id: string id: string
range: SourceRange range: SourceRange
command: EngineCommand | string command: EngineCommand | string
ast: Program
}): Promise<any> { }): Promise<any> {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
@ -1310,17 +1339,18 @@ export class EngineCommandManager {
} }
this.engineConnection?.send(command) this.engineConnection?.send(command)
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') { if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
return this.handlePendingCommand(id, command?.cmd, range) return this.handlePendingCommand(id, command?.cmd, ast, range)
} else if (typeof command === 'string') { } else if (typeof command === 'string') {
const parseCommand: EngineCommand = JSON.parse(command) const parseCommand: EngineCommand = JSON.parse(command)
if (parseCommand.type === 'modeling_cmd_req') if (parseCommand.type === 'modeling_cmd_req')
return this.handlePendingCommand(id, parseCommand?.cmd, range) return this.handlePendingCommand(id, parseCommand?.cmd, ast, range)
} }
throw Error('shouldnt reach here') throw Error('shouldnt reach here')
} }
handlePendingCommand( handlePendingCommand(
id: string, id: string,
command: Models['ModelingCmd_type'], command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange range?: SourceRange
) { ) {
let resolve: (val: any) => void = () => {} let resolve: (val: any) => void = () => {}
@ -1333,8 +1363,12 @@ export class EngineCommandManager {
} }
// TODO handle other commands that have a parent // TODO handle other commands that have a parent
} }
const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0])
: []
this.artifactMap[id] = { this.artifactMap[id] = {
range: range || [0, 0], range: range || [0, 0],
pathToNode,
type: 'pending', type: 'pending',
commandType: command.type, commandType: command.type,
parentId: getParentId(), parentId: getParentId(),
@ -1363,9 +1397,12 @@ export class EngineCommandManager {
const range: SourceRange = JSON.parse(rangeStr) const range: SourceRange = JSON.parse(rangeStr)
// We only care about the modeling command response. // We only care about the modeling command response.
return this.sendModelingCommand({ id, range, command: commandStr }).then( return this.sendModelingCommand({
({ raw }) => JSON.stringify(raw) id,
) range,
command: commandStr,
ast: this.getAst(),
}).then(({ raw }) => JSON.stringify(raw))
} }
commandResult(id: string): Promise<any> { commandResult(id: string): Promise<any> {
const command = this.artifactMap[id] const command = this.artifactMap[id]
@ -1392,102 +1429,6 @@ export class EngineCommandManager {
artifactMap: this.artifactMap, artifactMap: this.artifactMap,
} }
} }
private async initPlanes() {
const [xy, yz, xz] = [
await this.createPlane({
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
}),
await this.createPlane({
x_axis: { x: 0, y: 1, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
}),
await this.createPlane({
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
}),
]
this.defaultPlanes = { xy, yz, xz }
this.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) return
if (
![
this.defaultPlanes.xy,
this.defaultPlanes.yz,
this.defaultPlanes.xz,
].includes(data.entity_id)
)
return
this.onPlaneSelectCallback(data.entity_id)
},
})
}
planesInitialized(): boolean {
return (
this.defaultPlanes.xy !== '' &&
this.defaultPlanes.yz !== '' &&
this.defaultPlanes.xz !== ''
)
}
onPlaneSelectCallback = (id: string) => {}
onPlaneSelected(callback: (id: string) => void) {
this.onPlaneSelectCallback = callback
}
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
return await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: id,
hidden: hidden,
},
})
}
private async createPlane({
x_axis,
y_axis,
color,
}: {
x_axis: Models['Point3d_type']
y_axis: Models['Point3d_type']
color: Models['Color_type']
}): Promise<string> {
const planeId = uuidv4()
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'make_plane',
size: 100,
origin: { x: 0, y: 0, z: 0 },
x_axis,
y_axis,
clobber: false,
hide: true,
},
cmd_id: planeId,
})
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'plane_set_color',
plane_id: planeId,
color,
},
cmd_id: uuidv4(),
})
await this.setPlaneHidden(planeId, true)
return planeId
}
} }
export const engineCommandManager = new EngineCommandManager() export const engineCommandManager = new EngineCommandManager()

View File

@ -45,7 +45,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
} else if (!currentPath) { } else if (!currentPath) {
return [0, 0] return [0, 0]
} }
if (currentPath.type === 'topoint') { if (currentPath.type === 'ToPoint') {
return [currentPath.to[0], currentPath.to[1]] return [currentPath.to[0], currentPath.to[1]]
} }
return [0, 0] return [0, 0]
@ -445,6 +445,85 @@ export const yLine: SketchLineHelper = {
addTag: addTagWithTo('length'), addTag: addTagWithTo('length'),
} }
export const tangentialArcTo: SketchLineHelper = {
add: ({
node,
pathToNode,
to,
createCallback,
replaceExisting,
referencedSegment,
}) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const { node: pipe } = getNode<PipeExpression | CallExpression>(
'PipeExpression'
)
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
const toX = createLiteral(roundOff(to[0], 2))
const toY = createLiteral(roundOff(to[1], 2))
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const { callExp, valueUsedInTransform } = createCallback(
[toX, toY],
referencedSegment
)
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform,
}
}
const newLine = createCallExpression('tangentialArcTo', [
createArrayExpression([toX, toY]),
createPipeSubstitution(),
])
if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, newLine]
return {
modifiedAst: _node,
pathToNode: [
...pathToNode,
['body', 'PipeExpression'],
[pipe.body.length - 1, 'CallExpression'],
],
}
} else {
varDec.init = createPipeExpression([varDec.init, newLine])
}
return {
modifiedAst: _node,
pathToNode,
}
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const { node: callExpression } = getNodeFromPath<CallExpression>(
_node,
pathToNode
)
const x = createLiteral(roundOff(to[0], 2))
const y = createLiteral(roundOff(to[1], 2))
const firstArg = callExpression.arguments?.[0]
if (!mutateArrExp(firstArg, createArrayExpression([x, y]))) {
mutateObjExpProp(firstArg, createArrayExpression([x, y]), 'to')
}
return {
modifiedAst: _node,
pathToNode,
}
},
// TODO copy-paste from angledLine
addTag: addTagWithTo('angleLength'),
}
export const angledLine: SketchLineHelper = { export const angledLine: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -900,6 +979,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
angledLineToX, angledLineToX,
angledLineToY, angledLineToY,
angledLineThatIntersects, angledLineThatIntersects,
tangentialArcTo,
} as const } as const
export function changeSketchArguments( export function changeSketchArguments(
@ -942,14 +1022,28 @@ interface CreateLineFnCallArgs {
export function compareVec2Epsilon( export function compareVec2Epsilon(
vec1: [number, number], vec1: [number, number],
vec2: [number, number] vec2: [number, number],
compareEpsilon = 0.015625 // or 2^-6
) { ) {
const compareEpsilon = 0.015625 // or 2^-6
const xDifference = Math.abs(vec1[0] - vec2[0]) const xDifference = Math.abs(vec1[0] - vec2[0])
const yDifference = Math.abs(vec1[1] - vec2[1]) const yDifference = Math.abs(vec1[1] - vec2[1])
return xDifference < compareEpsilon && yDifference < compareEpsilon return xDifference < compareEpsilon && yDifference < compareEpsilon
} }
// this version uses this distance of the two points instead of comparing x and y separately
export function compareVec2Epsilon2(
vec1: [number, number],
vec2: [number, number],
compareEpsilon = 0.015625 // or 2^-6
) {
const xDifference = Math.abs(vec1[0] - vec2[0])
const yDifference = Math.abs(vec1[1] - vec2[1])
const distance = Math.sqrt(
xDifference * xDifference + yDifference * yDifference
)
return distance < compareEpsilon
}
export function addNewSketchLn({ export function addNewSketchLn({
node: _node, node: _node,
programMemory: previousProgramMemory, programMemory: previousProgramMemory,
@ -1288,5 +1382,9 @@ export function getFirstArg(callExp: CallExpression): {
if (['angledLineThatIntersects'].includes(name)) { if (['angledLineThatIntersects'].includes(name)) {
return getAngledLineThatIntersects(callExp) return getAngledLineThatIntersects(callExp)
} }
if (['tangentialArcTo'].includes(name)) {
// TODO probably needs it's own implementation
return getFirstArgValuesForXYFns(callExp)
}
throw new Error('unexpected call expression') throw new Error('unexpected call expression')
} }

View File

@ -388,7 +388,7 @@ show(part001)`
[index, index] [index, index]
).segment ).segment
expect(segment).toEqual({ expect(segment).toEqual({
type: 'toPoint', type: 'ToPoint',
to: [5.62, 1.79], to: [5.62, 1.79],
from: [3.48, 0.44], from: [3.48, 0.44],
name: '', name: '',
@ -405,7 +405,7 @@ show(part001)`
to: [0, 0.04], to: [0, 0.04],
from: [0, 0.04], from: [0, 0.04],
name: '', name: '',
type: 'base', type: 'Base',
}) })
}) })
}) })

View File

@ -22,7 +22,7 @@ export function getSketchSegmentFromSourceRange(
startSourceRange[1] >= rangeEnd && startSourceRange[1] >= rangeEnd &&
sketchGroup.start sketchGroup.start
) )
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 } return { segment: { ...sketchGroup.start, type: 'Base' }, index: -1 }
const lineIndex = sketchGroup.value.findIndex( const lineIndex = sketchGroup.value.findIndex(
({ __geoMeta: { sourceRange } }: Path) => ({ __geoMeta: { sourceRange } }: Path) =>

View File

@ -506,7 +506,7 @@ show(part001)`
const ast = parse(code) const ast = parse(code)
const constraintLevels: ReturnType< const constraintLevels: ReturnType<
typeof getConstraintLevelFromSourceRange typeof getConstraintLevelFromSourceRange
>[] = ['full', 'partial', 'free'] >['level'][] = ['full', 'partial', 'free']
constraintLevels.forEach((constraintLevel) => { constraintLevels.forEach((constraintLevel) => {
const recursivelySeachCommentsAndCheckConstraintLevel = ( const recursivelySeachCommentsAndCheckConstraintLevel = (
str: string, str: string,
@ -520,7 +520,7 @@ show(part001)`
const expectedConstraintLevel = getConstraintLevelFromSourceRange( const expectedConstraintLevel = getConstraintLevelFromSourceRange(
[offsetIndex, offsetIndex], [offsetIndex, offsetIndex],
ast ast
) ).level
expect(expectedConstraintLevel).toBe(constraintLevel) expect(expectedConstraintLevel).toBe(constraintLevel)
return recursivelySeachCommentsAndCheckConstraintLevel( return recursivelySeachCommentsAndCheckConstraintLevel(
str, str,

View File

@ -1503,20 +1503,21 @@ function getArgLiteralVal(arg: Value): number {
export function getConstraintLevelFromSourceRange( export function getConstraintLevelFromSourceRange(
cursorRange: Selection['range'], cursorRange: Selection['range'],
ast: Program ast: Program
): 'free' | 'partial' | 'full' { ): { range: [number, number]; level: 'free' | 'partial' | 'full' } {
const { node: sketchFnExp } = getNodeFromPath<CallExpression>( const { node: sketchFnExp } = getNodeFromPath<CallExpression>(
ast, ast,
getNodePathFromSourceRange(ast, cursorRange), getNodePathFromSourceRange(ast, cursorRange),
'CallExpression' 'CallExpression'
) )
const name = sketchFnExp?.callee?.name as ToolTip const name = sketchFnExp?.callee?.name as ToolTip
if (!toolTips.includes(name)) return 'free' const range: [number, number] = [sketchFnExp.start, sketchFnExp.end]
if (!toolTips.includes(name)) return { level: 'free', range: range }
const firstArg = getFirstArg(sketchFnExp) const firstArg = getFirstArg(sketchFnExp)
// check if the function is fully constrained // check if the function is fully constrained
if (isNotLiteralArrayOrStatic(firstArg.val)) { if (isNotLiteralArrayOrStatic(firstArg.val)) {
return 'full' return { level: 'full', range: range }
} }
// check if the function has no constraints // check if the function has no constraints
@ -1525,10 +1526,10 @@ export function getConstraintLevelFromSourceRange(
const isOneValFree = const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val) !Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return 'free' if (isTwoValFree) return { level: 'free', range: range }
if (isOneValFree) return 'partial' if (isOneValFree) return { level: 'partial', range: range }
return 'partial' return { level: 'partial', range: range }
} }
export function isLiteralArrayOrStatic( export function isLiteralArrayOrStatic(

View File

@ -26,15 +26,6 @@ export function pathMapToSelections(
return newSelections return newSelections
} }
export function isReducedMotion(): boolean {
return (
typeof window !== 'undefined' &&
window.matchMedia &&
// TODO/Note I (Kurt) think '(prefers-reduced-motion: reduce)' and '(prefers-reduced-motion)' are equivalent, but not 100% sure
window.matchMedia('(prefers-reduced-motion)').matches
)
}
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
artifactMap: ArtifactMap, artifactMap: ArtifactMap,
selectionRanges: Selections selectionRanges: Selections

View File

@ -4,6 +4,8 @@ import init, {
execute_wasm, execute_wasm,
lexer_wasm, lexer_wasm,
modify_ast_for_sketch_wasm, modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -12,7 +14,7 @@ import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem' import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import type { Program } from '../wasm-lib/kcl/bindings/Program' import type { Program } from '../wasm-lib/kcl/bindings/Program'
import type { Token } from '../wasm-lib/kcl/bindings/Token' import type { Token } from '../wasm-lib/kcl/bindings/Token'
import { DefaultPlanes } from '../wasm-lib/kcl/bindings/DefaultPlanes' import { Coords2d } from './std/sketch'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -118,15 +120,13 @@ export interface ProgramMemory {
export const executor = async ( export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager
planes: DefaultPlanes
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
programMemory, programMemory,
engineCommandManager, engineCommandManager
planes
) )
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
@ -137,15 +137,13 @@ export const executor = async (
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager
planes: DefaultPlanes
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
try { try {
const memory: ProgramMemory = await execute_wasm( const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory), JSON.stringify(programMemory),
engineCommandManager, engineCommandManager
JSON.stringify(planes)
) )
return memory return memory
} catch (e: any) { } catch (e: any) {
@ -212,3 +210,44 @@ export const modifyAstForSketch = async (
throw kclError throw kclError
} }
} }
export function isPointsCCW(points: Coords2d[]): number {
return is_points_ccw(new Float64Array(points.flat()))
}
export function getTangentialArcToInfo({
arcStartPoint,
arcEndPoint,
tanPreviousPoint,
obtuse = true,
}: {
arcStartPoint: Coords2d
arcEndPoint: Coords2d
tanPreviousPoint: Coords2d
obtuse?: boolean
}): {
center: Coords2d
arcMidPoint: Coords2d
radius: number
startAngle: number
endAngle: number
ccw: boolean
} {
const result = get_tangential_arc_to_info(
arcStartPoint[0],
arcStartPoint[1],
arcEndPoint[0],
arcEndPoint[1],
tanPreviousPoint[0],
tanPreviousPoint[1],
obtuse
)
return {
center: [result.center_x, result.center_y],
arcMidPoint: [result.arc_mid_point_x, result.arc_mid_point_y],
radius: result.radius,
startAngle: result.start_angle,
endAngle: result.end_angle,
ccw: result.ccw > 0,
}
}

View File

@ -33,7 +33,7 @@ interface MouseGuardZoomHandler {
lenientDragStartButton?: number lenientDragStartButton?: number
} }
interface MouseGuard { export interface MouseGuard {
pan: MouseGuardHandler pan: MouseGuardHandler
zoom: MouseGuardZoomHandler zoom: MouseGuardZoomHandler
rotate: MouseGuardHandler rotate: MouseGuardHandler

View File

@ -1,3 +1,6 @@
export function isTauri(): boolean { export function isTauri(): boolean {
return '__TAURI__' in window if (typeof window !== 'undefined') {
return '__TAURI__' in window
}
return false
} }

22
src/lib/paths.ts Normal file
View File

@ -0,0 +1,22 @@
import { onboardingPaths } from 'routes/Onboarding/paths'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
return Object.fromEntries(
Object.entries(routesObject).map(([constName, path]) => [
constName,
prepend + path,
])
)
}
export const paths = {
INDEX: '/',
HOME: '/home',
FILE: '/file',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding'
) as typeof onboardingPaths,
}

View File

@ -1,16 +1,24 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { engineCommandManager } from 'lang/std/engineConnection' import { engineCommandManager } from 'lang/std/engineConnection'
import { SourceRange } from 'lang/wasm' import { CallExpression, SourceRange, parse, recast } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { EditorSelection } from '@codemirror/state' import { EditorSelection } from '@codemirror/state'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
import { SelectionRange } from '@uiw/react-codemirror' import { SelectionRange } from '@uiw/react-codemirror'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'
import { doesPipeHaveCallExp } from 'lang/queryAst' import { doesPipeHaveCallExp, getNodeFromPath } from 'lang/queryAst'
import { CommandArgument } from './commandTypes' import { CommandArgument } from './commandTypes'
import {
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
clientSideScene,
getParentGroup,
} from 'clientSideScene/clientSideScene'
import { Mesh } from 'three'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/setup'
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b' export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01' export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
@ -173,6 +181,42 @@ export async function getEventForSelectWithPoint(
} }
} }
export function getEventForSegmentSelection(
obj: any
): ModelingMachineEvent | null {
const group = getParentGroup(obj)
const axisGroup = getParentGroup(obj, [AXIS_GROUP])
if (!group && !axisGroup) return null
if (axisGroup?.userData.type === AXIS_GROUP) {
return {
type: 'Set selection',
data: {
selectionType: 'otherSelection',
selection: obj?.userData?.type === X_AXIS ? 'x-axis' : 'y-axis',
},
}
}
const pathToNode = group?.userData?.pathToNode
if (!pathToNode) return null
// previous drags don't update ast for efficiency reasons
// So we want to make sure we have and updated ast with
// accurate source ranges
const updatedAst = parse(kclManager.code)
const node = getNodeFromPath<CallExpression>(
updatedAst,
pathToNode,
'CallExpression'
).node
const range: SourceRange = [node.start, node.end]
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range, type: 'default' },
},
}
}
export function handleSelectionBatch({ export function handleSelectionBatch({
selections, selections,
}: { }: {
@ -239,7 +283,6 @@ export function handleSelectionWithShift({
}) })
} }
if (otherSelection) { if (otherSelection) {
console.log('otherSelection in handleSelectionWithShift', otherSelection)
return handleSelectionBatch({ return handleSelectionBatch({
selections: { selections: {
codeBasedSelections: isShiftDown codeBasedSelections: isShiftDown
@ -335,6 +378,7 @@ export function processCodeMirrorRanges({
.filter(Boolean) as any .filter(Boolean) as any
if (!selectionRanges) return null if (!selectionRanges) return null
updateSceneObjectColors(codeBasedSelections)
return { return {
modelingEvent: { modelingEvent: {
type: 'Set selection', type: 'Set selection',
@ -350,6 +394,41 @@ export function processCodeMirrorRanges({
} }
} }
function updateSceneObjectColors(codeBasedSelections: Selection[]) {
let updated: Program
try {
updated = parse(recast(kclManager.ast))
} catch (e) {
console.error('error parsing code in processCodeMirrorRanges', e)
return
}
Object.values(clientSideScene.activeSegments).forEach((segmentGroup) => {
if (
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes(
segmentGroup?.userData?.type
)
)
return
const node = getNodeFromPath<CallExpression>(
updated,
segmentGroup.userData.pathToNode,
'CallExpression'
).node
const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection.range, [node.start, node.end])
})
const color = groupHasCursor ? 0x0000ff : 0xffffff
segmentGroup.traverse(
(child) => child instanceof Mesh && child.material.color.set(color)
)
// TODO if we had access to the xstate context and therefore selections
// we wouldn't need to set this here,
// it would be better to treat xstate context as the source of truth instead of having
// extra redundant state floating around
segmentGroup.userData.isSelected = groupHasCursor
})
}
function resetAndSetEngineEntitySelectionCmds( function resetAndSetEngineEntitySelectionCmds(
selections: SelectionToEngine[] selections: SelectionToEngine[]
): Models['WebSocketRequest_type'][] { ): Models['WebSocketRequest_type'][] {

View File

@ -3,7 +3,7 @@ import {
faArrowUp, faArrowUp,
faCircle, faCircle,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
const DESC = ':desc' const DESC = ':desc'

View File

@ -7,7 +7,7 @@ import {
} from '@tauri-apps/api/fs' } from '@tauri-apps/api/fs'
import { documentDir, homeDir, sep } from '@tauri-apps/api/path' import { documentDir, homeDir, sep } from '@tauri-apps/api/path'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { metadata } from 'tauri-plugin-fs-extra-api' import { metadata } from 'tauri-plugin-fs-extra-api'
const PROJECT_FOLDER = 'zoo-modeling-app-projects' const PROJECT_FOLDER = 'zoo-modeling-app-projects'

View File

@ -66,11 +66,7 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager }) as any as EngineCommandManager
await mockEngineCommandManager.waitForReady await mockEngineCommandManager.waitForReady
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, mockEngineCommandManager, { const programMemory = await _executor(ast, pm, mockEngineCommandManager)
xy: uuidv4(),
yz: uuidv4(),
xz: uuidv4(),
})
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
return programMemory return programMemory
} }
@ -89,11 +85,7 @@ export async function executor(
}) })
await engineCommandManager.waitForReady await engineCommandManager.waitForReady
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, engineCommandManager, { const programMemory = await _executor(ast, pm, engineCommandManager)
xy: uuidv4(),
yz: uuidv4(),
xz: uuidv4(),
})
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return programMemory return programMemory
} }

16
src/lib/types.ts Normal file
View File

@ -0,0 +1,16 @@
import { type Metadata } from 'tauri-plugin-fs-extra-api'
import { type FileEntry } from '@tauri-apps/api/fs'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
file?: FileEntry
}
export type ProjectWithEntryPointMetadata = FileEntry & {
entrypointMetadata: Metadata
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}

View File

@ -98,3 +98,12 @@ export function getNormalisedCoordinates({
y: Math.round((browserY / height) * streamHeight), y: Math.round((browserY / height) * streamHeight),
} }
} }
export function isReducedMotion(): boolean {
return (
typeof window !== 'undefined' &&
window.matchMedia &&
// TODO/Note I (Kurt) think '(prefers-reduced-motion: reduce)' and '(prefers-reduced-motion)' are equivalent, but not 100% sure
window.matchMedia('(prefers-reduced-motion)').matches
)
}

19
src/lib/utils2d.test.ts Normal file
View File

@ -0,0 +1,19 @@
import { Coords2d } from 'lang/std/sketch'
import { isPointsCCW, initPromise } from 'lang/wasm'
beforeAll(() => initPromise)
describe('test isPointsCW', () => {
test('basic test', () => {
const points: Coords2d[] = [
[2, 2],
[2, 0],
[0, -2],
]
const pointsRev = [...points].reverse()
const CCW = isPointsCCW(pointsRev)
const CW = isPointsCCW(points)
expect(CCW).toBe(1)
expect(CW).toBe(-1)
})
})

19
src/lib/utils2d.ts Normal file
View File

@ -0,0 +1,19 @@
import { Coords2d } from 'lang/std/sketch'
import { getAngle } from './utils'
export function deg2Rad(deg: number): number {
return (deg * Math.PI) / 180
}
export function getTangentPointFromPreviousArc(
lastArcCenter: Coords2d,
lastArcCCW: boolean,
lastArcEnd: Coords2d
): Coords2d {
const angleFromOldCenterToArcStart = getAngle(lastArcCenter, lastArcEnd)
const tangentialAngle = angleFromOldCenterToArcStart + (lastArcCCW ? -90 : 90)
return [
Math.cos(deg2Rad(tangentialAngle)) * 10 + lastArcEnd[0],
Math.sin(deg2Rad(tangentialAngle)) * 10 + lastArcEnd[1],
]
}

View File

@ -1,74 +0,0 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'': { type: '' }
'done.invoke.validateArgument': {
type: 'done.invoke.validateArgument'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.validateArguments': {
type: 'done.invoke.validateArguments'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.validateArgument': {
type: 'error.platform.validateArgument'
data: unknown
}
'error.platform.validateArguments': {
type: 'error.platform.validateArguments'
data: unknown
}
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {
'Validate all arguments': 'done.invoke.validateArguments'
'Validate argument': 'done.invoke.validateArgument'
}
missingImplementations: {
actions:
| 'Add arguments'
| 'Close dialog'
| 'Execute command'
| 'Open dialog'
delays: never
guards: never
services: never
}
eventsCausingActions: {
'Add arguments': 'done.invoke.validateArguments'
'Add commands': 'Add commands'
'Close dialog': 'Close'
'Execute command': '' | 'Submit'
'Open dialog': 'Open'
'Remove argument': 'Remove argument'
'Remove commands': 'Remove commands'
'Set current argument':
| 'Add argument'
| 'Edit argument'
| 'error.platform.validateArguments'
}
eventsCausingDelays: {}
eventsCausingGuards: {
'Arguments are ready': 'done.invoke.validateArguments'
'Command has no arguments': ''
}
eventsCausingServices: {
'Validate all arguments': 'done.invoke.validateArgument'
'Validate argument': 'Submit'
}
matchesStates:
| 'Checking Arguments'
| 'Closed'
| 'Command selected'
| 'Gathering arguments'
| 'Gathering arguments.Awaiting input'
| 'Gathering arguments.Validating'
| 'Review'
| 'Selecting command'
| { 'Gathering arguments'?: 'Awaiting input' | 'Validating' }
tags: never
}

View File

@ -1,5 +1,5 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { ProjectWithEntryPointMetadata } from 'Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { FileEntry } from '@tauri-apps/api/fs' import { FileEntry } from '@tauri-apps/api/fs'
export const FILE_PERSIST_KEY = 'Last opened KCL files' export const FILE_PERSIST_KEY = 'Last opened KCL files'

View File

@ -1,96 +0,0 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'done.invoke.create-file': {
type: 'done.invoke.create-file'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.delete-file': {
type: 'done.invoke.delete-file'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.read-files': {
type: 'done.invoke.read-files'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.rename-file': {
type: 'done.invoke.rename-file'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.create-file': {
type: 'error.platform.create-file'
data: unknown
}
'error.platform.delete-file': {
type: 'error.platform.delete-file'
data: unknown
}
'error.platform.read-files': {
type: 'error.platform.read-files'
data: unknown
}
'error.platform.rename-file': {
type: 'error.platform.rename-file'
data: unknown
}
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {
createFile: 'done.invoke.create-file'
deleteFile: 'done.invoke.delete-file'
readFiles: 'done.invoke.read-files'
renameFile: 'done.invoke.rename-file'
}
missingImplementations: {
actions: 'navigateToFile' | 'toastError' | 'toastSuccess'
delays: never
guards: 'Has at least 1 file'
services: 'createFile' | 'deleteFile' | 'readFiles' | 'renameFile'
}
eventsCausingActions: {
navigateToFile: 'Open file'
setFiles: 'done.invoke.read-files'
setSelectedDirectory: 'Set selected directory'
toastError:
| 'error.platform.create-file'
| 'error.platform.delete-file'
| 'error.platform.read-files'
| 'error.platform.rename-file'
toastSuccess:
| 'done.invoke.create-file'
| 'done.invoke.delete-file'
| 'done.invoke.rename-file'
}
eventsCausingDelays: {}
eventsCausingGuards: {
'Has at least 1 file': 'done.invoke.read-files'
}
eventsCausingServices: {
createFile: 'Create file'
deleteFile: 'Delete file'
readFiles:
| 'assign'
| 'done.invoke.create-file'
| 'done.invoke.delete-file'
| 'done.invoke.rename-file'
| 'error.platform.create-file'
| 'error.platform.rename-file'
| 'xstate.init'
renameFile: 'Rename file'
}
matchesStates:
| 'Creating file'
| 'Deleting file'
| 'Has files'
| 'Has no files'
| 'Opening file'
| 'Reading files'
| 'Renaming file'
tags: never
}

View File

@ -1,5 +1,5 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { ProjectWithEntryPointMetadata } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig' import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
export const homeMachine = createMachine( export const homeMachine = createMachine(

View File

@ -1,99 +0,0 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'done.invoke.create-project': {
type: 'done.invoke.create-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.delete-project': {
type: 'done.invoke.delete-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.read-projects': {
type: 'done.invoke.read-projects'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.rename-project': {
type: 'done.invoke.rename-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.create-project': {
type: 'error.platform.create-project'
data: unknown
}
'error.platform.delete-project': {
type: 'error.platform.delete-project'
data: unknown
}
'error.platform.read-projects': {
type: 'error.platform.read-projects'
data: unknown
}
'error.platform.rename-project': {
type: 'error.platform.rename-project'
data: unknown
}
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {
createProject: 'done.invoke.create-project'
deleteProject: 'done.invoke.delete-project'
readProjects: 'done.invoke.read-projects'
renameProject: 'done.invoke.rename-project'
}
missingImplementations: {
actions: 'navigateToProject' | 'toastError' | 'toastSuccess'
delays: never
guards: 'Has at least 1 project'
services:
| 'createProject'
| 'deleteProject'
| 'readProjects'
| 'renameProject'
}
eventsCausingActions: {
navigateToProject: 'Open project'
setProjects: 'done.invoke.read-projects'
toastError:
| 'error.platform.create-project'
| 'error.platform.delete-project'
| 'error.platform.read-projects'
| 'error.platform.rename-project'
toastSuccess:
| 'done.invoke.create-project'
| 'done.invoke.delete-project'
| 'done.invoke.rename-project'
}
eventsCausingDelays: {}
eventsCausingGuards: {
'Has at least 1 project': 'done.invoke.read-projects'
}
eventsCausingServices: {
createProject: 'Create project'
deleteProject: 'Delete project'
readProjects:
| 'assign'
| 'done.invoke.create-project'
| 'done.invoke.delete-project'
| 'done.invoke.rename-project'
| 'error.platform.create-project'
| 'error.platform.rename-project'
| 'xstate.init'
renameProject: 'Rename project'
}
matchesStates:
| 'Creating project'
| 'Deleting project'
| 'Has no projects'
| 'Has projects'
| 'Opening project'
| 'Reading projects'
| 'Renaming project'
tags: never
}

File diff suppressed because one or more lines are too long

View File

@ -1,123 +0,0 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
internalEvents: {
"": { type: "" };
"done.invoke.get-abs-x-info": { type: "done.invoke.get-abs-x-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-abs-y-info": { type: "done.invoke.get-abs-y-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-angle-info": { type: "done.invoke.get-angle-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-horizontal-info": { type: "done.invoke.get-horizontal-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-length-info": { type: "done.invoke.get-length-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-perpendicular-distance-info": { type: "done.invoke.get-perpendicular-distance-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.get-vertical-info": { type: "done.invoke.get-vertical-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"error.platform.get-abs-x-info": { type: "error.platform.get-abs-x-info"; data: unknown };
"error.platform.get-abs-y-info": { type: "error.platform.get-abs-y-info"; data: unknown };
"error.platform.get-angle-info": { type: "error.platform.get-angle-info"; data: unknown };
"error.platform.get-horizontal-info": { type: "error.platform.get-horizontal-info"; data: unknown };
"error.platform.get-length-info": { type: "error.platform.get-length-info"; data: unknown };
"error.platform.get-perpendicular-distance-info": { type: "error.platform.get-perpendicular-distance-info"; data: unknown };
"error.platform.get-vertical-info": { type: "error.platform.get-vertical-info"; data: unknown };
"xstate.init": { type: "xstate.init" };
"xstate.stop": { type: "xstate.stop" };
};
invokeSrcNameMap: {
"Get ABS X info": "done.invoke.get-abs-x-info";
"Get ABS Y info": "done.invoke.get-abs-y-info";
"Get angle info": "done.invoke.get-angle-info";
"Get horizontal info": "done.invoke.get-horizontal-info";
"Get length info": "done.invoke.get-length-info";
"Get perpendicular distance info": "done.invoke.get-perpendicular-distance-info";
"Get vertical info": "done.invoke.get-vertical-info";
};
missingImplementations: {
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute";
delays: never;
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face" | "has valid extrude selection";
services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
};
eventsCausingActions: {
"AST add line segment": "Add point";
"AST extrude": "Extrude";
"AST start new sketch": "Add point";
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
"Add to other selection": "Select axis";
"Clear selection": "Deselect all";
"Constrain equal length": "Constrain equal length";
"Constrain horizontally align": "Constrain horizontally align";
"Constrain parallel": "Constrain parallel";
"Constrain remove constraints": "Constrain remove constraints";
"Constrain snap to X": "Constrain snap to X";
"Constrain snap to Y": "Constrain snap to Y";
"Constrain vertically align": "Constrain vertically align";
"Make selection horizontal": "Make segment horizontal";
"Make selection vertical": "Make segment vertical";
"Modify AST": "Complete line";
"Remove from code-based selection": "Deselect edge" | "Deselect face" | "Deselect point";
"Remove from other selection": "Deselect axis";
"Set selection": "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info";
"Update code selection cursors": "Complete line" | "Deselect all" | "Deselect axis" | "Deselect edge" | "Deselect face" | "Deselect point" | "Deselect segment" | "Select edge" | "Select face" | "Select point" | "Select segment";
"create path": "Select default plane";
"default_camera_disable_sketch_mode": "Cancel";
"edit mode enter": "Enter sketch" | "Re-execute";
"edit_mode_exit": "Cancel";
"equip select": "CancelSketch" | "Enter sketch" | "Select default plane" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
"hide default planes": "Cancel" | "Select default plane" | "Set selection" | "xstate.stop";
"reset sketch metadata": "Cancel" | "Select default plane";
"set default plane id": "Select default plane";
"set sketch metadata": "Enter sketch";
"set sketchMetadata from pathToNode": "Re-execute";
"set tool": "Equip new tool";
"set tool line": "Equip tool";
"set tool move": "Equip move tool";
"show default planes": "Enter sketch";
"sketch exit execute": "Cancel" | "Complete line" | "Set selection" | "xstate.stop";
"sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
};
eventsCausingDelays: {
};
eventsCausingGuards: {
"Can canstrain parallel": "Constrain parallel";
"Can constrain ABS X": "Constrain ABS X";
"Can constrain ABS Y": "Constrain ABS Y";
"Can constrain angle": "Constrain angle";
"Can constrain equal length": "Constrain equal length";
"Can constrain horizontal distance": "Constrain horizontal distance";
"Can constrain horizontally align": "Constrain horizontally align";
"Can constrain length": "Constrain length";
"Can constrain perpendicular distance": "Constrain perpendicular distance";
"Can constrain remove constraints": "Constrain remove constraints";
"Can constrain snap to X": "Constrain snap to X";
"Can constrain snap to Y": "Constrain snap to Y";
"Can constrain vertical distance": "Constrain vertical distance";
"Can constrain vertically align": "Constrain vertically align";
"Can make selection horizontal": "Make segment horizontal";
"Can make selection vertical": "Make segment vertical";
"Selection contains axis": "Deselect axis";
"Selection contains edge": "Deselect edge";
"Selection contains face": "Deselect face";
"Selection contains line": "Deselect segment";
"Selection contains point": "Deselect point";
"Selection is not empty": "Deselect all";
"Selection is one face": "Enter sketch";
"can move": "";
"can move with execute": "";
"has valid extrude selection": "Extrude";
"is editing existing sketch": "";
};
eventsCausingServices: {
"Get ABS X info": "Constrain ABS X";
"Get ABS Y info": "Constrain ABS Y";
"Get angle info": "Constrain angle";
"Get horizontal info": "Constrain horizontal distance";
"Get length info": "Constrain length";
"Get perpendicular distance info": "Constrain perpendicular distance";
"Get vertical info": "Constrain vertical distance";
};
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
tags: never;
}

View File

@ -1,42 +0,0 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {}
missingImplementations: {
actions: 'toastSuccess'
delays: never
guards: never
services: never
}
eventsCausingActions: {
persistSettings:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
setThemeClass: 'Set Theme' | 'xstate.init'
toastSuccess:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
}
eventsCausingDelays: {}
eventsCausingGuards: {}
eventsCausingServices: {}
matchesStates: 'idle'
tags: never
}

View File

@ -2,15 +2,13 @@ import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals') import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
.then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry)
getCLS(onPerfEntry) getFID(onPerfEntry)
getFID(onPerfEntry) getFCP(onPerfEntry)
getFCP(onPerfEntry) getLCP(onPerfEntry)
getLCP(onPerfEntry) getTTFB(onPerfEntry)
getTTFB(onPerfEntry) })
})
.catch((e) => console.log(e))
} }
} }

View File

@ -14,12 +14,15 @@ import { AppHeader } from '../components/AppHeader'
import ProjectCard from '../components/ProjectCard' import ProjectCard from '../components/ProjectCard'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router' import {
type ProjectWithEntryPointMetadata,
type HomeLoaderData,
} from 'lib/types'
import Loading from '../components/Loading' import Loading from '../components/Loading'
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { homeMachine } from '../machines/homeMachine' import { homeMachine } from '../machines/homeMachine'
import { ContextFrom, EventFrom } from 'xstate' import { ContextFrom, EventFrom } from 'xstate'
import { paths } from '../Router' import { paths } from 'lib/paths'
import { import {
getNextSearchParams, getNextSearchParams,
getSortFunction, getSortFunction,
@ -45,12 +48,18 @@ const Home = () => {
send: sendToSettings, send: sendToSettings,
}, },
} = useGlobalStateContext() } = useGlobalStateContext()
if (newDefaultDirectory) {
sendToSettings({ // Set the default directory if it's been updated
type: 'Set Default Directory', // during the loading of the home page. This is wrapped
data: { defaultDirectory: newDefaultDirectory }, // in a single-use effect to avoid a potential infinite loop.
}) useEffect(() => {
} if (newDefaultDirectory) {
sendToSettings({
type: 'Set Default Directory',
data: { defaultDirectory: newDefaultDirectory },
})
}
}, [])
const [state, send] = useMachine(homeMachine, { const [state, send] = useMachine(homeMachine, {
context: { context: {

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