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]
ignore-words-list: crate,everytime
ignore-words-list: crate,everytime,inout
skip: **/target,node_modules,build,**/Cargo.lock

View File

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

View File

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

View File

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

4
.gitignore vendored
View File

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

View File

@ -10,4 +10,4 @@ src/wasm-lib/kcl/bindings
e2e/playwright/export-snapshots
# 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 { secrets } from './secrets'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { getUtils } from './test-utils'
import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme'
@ -53,40 +51,38 @@ test('Basic sketch', async ({ page }) => {
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await Promise.all([
u.doAndWaitForImageDiff(
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
),
u.waitForDefaultPlanesVisibilityChange(),
])
)
// select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
await u.waitForCmdReceive('set_tool')
await page.mouse.click(700, 200)
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
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 u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
'mouse_click',
false
)
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 startAt = '[18.26, -24.63]'
const num = '18.43'
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
@ -97,27 +93,22 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)`)
|> line([0, ${num + 0.01}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)
|> line([-36.69, 0], %)`)
|> line([0, ${num + 0.01}], %)
|> line([-48, 0], %)`)
// deselect line tool
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await page.getByRole('button', { name: 'Line' }).click()
await page.waitForTimeout(100)
// click between first two clicks to get center of the line
await u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
'select_with_point'
)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
await page.waitForTimeout(100)
// hold down shift
await page.keyboard.down('Shift')
@ -133,7 +124,7 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|> line([0, ${num}], %)
|> line([0, ${num + 0.01}], %)
|> angledLine([180, segLen('seg01', %)], %)`)
})
@ -271,59 +262,41 @@ test('Can create sketches on all planes and their back sides', async ({
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const camCmd: 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 },
},
}
const camPos: [number, number, number] = [100, 100, 100]
const TestSinglePlane = async ({
viewCmd,
expectedCode,
clickCoords,
}: {
viewCmd: EngineCommand
viewCmd: [number, number, number]
expectedCode: string
clickCoords: { x: number; y: number }
}) => {
await u.openDebugPanel()
await u.sendCustomCmd(viewCmd)
await u.updateCamPosition(viewCmd)
await u.clearCommandLogs()
// await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
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()
// draw a line
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 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 page.getByRole('button', { name: 'Line' }).click()
await u.clearCommandLogs()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
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 = (
plane = 'XY',
sign = ''
rounded = false
) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([${sign}6.88, -9.29], %)
|> line([${sign}6.95, 0], %)`
|> startProfileAt([28.91, -39${rounded ? '' : '.01'}], %)`
await TestSinglePlane({
viewCmd: camCmd,
viewCmd: camPos,
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({
viewCmd: camCmd,
expectedCode: codeTemplate('YZ'),
clickCoords: { x: 1000, y: 200 }, // green plane
viewCmd: camPos,
expectedCode: codeTemplate('YZ', true),
clickCoords: { x: 700, y: 300 }, // green plane
})
await TestSinglePlane({
viewCmd: camCmd,
expectedCode: codeTemplate('XZ', '-'),
clickCoords: { x: 630, y: 130 }, // blue plane
viewCmd: camPos,
expectedCode: codeTemplate('XZ'),
clickCoords: { x: 700, y: 80 }, // blue plane
})
// 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 },
},
}
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY', '-'),
clickCoords: { x: 705, y: 136 }, // back of red plane
expectedCode: codeTemplate('-XY', true),
clickCoords: { x: 601, y: 118 }, // back of red plane
})
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-YZ', '-'),
clickCoords: { x: 1000, y: 350 }, // back of green plane
expectedCode: codeTemplate('-YZ'),
clickCoords: { x: 730, y: 219 }, // back of green plane
})
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ'),
clickCoords: { x: 600, y: 400 }, // back of blue plane
expectedCode: codeTemplate('-XZ', true),
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.goto('/')
await u.waitForAuthSkipAppStart()
await u.waitForDefaultPlanesVisibilityChange()
// this test might be brittle as we add and remove functions
// 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 u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const xAxisClick = () => page.mouse.click(700, 250)
const emptySpaceClick = () => page.mouse.click(700, 300)
const topHorzSegmentClick = () => page.mouse.click(700, 285)
const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
const xAxisClick = () =>
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
const emptySpaceClick = () =>
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 page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
// select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
await u.waitForCmdReceive('set_tool')
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
const startXPx = 600
await u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
'mouse_click',
false
)
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 u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const startAt = '[18.26, -24.63]'
const num = '18.43'
const num2 = '36.69'
const num = 24.11
const num2 = '48'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
@ -520,20 +479,17 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)`)
|> line([0, ${num + 0.01}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)
|> line([0, ${num + 0.01}], %)
|> line([-${num2}, 0], %)`)
// deselect line tool
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel()
const selectionSequence = async () => {
@ -555,79 +511,72 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// now check clicking works including axis
// 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')
const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled()
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await xAxisClick()
await page.keyboard.up('Shift')
await absYButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
await emptySpaceClick()
// same selection but click the axis first
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await xAxisClick()
await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift')
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
await topHorzSegmentClick()
await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled()
// 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
await u.doAndWaitForCmd(
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
'select_clear',
false
)
await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled()
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await xAxisClick()
await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled()
// 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
await u.doAndWaitForCmd(
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
'select_clear',
false
)
await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.waitForTimeout(300)
await page.keyboard.down('Shift')
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 expect(page.locator('.cm-cursor')).toHaveCount(2)
// clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
await emptySpaceClick()
}
await selectionSequence()
// hovering in fresh sketch worked, lets try exiting and re-entering
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
'edit_mode_exit'
)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// select a line
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
await topHorzSegmentClick()
await page.waitForTimeout(200)
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
'edit_mode_enter',
false
)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(700) // wait for animation
// hover again and check it works
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.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K')
@ -735,3 +686,127 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|> 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.openAndClearDebugPanel()
const camCmd: EngineCommand = {
type: 'modeling_cmd_req',
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')
const camPos: [number, number, number] = [0, 85, 85]
await u.updateCamPosition(camPos)
// rotate
await u.closeDebugPanel()
@ -62,13 +51,11 @@ test('change camera, show planes', async ({ page, context }) => {
await page.mouse.up({ button: 'right' })
await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(500)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
@ -77,10 +64,8 @@ test('change camera, show planes', async ({ page, context }) => {
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
@ -93,12 +78,10 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.up('Shift')
await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
@ -107,10 +90,8 @@ test('change camera, show planes', async ({ page, context }) => {
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
@ -119,17 +100,15 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
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.keyboard.up('Control')
await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
@ -191,7 +170,6 @@ const part001 = startSketchOn('-XZ')
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
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),
removeCurrentCode: () => removeCurrentCode(page),
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),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
waitForDefaultPlanesVisibilityChange: () =>
waitForDefaultPlanesToBeVisible(page),
openDebugPanel: () => openDebugPanel(page),
closeDebugPanel: () => closeDebugPanel(page),
openAndClearDebugPanel: async () => {

View File

@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.46",
"@kittycad/lib": "^0.0.50",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
@ -21,6 +21,7 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@types/node": "^16.7.13",
"@types/react": "^18.2.41",
"@types/react-dom": "^18.0.0",
@ -45,6 +46,7 @@
"sketch-helpers": "^0.0.4",
"swr": "^2.2.2",
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
"three": "^0.160.0",
"toml": "^3.0.0",
"ts-node": "^10.9.1",
"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\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"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": {
"trailingComma": "es5",
@ -113,6 +117,7 @@
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3",
"@types/three": "^0.160.0",
"@types/uuid": "^9.0.4",
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2020.9.6",
@ -124,15 +129,18 @@
"@wdio/local-runner": "^8.24.3",
"@wdio/mocha-framework": "^8.24.3",
"@wdio/spec-reporter": "^8.24.2",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.13",
"eslint": "^8.53.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^10.8.0",
"husky": "^8.0.3",
"patch-package": "^8.0.0",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.31",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6",
@ -140,6 +148,7 @@
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.2.1",
"vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"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",
"tauri",
"tauri-build",
"tauri-driver",
"tauri-plugin-fs-extra",
"tokio",
"toml 0.8.2",
@ -1307,6 +1308,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "html5ever"
version = "0.25.2"
@ -2538,6 +2548,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "pico-args"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "pin-project"
version = "1.1.3"
@ -3419,6 +3435,37 @@ dependencies = [
"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]]
name = "simd-adler32"
version = "0.3.5"
@ -3855,6 +3902,25 @@ dependencies = [
"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]]
name = "tauri-macros"
version = "1.4.2"
@ -4062,9 +4128,21 @@ dependencies = [
"num_cpus",
"pin-project-lite",
"socket2 0.5.5",
"tokio-macros",
"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]]
name = "tokio-native-tls"
version = "0.3.1"
@ -4617,6 +4695,18 @@ dependencies = [
"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]]
name = "winapi"
version = "0.3.9"
@ -4743,6 +4833,15 @@ dependencies = [
"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]]
name = "windows-targets"
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 { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData } from 'react-router-dom'
import { IndexLoaderData } from './Router'
import { IndexLoaderData } from 'lib/types'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu'
@ -31,6 +31,8 @@ import { Themes, getSystemTheme } from 'lib/theme'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { ClientSideScene } from 'clientSideScene/setup'
// import { CamToggle } from 'components/CamToggle'
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
@ -83,10 +85,12 @@ export function App() {
useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => {
void engineCommandManager.sendSceneCommand(message)
}, 16)
engineCommandManager.sendSceneCommand(message)
}, 1000 / 15)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault()
if (state.matches('Sketch')) {
return
}
const { x, y } = getNormalisedCoordinates({
clientX: e.clientX,
@ -97,16 +101,6 @@ export function App() {
const newCmdId = uuidv4()
if (buttonDownInStream === undefined) {
if (state.matches('Sketch.Line Tool')) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'mouse_move',
window: { x, y },
},
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
@ -115,19 +109,7 @@ export function App() {
},
cmd_id: newCmdId,
})
}
} 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]
let interaction: CameraDragInteractionType_type
@ -238,6 +220,7 @@ export function App() {
open={openPanes.includes('debug')}
/>
)}
{/* <CamToggle /> */}
</div>
)
}

View File

@ -14,10 +14,7 @@ import {
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, {
onboardingRoutes,
onboardingPaths,
} from './routes/Onboarding'
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
import SignIn from './routes/SignIn'
import { Auth } from './Auth'
import { isTauri } from './lib/isTauri'
@ -29,7 +26,7 @@ import {
isProjectDirectory,
PROJECT_ENTRYPOINT,
} 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 { WasmErrBanner } from './components/WasmErrBanner'
import { GlobalStateProvider } from './components/GlobalStateProvider'
@ -42,9 +39,11 @@ import CommandBarProvider from 'components/CommandBar/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSinglton'
import { KclContextProvider, kclManager } from 'lang/KclSingleton'
import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path'
import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
if (VITE_KC_SENTRY_DSN && !TEST) {
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 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]
const addGlobalContextToElements = (
@ -146,18 +110,18 @@ const router = createBrowserRouter(
{
path: paths.FILE + '/:id',
element: (
<KclContextProvider>
<Auth>
<FileMachineProvider>
<KclContextProvider>
<ModelingMachineProvider>
<Outlet />
<App />
</ModelingMachineProvider>
<WasmErrBanner />
</KclContextProvider>
</FileMachineProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth>
</KclContextProvider>
),
id: paths.FILE,
loader: async ({
@ -249,7 +213,7 @@ const router = createBrowserRouter(
<Home />
</Auth>
),
loader: async () => {
loader: async (): Promise<HomeLoaderData | Response> => {
if (!isTauri()) {
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
}

View File

@ -89,15 +89,16 @@ export const Toolbar = () => {
</li>
)}
{state.matches('Sketch') && !state.matches('idle') && (
<li className="contents">
<>
<li className="contents" key="line-button">
<ActionButton
Element="button"
onClick={() =>
state.matches('Sketch.Line Tool')
state?.matches('Sketch.Line tool')
? send('CancelSketch')
: send('Equip tool')
: send('Equip Line tool')
}
aria-pressed={state.matches('Sketch.Line Tool')}
aria-pressed={state?.matches('Sketch.Line tool')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'line',
@ -107,26 +108,29 @@ export const Toolbar = () => {
Line
</ActionButton>
</li>
)}
{state.matches('Sketch') && (
<li className="contents">
<li className="contents" key="tangential-arc-button">
<ActionButton
Element="button"
onClick={() =>
state.matches('Sketch.Move Tool')
state.matches('Sketch.Tangential arc to')
? send('CancelSketch')
: send('Equip move tool')
: send('Equip tangential arc to')
}
aria-pressed={state.matches('Sketch.Move Tool')}
aria-pressed={state.matches('Sketch.Tangential arc to')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'move',
icon: 'line',
bgClassName,
}}
disabled={
!state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')
}
>
Move
Tangential Arc
</ActionButton>
</li>
</>
)}
{state.matches('Sketch.SketchIdle') &&
state.nextEvents
@ -151,7 +155,7 @@ export const Toolbar = () => {
return 0
})
.map((eventName) => (
<li className="contents">
<li className="contents" key={eventName}>
<ActionButton
Element="button"
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 React from 'react'
import { paths } from '../Router'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom'
import type { LinkProps } from 'react-router-dom'

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import {
} from '../lang/modifyAst'
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { kclManager, useKclContext } from 'lang/KclSingleton'
import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'useStore'
@ -138,22 +138,21 @@ export function useCalc({
}, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(() => {
const execAstAndSetResult = async () => {
try {
const code = `const __result__ = ${value}`
const ast = parse(code)
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
const { programMemory } = await executeAst({
executeAst({
ast,
engineCommandManager,
defaultPlanes: kclManager.defaultPlanes,
useFakeExecutor: true,
programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory)
),
})
}).then(({ programMemory }) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
@ -165,11 +164,11 @@ export function useCalc({
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}
execAstAndSetResult().catch(() => {
})
} catch (e) {
setCalcResult('NAN')
setValueNode(null)
})
}
}, [value, availableVarInfo])
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 { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lang/KclSinglton'
import { kclManager } from 'lang/KclSingleton'
export const CodeMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =

View File

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

View File

@ -1,6 +1,7 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/setup'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return (
@ -15,6 +16,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
>
<section className="p-4 flex flex-col gap-4">
<EngineCommands />
<CamDebugSettings />
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
</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') {
values.selection = { type: 'default_scene' }
}
void engineCommandManager.sendSceneCommand({
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',

View File

@ -1,6 +1,7 @@
import { useMachine } from '@xstate/react'
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 { toast } from 'react-hot-toast'
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 Tooltip from './Tooltip'
import { FileEntry } from '@tauri-apps/api/fs'

View File

@ -1,6 +1,6 @@
import { useMachine } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import { paths } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react'
@ -101,7 +101,7 @@ export const GlobalStateProvider = ({
goToSignInPage: () => {
navigate(paths.SIGN_IN)
void logout()
logout()
},
goToIndexPage: () => {
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 { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSinglton'
import { useKclContext } from 'lang/KclSingleton'
const ReactJsonTypeHack = ReactJson as any

View File

@ -41,9 +41,9 @@ describe('processMemory', () => {
otherVar: 3,
theExtrude: [],
theSketch: [
{ 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: [2.15, 4.32], from: [0.98, 5.16], 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: [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 { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSinglton'
import { useKclContext } from 'lang/KclSingleton'
interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>

View File

@ -13,23 +13,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
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 { kclManager, useKclContext } from 'lang/KclSingleton'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
angleBetweenInfo,
@ -49,6 +33,9 @@ import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
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> = {
state: StateFrom<T>
@ -92,181 +79,10 @@ export const ModelingMachineProvider = ({
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine,
{
// context: persistedSettings,
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': () => {
void kclManager.executeAst()
kclManager.executeAst()
},
'set tool': () => {}, // TODO
'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
const setSelections = event.data
@ -274,36 +90,6 @@ export const ModelingMachineProvider = ({
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
return { selectionRanges: setSelections.selection }
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 {
codeMirrorSelection,
selectionRangeTypeMap,
@ -384,12 +170,6 @@ export const ModelingMachineProvider = ({
}),
},
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 }) => {
// 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
@ -409,6 +189,29 @@ export const ModelingMachineProvider = ({
},
},
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 ({
selectionRanges,
}): 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(() => {
kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' })
@ -565,10 +357,7 @@ export const ModelingMachineProvider = ({
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
onCancel: () => {
console.log('firing onCancel!!')
modelingSend({ type: 'Cancel' })
},
onCancel: () => modelingSend({ type: 'Cancel' }),
})
return (

View File

@ -1,5 +1,6 @@
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 { ActionButton } from './ActionButton'
import {

View File

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

View File

@ -1,7 +1,8 @@
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
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 { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton'

View File

@ -11,16 +11,13 @@ import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib'
import { getNodeFromPath } from 'lang/queryAst'
import { VariableDeclarator, recast, CallExpression } from 'lang/wasm'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { changeSketchArguments } from 'lang/std/sketch'
import { useKclContext } from 'lang/KclSingleton'
import { ClientSideScene } from 'clientSideScene/setup'
export const Stream = ({ className = '' }) => {
export const Stream = ({ className = '' }: { className?: string }) => {
const [isLoading, setIsLoading] = useState(true)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null)
@ -39,7 +36,7 @@ export const Stream = ({ className = '' }) => {
}))
const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls
const { send, state, context } = useModelingContext()
const { state } = useModelingContext()
const { isExecuting } = useKclContext()
useEffect(() => {
@ -53,8 +50,10 @@ export const Stream = ({ className = '' }) => {
videoRef.current.srcObject = mediaStream
}, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!videoRef.current) return
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({
clientX: e.clientX,
clientY: e.clientY,
@ -62,55 +61,6 @@ export const Stream = ({ className = '' }) => {
...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)
setClickCoords({ x, y })
}
@ -118,7 +68,7 @@ export const Stream = ({ className = '' }) => {
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
void engineCommandManager.sendSceneCommand({
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
@ -128,13 +78,15 @@ export const Stream = ({ className = '' }) => {
})
}, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
clientX,
clientY,
ctrlKey,
}) => {
if (!videoRef.current) return
setButtonDownInStream(undefined)
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
@ -155,208 +107,21 @@ export const Stream = ({ className = '' }) => {
cmd_id: newCmdId,
}
if (!didDragInStream && state.matches('Sketch no face')) {
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'))
) {
if (!didDragInStream) {
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 = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
void engineCommandManager.sendSceneCommand(command)
} else if (didDragInStream && state.matches('Sketch.Move Tool')) {
engineCommandManager.sendSceneCommand(command)
} else if (didDragInStream) {
command.cmd = {
type: 'handle_mouse_drag_end',
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)
} else {
engineCommandManager.sendSceneCommand(command)
}
setDidDragInStream(false)
@ -364,6 +129,8 @@ export const Stream = ({ className = '' }) => {
}
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
if (!clickCoords) return
const delta =
@ -376,16 +143,19 @@ export const Stream = ({ className = '' }) => {
}
return (
<div id="stream" className={className}>
<div
id="stream"
className={className}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
>
<video
ref={videoRef}
muted
autoPlay
controls={false}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove}
@ -393,6 +163,7 @@ export const Stream = ({ className = '' }) => {
disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/>
<ClientSideScene cameraControls={settings.context.cameraControls} />
{isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<Loading>

View File

@ -11,7 +11,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme'
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { linter, lintGutter } from '@codemirror/lint'
import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
@ -24,7 +24,9 @@ import { CSSRuleObject } from 'tailwindcss/types/config'
import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact'
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 = {
formatCode: {
@ -56,10 +58,12 @@ export const TextEditor = ({
isShiftDown: s.isShiftDown,
}))
const { code, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const {
context: { selectionRanges, selectionRangeTypeMap },
send,
state,
} = useModelingContext()
const { settings: { context: { textWrapping } = {} } = {} } =
@ -76,12 +80,10 @@ export const TextEditor = ({
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer)
.then((lspServer) => {
void lspServer.start()
Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start()
setIsLSPServerReady(true)
})
.catch((e) => console.log(e))
}
const lspClient = new LanguageServerClient({ client })
@ -117,6 +119,12 @@ export const TextEditor = ({
if (!editorView) {
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({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges,
@ -124,7 +132,20 @@ export const TextEditor = ({
isShiftDown,
})
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)
eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event)
@ -153,7 +174,7 @@ export const TextEditor = ({
key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => {
if (convertEnabled) {
void convertCallback()
convertCallback()
return true
}
return false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import {
} from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lang/KclSinglton'
import { kclManager } from 'lang/KclSingleton'
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 { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useState } from 'react'
import { paths } from '../Router'
import { paths } from 'lib/paths'
import { Models } from '@kittycad/lib'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import {
} from './std/engineConnection'
import { deferExecution } from 'lib/utils'
import {
CallExpression,
initPromise,
parse,
PathToNode,
@ -17,7 +18,7 @@ import {
import { bracket } from 'lib/exampleKcl'
import { createContext, useContext, useEffect, useState } from 'react'
import { getNodeFromPath } from './queryAst'
import { IndexLoaderData } from 'Router'
import { type IndexLoaderData } from 'lib/types'
import { Params, useLoaderData } from 'react-router-dom'
import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/api/fs'
@ -59,7 +60,7 @@ class KclManager {
} catch (e) {
console.error(e)
}
void this.executeAst(ast)
this.executeAst(ast)
}, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {}
@ -98,7 +99,7 @@ class KclManager {
})
})
} else {
localStorage.setItem(PERSIST_CODE_TOKEN, code)
localStorage?.setItem(PERSIST_CODE_TOKEN, code)
}
}
@ -110,10 +111,6 @@ class KclManager {
this._programMemoryCallBack(programMemory)
}
get defaultPlanes() {
return this?.engineCommandManager?.defaultPlanes
}
get logs() {
return this._logs
}
@ -168,11 +165,13 @@ class KclManager {
zustandStore.state.code = ''
localStorage.setItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) {
console.log('stored brack thing')
this.code = bracket
} else {
this.code = storedCode
}
this.ensureWasmInit().then(() => {
this.ast = this.safeParse(this.code) || this.ast
})
}
registerCallBacks({
setCode,
@ -235,7 +234,6 @@ class KclManager {
const { logs, errors, programMemory } = await executeAst({
ast,
engineCommandManager: this.engineCommandManager,
defaultPlanes: this.defaultPlanes,
})
this.isExecuting = false
this.logs = logs
@ -251,13 +249,20 @@ class KclManager {
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()
const newCode = recast(ast)
const newAst = this.safeParse(newCode)
if (!newAst) return
await this?.engineCommandManager?.waitForReady
if (updateCode) {
if (updates !== 'none') {
this.setCode(recast(ast))
}
this._ast = { ...newAst }
@ -265,22 +270,38 @@ class KclManager {
const { logs, errors, programMemory } = await executeAst({
ast: newAst,
engineCommandManager: this.engineCommandManager,
defaultPlanes: this.defaultPlanes,
useFakeExecutor: true,
})
this._logs = logs
this._kclErrors = errors
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) {
await this.ensureWasmInit()
await this?.engineCommandManager?.waitForReady
if (!this?.engineCommandManager?.planesInitialized()) return
const result = await executeCode({
engineCommandManager,
code: code || this._code,
lastAst: this._ast,
defaultPlanes: this.defaultPlanes,
force: false,
})
if (!result.isChange) return
@ -366,26 +387,10 @@ class KclManager {
// 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
// instead.
await this.executeAstMock(astWithUpdatedSource, true)
await this.executeAstMock(astWithUpdatedSource, { updates: 'code' })
}
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)

View File

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

View File

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

View File

@ -30,41 +30,28 @@ import {
createFirstArg,
} from './std/sketch'
import { isLiteralArrayOrStatic } from './std/sketchcombos'
import { DefaultPlaneStr } from 'clientSideScene/clientSideScene'
import { roundOff } from 'lib/utils'
export function addStartSketch(
export function startSketchOnDefault(
node: Program,
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz',
start: [number, number],
end: [number, number]
axis: DefaultPlaneStr,
name = ''
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
const _node = { ...node }
const _name = findUniqueName(node, 'part')
const _name = name || findUniqueName(node, 'part')
const startSketchOn = createCallExpressionStdLib('startSketchOn', [
createLiteral(axis.toUpperCase()),
])
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
createPipeSubstitution(),
])
const initialLineTo = createCallExpression('line', [
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
createPipeSubstitution(),
createLiteral(axis),
])
const pipeBody = [startSketchOn, startProfileAt, initialLineTo]
const variableDeclaration = createVariableDeclaration(
_name,
createPipeExpression(pipeBody)
)
const newIndex = node.body.length
const variableDeclaration = createVariableDeclaration(_name, startSketchOn)
_node.body = [...node.body, variableDeclaration]
const sketchIndex = _node.body.length - 1
let pathToNode: PathToNode = [
['body', ''],
[newIndex.toString(10), 'index'],
[sketchIndex, 'index'],
['declarations', 'VariableDeclaration'],
['0', 'index'],
['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(
node: Program,
axis: 'xy' | 'xz' | 'yz',

View File

@ -65,13 +65,13 @@ export function getNodeFromPath<T>(
}
}
} catch (e) {
console.error(
`Could not find path ${pathItem} in node ${JSON.stringify(
currentNode,
null,
2
)}, successful path was ${successfulPaths}`
)
// console.error(
// `Could not find path ${pathItem} in node ${JSON.stringify(
// currentNode,
// null,
// 2
// )}, successful path was ${successfulPaths}`
// )
}
}
return {
@ -266,6 +266,7 @@ function moreNodePathFromSourceRange(
}
}
}
if (_node.type === 'PipeSubstitution' && isInRange) return path
console.error('not implemented: ' + node.type)
return path
}
@ -489,7 +490,7 @@ export function isLinesParallelAndConstrained(
const constraintLevel = getConstraintLevelFromSourceRange(
secondaryLine.range,
ast
)
).level
const isConstrained =
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 { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
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 = ''
interface CommandInfo {
commandType: CommandTypes
range: SourceRange
pathToNode: PathToNode
parentId?: string
}
@ -601,9 +603,14 @@ class EngineConnection {
})
.join('\n')
if (message.request_id) {
const artifactThatFailed =
engineCommandManager.artifactMap[message.request_id] ||
engineCommandManager.lastArtifactMap[message.request_id]
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 {
console.error(`Error from server:\n${errorsString}`)
}
@ -618,8 +625,6 @@ class EngineConnection {
return
}
console.log('received', resp)
switch (resp.type) {
case 'ice_server_info':
let ice_servers = resp.data?.ice_servers
@ -863,10 +868,10 @@ export type CommandLog =
export class EngineCommandManager {
artifactMap: ArtifactMap = {}
lastArtifactMap: ArtifactMap = {}
private getAst: () => Program = () => ({ start: 0, end: 0, body: [] } as any)
outSequence = 1
inSequence = 1
engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes = { xy: '', yz: '', xz: '' }
_commandLogs: CommandLog[] = []
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
// Folks should realize that wait for ready does not get called _everytime_
@ -890,6 +895,11 @@ export class EngineCommandManager {
constructor() {
this.engineConnection = undefined
;(async () => {
// circular dependency needs one to be lazy loaded
const { kclManager } = await import('lang/KclSingleton')
this.getAst = () => kclManager.ast
})()
}
start({
@ -946,16 +956,9 @@ export class EngineCommandManager {
gizmo_mode: true,
},
})
setupSingleton.onStreamStart()
// Initialize the planes.
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: () => {
setIsStreamReady(false)
@ -1075,6 +1078,7 @@ export class EngineCommandManager {
this.artifactMap[id] = {
type: 'result',
range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse,
@ -1092,6 +1096,7 @@ export class EngineCommandManager {
type: 'result',
commandType: command?.commandType,
range: command?.range,
pathToNode: command?.pathToNode,
data: modelingResponse,
raw: message,
}
@ -1109,6 +1114,7 @@ export class EngineCommandManager {
this.artifactMap[id] = {
type: 'failed',
range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
errors,
@ -1123,6 +1129,7 @@ export class EngineCommandManager {
this.artifactMap[id] = {
type: 'failed',
range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
errors,
@ -1218,7 +1225,10 @@ export class EngineCommandManager {
registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
this._commandLogCallBack = callback
}
sendSceneCommand(command: EngineCommand): Promise<any> {
sendSceneCommand(
command: EngineCommand,
forceWebsocket = false
): Promise<any> {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
@ -1232,7 +1242,9 @@ export class EngineCommandManager {
command.type === 'modeling_cmd_req' &&
(command.cmd.type === 'highlight_set_entity' ||
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
@ -1249,14 +1261,23 @@ export class EngineCommandManager {
console.log('sending command', 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()
const cmd = command.cmd
if (
(cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') &&
this.engineConnection?.unreliableDataChannel
cmd.type === 'handle_mouse_drag_move' ||
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.engineConnection?.unreliableSend(command)
return Promise.resolve()
@ -1277,6 +1298,12 @@ export class EngineCommandManager {
this.engineConnection?.unreliableSend(command)
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
this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id, command.cmd)
@ -1285,10 +1312,12 @@ export class EngineCommandManager {
id,
range,
command,
ast,
}: {
id: string
range: SourceRange
command: EngineCommand | string
ast: Program
}): Promise<any> {
if (this.engineConnection === undefined) {
return Promise.resolve()
@ -1310,17 +1339,18 @@ export class EngineCommandManager {
}
this.engineConnection?.send(command)
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') {
const parseCommand: EngineCommand = JSON.parse(command)
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')
}
handlePendingCommand(
id: string,
command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange
) {
let resolve: (val: any) => void = () => {}
@ -1333,8 +1363,12 @@ export class EngineCommandManager {
}
// TODO handle other commands that have a parent
}
const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0])
: []
this.artifactMap[id] = {
range: range || [0, 0],
pathToNode,
type: 'pending',
commandType: command.type,
parentId: getParentId(),
@ -1363,9 +1397,12 @@ export class EngineCommandManager {
const range: SourceRange = JSON.parse(rangeStr)
// We only care about the modeling command response.
return this.sendModelingCommand({ id, range, command: commandStr }).then(
({ raw }) => JSON.stringify(raw)
)
return this.sendModelingCommand({
id,
range,
command: commandStr,
ast: this.getAst(),
}).then(({ raw }) => JSON.stringify(raw))
}
commandResult(id: string): Promise<any> {
const command = this.artifactMap[id]
@ -1392,102 +1429,6 @@ export class EngineCommandManager {
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()

View File

@ -45,7 +45,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
} else if (!currentPath) {
return [0, 0]
}
if (currentPath.type === 'topoint') {
if (currentPath.type === 'ToPoint') {
return [currentPath.to[0], currentPath.to[1]]
}
return [0, 0]
@ -445,6 +445,85 @@ export const yLine: SketchLineHelper = {
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 = {
add: ({
node,
@ -900,6 +979,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
angledLineToX,
angledLineToY,
angledLineThatIntersects,
tangentialArcTo,
} as const
export function changeSketchArguments(
@ -942,14 +1022,28 @@ interface CreateLineFnCallArgs {
export function compareVec2Epsilon(
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 yDifference = Math.abs(vec1[1] - vec2[1])
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({
node: _node,
programMemory: previousProgramMemory,
@ -1288,5 +1382,9 @@ export function getFirstArg(callExp: CallExpression): {
if (['angledLineThatIntersects'].includes(name)) {
return getAngledLineThatIntersects(callExp)
}
if (['tangentialArcTo'].includes(name)) {
// TODO probably needs it's own implementation
return getFirstArgValuesForXYFns(callExp)
}
throw new Error('unexpected call expression')
}

View File

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

View File

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

View File

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

View File

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

View File

@ -26,15 +26,6 @@ export function pathMapToSelections(
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(
artifactMap: ArtifactMap,
selectionRanges: Selections

View File

@ -4,6 +4,8 @@ import init, {
execute_wasm,
lexer_wasm,
modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
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 type { Program } from '../wasm-lib/kcl/bindings/Program'
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 { Value } from '../wasm-lib/kcl/bindings/Value'
@ -118,15 +120,13 @@ export interface ProgramMemory {
export const executor = async (
node: Program,
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
planes: DefaultPlanes
engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => {
engineCommandManager.startNewSession()
const _programMemory = await _executor(
node,
programMemory,
engineCommandManager,
planes
engineCommandManager
)
await engineCommandManager.waitForAllCommands()
@ -137,15 +137,13 @@ export const executor = async (
export const _executor = async (
node: Program,
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
planes: DefaultPlanes
engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => {
try {
const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node),
JSON.stringify(programMemory),
engineCommandManager,
JSON.stringify(planes)
engineCommandManager
)
return memory
} catch (e: any) {
@ -212,3 +210,44 @@ export const modifyAstForSketch = async (
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
}
interface MouseGuard {
export interface MouseGuard {
pan: MouseGuardHandler
zoom: MouseGuardZoomHandler
rotate: MouseGuardHandler

View File

@ -1,3 +1,6 @@
export function isTauri(): boolean {
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 { engineCommandManager } from 'lang/std/engineConnection'
import { SourceRange } from 'lang/wasm'
import { CallExpression, SourceRange, parse, recast } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { v4 as uuidv4 } from 'uuid'
import { EditorSelection } from '@codemirror/state'
import { kclManager } from 'lang/KclSinglton'
import { kclManager } from 'lang/KclSingleton'
import { SelectionRange } from '@uiw/react-codemirror'
import { isOverlap } from 'lib/utils'
import { isCursorInSketchCommandRange } from 'lang/util'
import { Program } from 'lang/wasm'
import { doesPipeHaveCallExp } from 'lang/queryAst'
import { doesPipeHaveCallExp, getNodeFromPath } from 'lang/queryAst'
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 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({
selections,
}: {
@ -239,7 +283,6 @@ export function handleSelectionWithShift({
})
}
if (otherSelection) {
console.log('otherSelection in handleSelectionWithShift', otherSelection)
return handleSelectionBatch({
selections: {
codeBasedSelections: isShiftDown
@ -335,6 +378,7 @@ export function processCodeMirrorRanges({
.filter(Boolean) as any
if (!selectionRanges) return null
updateSceneObjectColors(codeBasedSelections)
return {
modelingEvent: {
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(
selections: SelectionToEngine[]
): Models['WebSocketRequest_type'][] {

View File

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

View File

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

View File

@ -66,11 +66,7 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager
await mockEngineCommandManager.waitForReady
mockEngineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, mockEngineCommandManager, {
xy: uuidv4(),
yz: uuidv4(),
xz: uuidv4(),
})
const programMemory = await _executor(ast, pm, mockEngineCommandManager)
await mockEngineCommandManager.waitForAllCommands()
return programMemory
}
@ -89,11 +85,7 @@ export async function executor(
})
await engineCommandManager.waitForReady
engineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, engineCommandManager, {
xy: uuidv4(),
yz: uuidv4(),
xz: uuidv4(),
})
const programMemory = await _executor(ast, pm, engineCommandManager)
await engineCommandManager.waitForAllCommands()
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),
}
}
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 { ProjectWithEntryPointMetadata } from 'Router'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { FileEntry } from '@tauri-apps/api/fs'
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 { ProjectWithEntryPointMetadata } from '../Router'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
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) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals')
.then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(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 { useLoaderData, useNavigate, useSearchParams } 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 { useMachine } from '@xstate/react'
import { homeMachine } from '../machines/homeMachine'
import { ContextFrom, EventFrom } from 'xstate'
import { paths } from '../Router'
import { paths } from 'lib/paths'
import {
getNextSearchParams,
getSortFunction,
@ -45,12 +48,18 @@ const Home = () => {
send: sendToSettings,
},
} = useGlobalStateContext()
// Set the default directory if it's been updated
// during the loading of the home page. This is wrapped
// 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, {
context: {

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