Compare commits
19 Commits
lee/native
...
kurt-add-s
Author | SHA1 | Date | |
---|---|---|---|
3e0c44e689 | |||
8666989c85 | |||
bdf49c2084 | |||
a06b9d560a | |||
b81ff66f2b | |||
c0e6947170 | |||
65ebde0b34 | |||
0d6618b60a | |||
f0c44d11b3 | |||
44e71cd4bc | |||
a9f716dad8 | |||
a2455832e7 | |||
8f5034f997 | |||
af1c2c7ae1 | |||
ff38ae091e | |||
1dd7c95b8c | |||
20042ec87c | |||
fccf3508a7 | |||
8dab5527b8 |
@ -3,4 +3,4 @@ VITE_KC_API_BASE_URL=https://api.zoo.dev
|
|||||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
VITE_KC_SENTRY_DSN=
|
||||||
|
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report for the Zoo Modeling App
|
||||||
|
title: "[BUG]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us resolve it."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: describe-bug
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: "Explain the bug..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-bug
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: Description of what you expected to happen.
|
||||||
|
placeholder: "I expected that..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots and Recordings
|
||||||
|
description: If applicable, add screenshots to help explain your problem. Maximum upload size is 10MB.
|
||||||
|
placeholder: "You can attach images or video recordings here."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: desktop-os
|
||||||
|
attributes:
|
||||||
|
label: Desktop OS
|
||||||
|
description: "Your operating system"
|
||||||
|
placeholder: "example: Windows 10, MacOS Big Sur"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser
|
||||||
|
description: "If you are using the web version, please specify the browser you are using."
|
||||||
|
placeholder: "example: Chrome, Safari"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: "The version of the Zoo Modeling App you're using."
|
||||||
|
placeholder: "example: v0.15.0. You can find this in the settings."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
placeholder: "Anything else you want to add..."
|
||||||
|
validations:
|
||||||
|
required: false
|
1
.github/workflows/ci.yml
vendored
@ -374,6 +374,7 @@ jobs:
|
|||||||
announce_release:
|
announce_release:
|
||||||
needs: [publish-apps-release]
|
needs: [publish-apps-release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'release'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
2
.github/workflows/playwright.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- uses: KittyCAD/action-install-cli@v0.2.16
|
- uses: KittyCAD/action-install-cli@v0.2.21
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
|
@ -6,11 +6,13 @@ once fixed in engine will just start working here with no language changes.
|
|||||||
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
|
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
|
||||||
are sketching) you will get multiple models returned instead of one single
|
are sketching) you will get multiple models returned instead of one single
|
||||||
model for that sketch and its underlying 3D object.
|
model for that sketch and its underlying 3D object.
|
||||||
|
If you see a red line around your model, it means this is happening.
|
||||||
|
|
||||||
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
|
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
|
||||||
item in the pattern is being subtracted. This is an engine bug that is being
|
item in the pattern is being subtracted. This is an engine bug that is being
|
||||||
worked on.
|
worked on.
|
||||||
|
|
||||||
- **Import**: Right now you can import a file, even if that file has brep data
|
- **Import**: Right now you can import a file, even if that file has brep data
|
||||||
you cannot edit it. You also cannot move or transform the imported objects at
|
you cannot edit it, after v1, the engine will account for this. You also cannot
|
||||||
all. In the future, after v1, the engine will account for this.
|
currently move or transform the imported objects at all, once we have assemblies
|
||||||
|
this will work.
|
||||||
|
4524
docs/kcl/std.json
640
docs/kcl/std.md
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 259 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
@ -14,6 +14,12 @@ document.addEventListener('mousemove', (e) =>
|
|||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const commonPoints = {
|
||||||
|
startAt: '[26.38, -35.59]',
|
||||||
|
num1: 26.63,
|
||||||
|
num2: 53.01,
|
||||||
|
}
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ context, page }) => {
|
||||||
// wait for Vite preview server to be up
|
// wait for Vite preview server to be up
|
||||||
await waitOn({
|
await waitOn({
|
||||||
@ -52,6 +58,9 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
@ -72,35 +81,34 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
const startAt = '[23.74, -32.03]'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const num = 23.97
|
const num = 26.63
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> line([-47.71, 0], %)`)
|
|> line([-${commonPoints.num2}, 0], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await page.getByRole('button', { name: 'Line' }).click()
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
@ -122,9 +130,9 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -305,11 +313,9 @@ test('Can create sketches on all planes and their back sides', async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const codeTemplate = (
|
const codeTemplate = (
|
||||||
plane = 'XY',
|
plane = 'XY'
|
||||||
rounded = false,
|
|
||||||
otherThing = '1'
|
|
||||||
) => `const part001 = startSketchOn('${plane}')
|
) => `const part001 = startSketchOn('${plane}')
|
||||||
|> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)`
|
|> startProfileAt([32.13, -43.34], %)`
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camPos,
|
viewCmd: camPos,
|
||||||
expectedCode: codeTemplate('XY'),
|
expectedCode: codeTemplate('XY'),
|
||||||
@ -318,7 +324,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
|||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camPos,
|
viewCmd: camPos,
|
||||||
expectedCode: codeTemplate('YZ', true),
|
expectedCode: codeTemplate('YZ'),
|
||||||
clickCoords: { x: 700, y: 300 }, // green plane
|
clickCoords: { x: 700, y: 300 }, // green plane
|
||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
@ -329,7 +335,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
|||||||
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
|
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmdBackSide,
|
viewCmd: camCmdBackSide,
|
||||||
expectedCode: codeTemplate('-XY', false, '3'),
|
expectedCode: codeTemplate('-XY'),
|
||||||
clickCoords: { x: 601, y: 118 }, // back of red plane
|
clickCoords: { x: 601, y: 118 }, // back of red plane
|
||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
@ -339,7 +345,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
|||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmdBackSide,
|
viewCmd: camCmdBackSide,
|
||||||
expectedCode: codeTemplate('-XZ', true),
|
expectedCode: codeTemplate('-XZ'),
|
||||||
clickCoords: { x: 680, y: 427 }, // back of blue plane
|
clickCoords: { x: 680, y: 427 }, // back of blue plane
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -453,6 +459,9 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
|
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
@ -461,35 +470,32 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
const startAt = '[23.74, -32.03]'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
|
||||||
const num = 23.97
|
|
||||||
const num2 = '47.71'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> line([-${num2}, 0], %)`)
|
|> line([-${commonPoints.num2}, 0], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await page.getByRole('button', { name: 'Line' }).click()
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
@ -539,7 +545,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
// check the same selection again by putting cursor in code first then selecting axis
|
// check the same selection again by putting cursor in code first then selecting axis
|
||||||
await page.getByText(` |> line([-${num2}, 0], %)`).click()
|
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
await xAxisClick()
|
await xAxisClick()
|
||||||
@ -550,7 +556,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
// select segment in editor than another segment in scene and check there are two cursors
|
// select segment in editor than another segment in scene and check there are two cursors
|
||||||
await page.getByText(` |> line([-${num2}, 0], %)`).click()
|
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(300)
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
||||||
@ -575,7 +581,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
|
|
||||||
// select a line
|
// select a line
|
||||||
// await topHorzSegmentClick()
|
// await topHorzSegmentClick()
|
||||||
await page.getByText(startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
@ -637,12 +643,15 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
await context.addInitScript(async (token) => {
|
await context.addInitScript(async (token) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
const distance = sqrt(20)
|
||||||
|> line([25.1, 0.41], %)
|
const part001 = startSketchOn('-XZ')
|
||||||
|> line([0.73, -14.93], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> close(%)`
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -667,24 +676,42 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
// Click to select face and set distance
|
// Click to select face and set distance
|
||||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||||
await page.getByRole('button', { name: 'Continue' }).click()
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
|
// Assert that we're on the distance step
|
||||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||||
await page.keyboard.press('Enter')
|
|
||||||
|
// Assert that the an alternative variable name is chosen,
|
||||||
|
// since the default variable name is already in use (distance)
|
||||||
|
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||||
|
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||||
|
'distance001'
|
||||||
|
)
|
||||||
|
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
// Review step and argument hotkeys
|
// Review step and argument hotkeys
|
||||||
await page.keyboard.press('2')
|
await expect(
|
||||||
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
|
page.getByRole('button', { name: 'Submit command' })
|
||||||
|
).toBeEnabled()
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Distance 12', exact: false })
|
||||||
|
).toBeDisabled()
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
// Check that the code was updated
|
// Check that the code was updated
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
// Unfortunately this indentation seems to matter for the test
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const distance = sqrt(20)
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
const distance001 = 5 + 7
|
||||||
|> line([25.1, 0.41], %)
|
const part001 = startSketchOn('-XZ')
|
||||||
|> line([0.73, -14.93], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> close(%)
|
|> line([0.73, -14.93], %)
|
||||||
|> extrude(5, %)`
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -696,6 +723,9 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
@ -716,34 +746,32 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
const startAt = '[23.74, -32.03]'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const num = 23.97
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> line([-47.71, 0], %)`
|
|> line([-${commonPoints.num2}, 0], %)`
|
||||||
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
|
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
|
||||||
|
|
||||||
// exit the sketch
|
// exit the sketch
|
||||||
@ -765,7 +793,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
const startAt2 = '[23.61, -31.85]'
|
const startAt2 = '[26.23, -35.39]'
|
||||||
await expect(
|
await expect(
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
).toBe(
|
).toBe(
|
||||||
@ -779,7 +807,7 @@ const part002 = startSketchOn('XY')
|
|||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const num2 = 23.83
|
const num2 = 26.48
|
||||||
await expect(
|
await expect(
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
).toBe(
|
).toBe(
|
||||||
@ -808,7 +836,7 @@ const part002 = startSketchOn('XY')
|
|||||||
|> startProfileAt(${startAt2}, %)
|
|> startProfileAt(${startAt2}, %)
|
||||||
|> line([${num2}, 0], %)
|
|> line([${num2}, 0], %)
|
||||||
|> line([0, ${num2}], %)
|
|> line([0, ${num2}], %)
|
||||||
|> line([-47.44, 0], %)`.replace(/\s/g, '')
|
|> line([-52.71, 0], %)`.replace(/\s/g, '')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -918,6 +946,11 @@ fn yohey = (pos) => {
|
|||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// wait for start sketch as a proxy for the stream being ready
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
|
||||||
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
|
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
|
||||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||||
await expect(
|
await expect(
|
||||||
@ -964,6 +997,9 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
|
@ -31,6 +31,12 @@ test.beforeEach(async ({ context, page }) => {
|
|||||||
|
|
||||||
test.setTimeout(60000)
|
test.setTimeout(60000)
|
||||||
|
|
||||||
|
const commonPoints = {
|
||||||
|
startAt: '[26.38, -35.59]',
|
||||||
|
num1: 26.63,
|
||||||
|
num2: 53.01,
|
||||||
|
}
|
||||||
|
|
||||||
test('change camera, show planes', async ({ page, context }) => {
|
test('change camera, show planes', async ({ page, context }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
@ -342,24 +348,33 @@ const part001 = startSketchOn('-XZ')
|
|||||||
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
||||||
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
||||||
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
||||||
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
console.log(
|
||||||
|
`taking snapshot of using: "zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}"`
|
||||||
|
)
|
||||||
|
const cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||||
const child = spawn(cliCommand, { shell: true })
|
const child = spawn(cliCommand, { shell: true })
|
||||||
await new Promise((resolve, reject) => {
|
const result = await new Promise<string>((resolve, reject) => {
|
||||||
child.on('error', (code: any, msg: any) => {
|
child.on('error', (code: any, msg: any) => {
|
||||||
console.log('error', code, msg)
|
console.log('error', code, msg)
|
||||||
reject()
|
reject('error')
|
||||||
})
|
})
|
||||||
child.on('exit', (code, msg) => {
|
child.on('exit', (code, msg) => {
|
||||||
console.log('exit', code, msg)
|
console.log('exit', code, msg)
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
reject(`exit code ${code} for model ${modelPath}`)
|
reject(`exit code ${code} for model ${modelPath}`)
|
||||||
} else {
|
} else {
|
||||||
resolve(true)
|
resolve('success')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
||||||
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
||||||
})
|
})
|
||||||
|
expect(result).toBe('success')
|
||||||
|
if (result === 'success') {
|
||||||
|
console.log(`snapshot taken for ${modelPath}`)
|
||||||
|
} else {
|
||||||
|
console.log(`snapshot failed for ${modelPath}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -428,6 +443,9 @@ test('Draft segments should look right', async ({ page }) => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
@ -448,10 +466,9 @@ test('Draft segments should look right', async ({ page }) => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
const startAt = '[23.74, -32.03]'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -463,11 +480,10 @@ test('Draft segments should look right', async ({ page }) => {
|
|||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const num = 23.97
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 94 KiB |
@ -85,7 +85,7 @@
|
|||||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"lint": "eslint --fix src",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
||||||
"postinstall": "patch-package && yarn xstate:typegen",
|
"postinstall": "yarn xstate:typegen",
|
||||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
@ -134,7 +134,6 @@
|
|||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
"happy-dom": "^10.8.0",
|
"happy-dom": "^10.8.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"patch-package": "^8.0.0",
|
|
||||||
"pixelmatch": "^5.3.0",
|
"pixelmatch": "^5.3.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
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 ) {
|
|
||||||
|
|
@ -35,7 +35,9 @@ import {
|
|||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
import CommandBarProvider, {
|
||||||
|
CommandBar,
|
||||||
|
} from 'components/CommandBar/CommandBar'
|
||||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
@ -117,6 +119,7 @@ const router = createBrowserRouter(
|
|||||||
<ModelingMachineProvider>
|
<ModelingMachineProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<App />
|
<App />
|
||||||
|
<CommandBar />
|
||||||
</ModelingMachineProvider>
|
</ModelingMachineProvider>
|
||||||
<WasmErrBanner />
|
<WasmErrBanner />
|
||||||
</FileMachineProvider>
|
</FileMachineProvider>
|
||||||
@ -216,6 +219,7 @@ const router = createBrowserRouter(
|
|||||||
<Auth>
|
<Auth>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<Home />
|
<Home />
|
||||||
|
<CommandBar />
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
loader: async (): Promise<HomeLoaderData | Response> => {
|
loader: async (): Promise<HomeLoaderData | Response> => {
|
||||||
|
@ -6,7 +6,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
import { kclManager } from 'lang/KclSingleton'
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import {
|
||||||
|
NetworkHealthState,
|
||||||
|
useNetworkStatus,
|
||||||
|
} from 'components/NetworkHealthIndicator'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
@ -24,6 +29,13 @@ export const Toolbar = () => {
|
|||||||
context.selectionRanges
|
context.selectionRanges
|
||||||
)
|
)
|
||||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||||
|
const { overallState } = useNetworkStatus()
|
||||||
|
const { isExecuting } = useKclContext()
|
||||||
|
const { isStreamReady } = useStore((s) => ({
|
||||||
|
isStreamReady: s.isStreamReady,
|
||||||
|
}))
|
||||||
|
const disableAllButtons =
|
||||||
|
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||||
|
|
||||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||||
const span = toolbarButtonsRef.current
|
const span = toolbarButtonsRef.current
|
||||||
@ -60,6 +72,7 @@ export const Toolbar = () => {
|
|||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
<span data-testid="start-sketch">Start Sketch</span>
|
<span data-testid="start-sketch">Start Sketch</span>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -74,6 +87,7 @@ export const Toolbar = () => {
|
|||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
Edit Sketch
|
Edit Sketch
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -88,6 +102,7 @@ export const Toolbar = () => {
|
|||||||
icon: 'arrowLeft',
|
icon: 'arrowLeft',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
Exit Sketch
|
Exit Sketch
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -109,6 +124,7 @@ export const Toolbar = () => {
|
|||||||
icon: 'line',
|
icon: 'line',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
Line
|
Line
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -128,8 +144,9 @@ export const Toolbar = () => {
|
|||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
!state.can('Equip tangential arc to') &&
|
(!state.can('Equip tangential arc to') &&
|
||||||
!state.matches('Sketch.Tangential arc to')
|
!state.matches('Sketch.Tangential arc to')) ||
|
||||||
|
disableAllButtons
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Tangential Arc
|
Tangential Arc
|
||||||
@ -169,7 +186,7 @@ export const Toolbar = () => {
|
|||||||
disabled={
|
disabled={
|
||||||
!state.nextEvents
|
!state.nextEvents
|
||||||
.filter((event) => state.can(event as any))
|
.filter((event) => state.can(event as any))
|
||||||
.includes(eventName)
|
.includes(eventName) || disableAllButtons
|
||||||
}
|
}
|
||||||
title={eventName}
|
title={eventName}
|
||||||
icon={{
|
icon={{
|
||||||
@ -194,7 +211,7 @@ export const Toolbar = () => {
|
|||||||
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!state.can('Extrude')}
|
disabled={!state.can('Extrude') || disableAllButtons}
|
||||||
title={
|
title={
|
||||||
state.can('Extrude')
|
state.can('Extrude')
|
||||||
? 'extrude'
|
? 'extrude'
|
||||||
|
879
src/clientSideScene/CameraControls.ts
Normal file
@ -0,0 +1,879 @@
|
|||||||
|
import { MouseGuard } from 'lib/cameraControls'
|
||||||
|
import {
|
||||||
|
Euler,
|
||||||
|
MathUtils,
|
||||||
|
Matrix4,
|
||||||
|
OrthographicCamera,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Quaternion,
|
||||||
|
Spherical,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import {
|
||||||
|
DEBUG_SHOW_INTERSECTION_PLANE,
|
||||||
|
INTERSECTION_PLANE_LAYER,
|
||||||
|
SKETCH_LAYER,
|
||||||
|
ZOOM_MAGIC_NUMBER,
|
||||||
|
} from './sceneInfra'
|
||||||
|
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
|
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||||
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
|
import { isQuaternionVertical } from './helpers'
|
||||||
|
|
||||||
|
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||||
|
const FRAMES_TO_ANIMATE_IN = 30
|
||||||
|
|
||||||
|
const tempQuaternion = new Quaternion() // just used for maths
|
||||||
|
|
||||||
|
interface ThreeCamValues {
|
||||||
|
position: Vector3
|
||||||
|
quaternion: Quaternion
|
||||||
|
zoom: number
|
||||||
|
isPerspective: boolean
|
||||||
|
target: Vector3
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReactCameraProperties =
|
||||||
|
| {
|
||||||
|
type: 'perspective'
|
||||||
|
fov?: number
|
||||||
|
position: [number, number, number]
|
||||||
|
quaternion: [number, number, number, number]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'orthographic'
|
||||||
|
zoom?: number
|
||||||
|
position: [number, number, number]
|
||||||
|
quaternion: [number, number, number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCmdDelay = 50
|
||||||
|
|
||||||
|
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
|
||||||
|
const cmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
...convertThreeCamValuesToEngineCam(threeValues),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engineCommandManager.sendSceneCommand(cmd)
|
||||||
|
}, 1000 / 15)
|
||||||
|
|
||||||
|
let lastPerspectiveCmd: EngineCommand | null = null
|
||||||
|
let lastPerspectiveCmdTime: number = Date.now()
|
||||||
|
let lastPerspectiveCmdTimeoutId: number | null = null
|
||||||
|
|
||||||
|
const sendLastPerspectiveReliableChannel = () => {
|
||||||
|
if (
|
||||||
|
lastPerspectiveCmd &&
|
||||||
|
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
|
||||||
|
) {
|
||||||
|
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
|
||||||
|
lastPerspectiveCmdTime = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledUpdateEngineFov = throttle(
|
||||||
|
(vals: {
|
||||||
|
position: Vector3
|
||||||
|
quaternion: Quaternion
|
||||||
|
zoom: number
|
||||||
|
fov: number
|
||||||
|
target: Vector3
|
||||||
|
}) => {
|
||||||
|
const cmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_perspective_settings',
|
||||||
|
...convertThreeCamValuesToEngineCam({
|
||||||
|
...vals,
|
||||||
|
isPerspective: true,
|
||||||
|
}),
|
||||||
|
fov_y: vals.fov,
|
||||||
|
...calculateNearFarFromFOV(vals.fov),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engineCommandManager.sendSceneCommand(cmd)
|
||||||
|
lastPerspectiveCmd = cmd
|
||||||
|
lastPerspectiveCmdTime = Date.now()
|
||||||
|
if (lastPerspectiveCmdTimeoutId !== null) {
|
||||||
|
clearTimeout(lastPerspectiveCmdTimeoutId)
|
||||||
|
}
|
||||||
|
lastPerspectiveCmdTimeoutId = setTimeout(
|
||||||
|
sendLastPerspectiveReliableChannel,
|
||||||
|
lastCmdDelay
|
||||||
|
) as any as number
|
||||||
|
},
|
||||||
|
1000 / 15
|
||||||
|
)
|
||||||
|
|
||||||
|
export class CameraControls {
|
||||||
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
|
target: Vector3
|
||||||
|
domElement: HTMLCanvasElement
|
||||||
|
isDragging: boolean
|
||||||
|
mouseDownPosition: Vector2
|
||||||
|
mouseNewPosition: Vector2
|
||||||
|
rotationSpeed = 0.3
|
||||||
|
enableRotate = true
|
||||||
|
enablePan = true
|
||||||
|
enableZoom = true
|
||||||
|
lastPerspectiveFov: number = 45
|
||||||
|
pendingZoom: number | null = null
|
||||||
|
pendingRotation: Vector2 | null = null
|
||||||
|
pendingPan: Vector2 | null = null
|
||||||
|
interactionGuards: MouseGuard = {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Shift + drag or middle click + drag',
|
||||||
|
callback: (e) => !!(e.buttons & 4) && !e.ctrlKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Right click + drag',
|
||||||
|
callback: (e) => {
|
||||||
|
console.log('event', e)
|
||||||
|
return !!(e.buttons & 2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
isFovAnimationInProgress = false
|
||||||
|
fovBeforeOrtho = 45
|
||||||
|
get isPerspective() {
|
||||||
|
return this.camera instanceof PerspectiveCamera
|
||||||
|
}
|
||||||
|
|
||||||
|
// reacts hooks into some of this singleton's properties
|
||||||
|
reactCameraProperties: ReactCameraProperties = {
|
||||||
|
type: 'perspective',
|
||||||
|
fov: 12,
|
||||||
|
position: [0, 0, 0],
|
||||||
|
quaternion: [0, 0, 0, 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
setCam = (camProps: ReactCameraProperties) => {
|
||||||
|
if (
|
||||||
|
camProps.type === 'perspective' &&
|
||||||
|
this.camera instanceof OrthographicCamera
|
||||||
|
) {
|
||||||
|
this.usePerspectiveCamera()
|
||||||
|
} else if (
|
||||||
|
camProps.type === 'orthographic' &&
|
||||||
|
this.camera instanceof PerspectiveCamera
|
||||||
|
) {
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
}
|
||||||
|
this.camera.position.set(...camProps.position)
|
||||||
|
this.camera.quaternion.set(...camProps.quaternion)
|
||||||
|
if (
|
||||||
|
camProps.type === 'perspective' &&
|
||||||
|
this.camera instanceof PerspectiveCamera
|
||||||
|
) {
|
||||||
|
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
|
||||||
|
// at the same time
|
||||||
|
} else if (
|
||||||
|
camProps.type === 'orthographic' &&
|
||||||
|
this.camera instanceof OrthographicCamera
|
||||||
|
) {
|
||||||
|
this.camera.zoom = camProps.zoom || 1
|
||||||
|
}
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.update(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(isOrtho = false, domElement: HTMLCanvasElement) {
|
||||||
|
this.camera = isOrtho ? new OrthographicCamera() : new PerspectiveCamera()
|
||||||
|
this.camera.up.set(0, 0, 1)
|
||||||
|
this.camera.far = 20000
|
||||||
|
this.target = new Vector3()
|
||||||
|
this.domElement = domElement
|
||||||
|
this.isDragging = false
|
||||||
|
this.mouseDownPosition = new Vector2()
|
||||||
|
this.mouseNewPosition = new Vector2()
|
||||||
|
|
||||||
|
this.domElement.addEventListener('pointerdown', this.onMouseDown)
|
||||||
|
this.domElement.addEventListener('pointermove', this.onMouseMove)
|
||||||
|
this.domElement.addEventListener('pointerup', this.onMouseUp)
|
||||||
|
this.domElement.addEventListener('wheel', this.onMouseWheel)
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
this.onWindowResize()
|
||||||
|
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
||||||
|
() => {}
|
||||||
|
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
|
||||||
|
this._isCamMovingCallback = cb
|
||||||
|
}
|
||||||
|
private _camChangeCallbacks: { [key: string]: () => void } = {}
|
||||||
|
subscribeToCamChange(cb: () => void) {
|
||||||
|
const cbId = uuidv4()
|
||||||
|
this._camChangeCallbacks[cbId] = cb
|
||||||
|
const unsubscribe = () => {
|
||||||
|
delete this._camChangeCallbacks[cbId]
|
||||||
|
}
|
||||||
|
return unsubscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowResize = () => {
|
||||||
|
if (this.camera instanceof PerspectiveCamera) {
|
||||||
|
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||||
|
} else if (this.camera instanceof OrthographicCamera) {
|
||||||
|
const aspect = window.innerWidth / window.innerHeight
|
||||||
|
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||||
|
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||||
|
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
|
||||||
|
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
|
||||||
|
}
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown = (event: MouseEvent) => {
|
||||||
|
this.isDragging = true
|
||||||
|
this.mouseDownPosition.set(event.clientX, event.clientY)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove = (event: MouseEvent) => {
|
||||||
|
if (this.isDragging) {
|
||||||
|
this.mouseNewPosition.set(event.clientX, event.clientY)
|
||||||
|
const deltaMove = this.mouseNewPosition
|
||||||
|
.clone()
|
||||||
|
.sub(this.mouseDownPosition)
|
||||||
|
this.mouseDownPosition.copy(this.mouseNewPosition)
|
||||||
|
|
||||||
|
let state: 'pan' | 'rotate' | 'zoom' = 'pan'
|
||||||
|
|
||||||
|
if (this.interactionGuards.pan.callback(event as any)) {
|
||||||
|
if (this.enablePan === false) return
|
||||||
|
// handleMouseDownPan(event)
|
||||||
|
state = 'pan'
|
||||||
|
} else if (this.interactionGuards.rotate.callback(event as any)) {
|
||||||
|
if (this.enableRotate === false) return
|
||||||
|
// handleMouseDownRotate(event)
|
||||||
|
state = 'rotate'
|
||||||
|
} else if (this.interactionGuards.zoom.dragCallback(event as any)) {
|
||||||
|
if (this.enableZoom === false) return
|
||||||
|
// handleMouseDownDolly(event)
|
||||||
|
state = 'zoom'
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement camera movement logic here based on deltaMove
|
||||||
|
// For example, for rotating the camera around the target:
|
||||||
|
if (state === 'rotate') {
|
||||||
|
this.pendingRotation = this.pendingRotation
|
||||||
|
? this.pendingRotation
|
||||||
|
: new Vector2()
|
||||||
|
this.pendingRotation.x += deltaMove.x
|
||||||
|
this.pendingRotation.y += deltaMove.y
|
||||||
|
} else if (state === 'zoom') {
|
||||||
|
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||||
|
this.pendingZoom *= 1 + deltaMove.y * 0.01
|
||||||
|
} else if (state === 'pan') {
|
||||||
|
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
|
||||||
|
let distance = this.camera.position.distanceTo(this.target)
|
||||||
|
if (this.camera instanceof OrthographicCamera) {
|
||||||
|
const zoomFudgeFactor = 2280
|
||||||
|
distance = zoomFudgeFactor / (this.camera.zoom * 45)
|
||||||
|
}
|
||||||
|
const panSpeed = (distance / 1000 / 45) * this.fovBeforeOrtho
|
||||||
|
this.pendingPan.x += -deltaMove.x * panSpeed
|
||||||
|
this.pendingPan.y += deltaMove.y * panSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp = (event: MouseEvent) => {
|
||||||
|
this.isDragging = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseWheel = (event: WheelEvent) => {
|
||||||
|
// Assume trackpad if the deltas are small and integers
|
||||||
|
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
|
||||||
|
|
||||||
|
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
|
||||||
|
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||||
|
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
useOrthographicCamera = () => {
|
||||||
|
if (this.camera instanceof OrthographicCamera) return
|
||||||
|
const { x: px, y: py, z: pz } = this.camera.position
|
||||||
|
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||||
|
const aspect = window.innerWidth / window.innerHeight
|
||||||
|
this.lastPerspectiveFov = this.camera.fov
|
||||||
|
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
|
||||||
|
this.camera = new OrthographicCamera(
|
||||||
|
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||||
|
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||||
|
ORTHOGRAPHIC_CAMERA_SIZE,
|
||||||
|
-ORTHOGRAPHIC_CAMERA_SIZE,
|
||||||
|
z_near,
|
||||||
|
z_far
|
||||||
|
)
|
||||||
|
this.camera.up.set(0, 0, 1)
|
||||||
|
this.camera.layers.enable(SKETCH_LAYER)
|
||||||
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
|
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
this.camera.position.set(px, py, pz)
|
||||||
|
const distance = this.camera.position.distanceTo(this.target.clone())
|
||||||
|
const fovFactor = 45 / this.lastPerspectiveFov
|
||||||
|
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
|
||||||
|
|
||||||
|
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_set_orthographic',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
private createPerspectiveCamera = () => {
|
||||||
|
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
|
||||||
|
this.camera = new PerspectiveCamera(
|
||||||
|
this.lastPerspectiveFov,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
z_near,
|
||||||
|
z_far
|
||||||
|
)
|
||||||
|
this.camera.up.set(0, 0, 1)
|
||||||
|
this.camera.layers.enable(SKETCH_LAYER)
|
||||||
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
|
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
return this.camera
|
||||||
|
}
|
||||||
|
usePerspectiveCamera = () => {
|
||||||
|
const { x: px, y: py, z: pz } = this.camera.position
|
||||||
|
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||||
|
const zoom = this.camera.zoom
|
||||||
|
this.camera = this.createPerspectiveCamera()
|
||||||
|
|
||||||
|
this.camera.position.set(px, py, pz)
|
||||||
|
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||||
|
const zoomFudgeFactor = 2280
|
||||||
|
const distance = zoomFudgeFactor / (zoom * this.lastPerspectiveFov)
|
||||||
|
const direction = new Vector3().subVectors(
|
||||||
|
this.camera.position,
|
||||||
|
this.target
|
||||||
|
)
|
||||||
|
direction.normalize()
|
||||||
|
this.camera.position.copy(this.target).addScaledVector(direction, distance)
|
||||||
|
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_set_perspective',
|
||||||
|
parameters: {
|
||||||
|
fov_y: this.camera.fov,
|
||||||
|
...calculateNearFarFromFOV(this.lastPerspectiveFov),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.onCameraChange()
|
||||||
|
this.update()
|
||||||
|
return this.camera
|
||||||
|
}
|
||||||
|
|
||||||
|
dollyZoom = (newFov: number) => {
|
||||||
|
if (!(this.camera instanceof PerspectiveCamera)) {
|
||||||
|
console.warn('Dolly zoom is only applicable to perspective cameras.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastPerspectiveFov = newFov
|
||||||
|
|
||||||
|
// Calculate the direction vector from the camera towards the controls target
|
||||||
|
const direction = new Vector3()
|
||||||
|
.subVectors(this.target, this.camera.position)
|
||||||
|
.normalize()
|
||||||
|
|
||||||
|
// Calculate the distance to the controls target before changing the FOV
|
||||||
|
const distanceBefore = this.camera.position.distanceTo(this.target)
|
||||||
|
|
||||||
|
// Calculate the scale factor for the new FOV compared to the old one
|
||||||
|
// This needs to be calculated before updating the camera's FOV
|
||||||
|
const oldFov = this.camera.fov
|
||||||
|
|
||||||
|
const viewHeightFactor = (fov: number) => {
|
||||||
|
/* *
|
||||||
|
/|
|
||||||
|
/ |
|
||||||
|
/ |
|
||||||
|
/ |
|
||||||
|
/ | viewHeight/2
|
||||||
|
/ |
|
||||||
|
/ |
|
||||||
|
/↙️fov/2 |
|
||||||
|
/________|
|
||||||
|
\ |
|
||||||
|
\._._._.|
|
||||||
|
*/
|
||||||
|
return Math.tan(deg2Rad(fov / 2))
|
||||||
|
}
|
||||||
|
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
|
||||||
|
|
||||||
|
this.camera.fov = newFov
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
|
||||||
|
const distanceAfter = distanceBefore * scaleFactor
|
||||||
|
|
||||||
|
const newPosition = this.target
|
||||||
|
.clone()
|
||||||
|
.add(direction.multiplyScalar(-distanceAfter))
|
||||||
|
this.camera.position.copy(newPosition)
|
||||||
|
|
||||||
|
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
|
||||||
|
this.camera.near = z_near
|
||||||
|
this.camera.far = z_far
|
||||||
|
|
||||||
|
throttledUpdateEngineFov({
|
||||||
|
fov: newFov,
|
||||||
|
position: newPosition,
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
target: this.target,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update = (forceUpdate = false) => {
|
||||||
|
// If there are any changes that need to be applied to the camera, apply them here.
|
||||||
|
|
||||||
|
let didChange = forceUpdate
|
||||||
|
if (this.pendingRotation) {
|
||||||
|
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
|
||||||
|
this.pendingRotation = null // Clear the pending rotation after applying it
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingZoom) {
|
||||||
|
if (this.camera instanceof PerspectiveCamera) {
|
||||||
|
// move camera towards or away from the target
|
||||||
|
const distance = this.camera.position.distanceTo(this.target)
|
||||||
|
const newDistance = distance * this.pendingZoom
|
||||||
|
const direction = this.camera.position
|
||||||
|
.clone()
|
||||||
|
.sub(this.target)
|
||||||
|
.normalize()
|
||||||
|
const newPosition = this.target
|
||||||
|
.clone()
|
||||||
|
.add(direction.multiplyScalar(newDistance))
|
||||||
|
this.camera.position.copy(newPosition)
|
||||||
|
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.pendingZoom = null // Clear the pending zoom after applying it
|
||||||
|
} else {
|
||||||
|
// TODO change ortho zoom
|
||||||
|
this.camera.zoom = this.camera.zoom / this.pendingZoom
|
||||||
|
this.pendingZoom = null
|
||||||
|
}
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingPan) {
|
||||||
|
// move camera left/right and up/down
|
||||||
|
const offset = this.camera.position.clone().sub(this.target)
|
||||||
|
const direction = offset.clone().normalize()
|
||||||
|
const cameraQuaternion = this.camera.quaternion
|
||||||
|
const up = new Vector3(0, 1, 0).applyQuaternion(cameraQuaternion)
|
||||||
|
const right = new Vector3().crossVectors(up, direction)
|
||||||
|
right.multiplyScalar(this.pendingPan.x)
|
||||||
|
up.multiplyScalar(this.pendingPan.y)
|
||||||
|
const newPosition = this.camera.position.clone().add(right).add(up)
|
||||||
|
this.target.add(right)
|
||||||
|
this.target.add(up)
|
||||||
|
this.camera.position.copy(newPosition)
|
||||||
|
this.pendingPan = null
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.safeLookAtTarget()
|
||||||
|
|
||||||
|
// Update the camera's matrices
|
||||||
|
this.camera.updateMatrixWorld()
|
||||||
|
if (didChange) {
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// damping would be implemented here in update if we choose to add it.
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCamera = (deltaX: number, deltaY: number) => {
|
||||||
|
const quat = new Quaternion().setFromUnitVectors(
|
||||||
|
new Vector3(0, 0, 1),
|
||||||
|
new Vector3(0, 1, 0)
|
||||||
|
)
|
||||||
|
const quatInverse = quat.clone().invert()
|
||||||
|
|
||||||
|
const angleX = deltaX * this.rotationSpeed // rotationSpeed is a constant that defines how fast the camera rotates
|
||||||
|
const angleY = deltaY * this.rotationSpeed
|
||||||
|
|
||||||
|
// Convert angles to radians
|
||||||
|
const radianX = MathUtils.degToRad(angleX)
|
||||||
|
const radianY = MathUtils.degToRad(angleY)
|
||||||
|
|
||||||
|
// Get the offset from the camera to the target
|
||||||
|
const offset = new Vector3().subVectors(this.camera.position, this.target)
|
||||||
|
|
||||||
|
// spherical is a y-up paradigm, need to conform to that for now
|
||||||
|
offset.applyQuaternion(quat)
|
||||||
|
|
||||||
|
// Convert offset to spherical coordinates
|
||||||
|
const spherical = new Spherical().setFromVector3(offset)
|
||||||
|
|
||||||
|
// Apply the rotations
|
||||||
|
spherical.theta -= radianX
|
||||||
|
spherical.phi -= radianY
|
||||||
|
|
||||||
|
// Restrict the phi angle to avoid the camera flipping at the poles
|
||||||
|
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
|
||||||
|
|
||||||
|
// Convert back to Cartesian coordinates
|
||||||
|
offset.setFromSpherical(spherical)
|
||||||
|
|
||||||
|
// put the offset back into the z-up paradigm
|
||||||
|
offset.applyQuaternion(quatInverse)
|
||||||
|
|
||||||
|
// Update the camera's position
|
||||||
|
this.camera.position.copy(this.target).add(offset)
|
||||||
|
|
||||||
|
// Look at the target
|
||||||
|
this.camera.updateMatrixWorld()
|
||||||
|
}
|
||||||
|
|
||||||
|
safeLookAtTarget(up = new Vector3(0, 0, 1)) {
|
||||||
|
const quaternion = _lookAt(this.camera.position, this.target, up)
|
||||||
|
this.camera.quaternion.copy(quaternion)
|
||||||
|
this.camera.updateMatrixWorld()
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenCamToNegYAxis(
|
||||||
|
// -90 degrees from the x axis puts the camera on the negative y axis
|
||||||
|
targetAngle = -Math.PI / 2,
|
||||||
|
duration = 500
|
||||||
|
): Promise<void> {
|
||||||
|
// should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative
|
||||||
|
// zPosition should stay the same
|
||||||
|
const xyRadius = Math.sqrt(
|
||||||
|
(this.target.x - this.camera.position.x) ** 2 +
|
||||||
|
(this.target.y - this.camera.position.y) ** 2
|
||||||
|
)
|
||||||
|
const xyAngle = Math.atan2(
|
||||||
|
this.camera.position.y - this.target.y,
|
||||||
|
this.camera.position.x - this.target.x
|
||||||
|
)
|
||||||
|
this._isCamMovingCallback(true, true)
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
new TWEEN.Tween({ angle: xyAngle })
|
||||||
|
.to({ angle: targetAngle }, duration)
|
||||||
|
.onUpdate((obj) => {
|
||||||
|
const x = xyRadius * Math.cos(obj.angle)
|
||||||
|
const y = xyRadius * Math.sin(obj.angle)
|
||||||
|
this.camera.position.set(
|
||||||
|
this.target.x + x,
|
||||||
|
this.target.y + y,
|
||||||
|
this.camera.position.z
|
||||||
|
)
|
||||||
|
this.update()
|
||||||
|
this.onCameraChange()
|
||||||
|
})
|
||||||
|
.onComplete((obj) => {
|
||||||
|
const x = xyRadius * Math.cos(obj.angle)
|
||||||
|
const y = xyRadius * Math.sin(obj.angle)
|
||||||
|
this.camera.position.set(
|
||||||
|
this.target.x + x,
|
||||||
|
this.target.y + y,
|
||||||
|
this.camera.position.z
|
||||||
|
)
|
||||||
|
this.update()
|
||||||
|
this.onCameraChange()
|
||||||
|
this._isCamMovingCallback(false, true)
|
||||||
|
|
||||||
|
// resolve after a couple of frames
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => resolve())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async tweenCameraToQuaternion(
|
||||||
|
targetQuaternion: Quaternion,
|
||||||
|
duration = 500,
|
||||||
|
toOrthographic = true
|
||||||
|
): Promise<void> {
|
||||||
|
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||||
|
let remainingDuration = duration
|
||||||
|
if (isVertical) {
|
||||||
|
remainingDuration = duration * 0.5
|
||||||
|
const orbitRotationDuration = duration * 0.65
|
||||||
|
let targetAngle = -Math.PI / 2
|
||||||
|
const v = new Vector3(0, 0, 1).applyQuaternion(targetQuaternion)
|
||||||
|
if (v.z < 0) targetAngle = Math.PI / 2
|
||||||
|
await this.tweenCamToNegYAxis(targetAngle, orbitRotationDuration)
|
||||||
|
}
|
||||||
|
await this._tweenCameraToQuaternion(
|
||||||
|
targetQuaternion,
|
||||||
|
remainingDuration,
|
||||||
|
toOrthographic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_tweenCameraToQuaternion(
|
||||||
|
targetQuaternion: Quaternion,
|
||||||
|
duration = 500,
|
||||||
|
toOrthographic = false
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const camera = this.camera
|
||||||
|
this._isCamMovingCallback(true, true)
|
||||||
|
const initialQuaternion = camera.quaternion.clone()
|
||||||
|
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||||
|
let tweenEnd = isVertical ? 0.99 : 1
|
||||||
|
const controlsTarget = this.target.clone()
|
||||||
|
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
|
||||||
|
|
||||||
|
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
|
||||||
|
const currentQ = tempQuaternion.slerpQuaternions(
|
||||||
|
initialQuaternion,
|
||||||
|
targetQuaternion,
|
||||||
|
animationProgress
|
||||||
|
)
|
||||||
|
if (this.camera instanceof PerspectiveCamera)
|
||||||
|
// changing the camera position back when it's orthographic doesn't do anything
|
||||||
|
// and it messes up animating back to perspective later
|
||||||
|
this.camera.position
|
||||||
|
.set(0, 0, 1)
|
||||||
|
.applyQuaternion(currentQ)
|
||||||
|
.multiplyScalar(initialDistance)
|
||||||
|
.add(controlsTarget)
|
||||||
|
|
||||||
|
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
|
||||||
|
this.camera.quaternion.copy(currentQ)
|
||||||
|
this.target.copy(controlsTarget)
|
||||||
|
// this.controls.update()
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.update()
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onComplete = async () => {
|
||||||
|
if (isReducedMotion() && toOrthographic) {
|
||||||
|
cameraAtTime(0.99)
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
} else if (toOrthographic) {
|
||||||
|
await this.animateToOrthographic()
|
||||||
|
}
|
||||||
|
this.enableRotate = false
|
||||||
|
this._isCamMovingCallback(false, true)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReducedMotion()) {
|
||||||
|
onComplete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
new TWEEN.Tween({ t: 0 })
|
||||||
|
.to({ t: tweenEnd }, duration)
|
||||||
|
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||||
|
.onUpdate(({ t }) => cameraAtTime(t))
|
||||||
|
.onComplete(onComplete)
|
||||||
|
.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
animateToOrthographic = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
this.isFovAnimationInProgress = true
|
||||||
|
let currentFov = this.lastPerspectiveFov
|
||||||
|
this.fovBeforeOrtho = currentFov
|
||||||
|
|
||||||
|
const targetFov = 4
|
||||||
|
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
|
||||||
|
let frameWaitOnFinish = 10
|
||||||
|
|
||||||
|
const animateFovChange = () => {
|
||||||
|
if (this.camera instanceof PerspectiveCamera) {
|
||||||
|
if (this.camera.fov > targetFov) {
|
||||||
|
// Decrease the FOV
|
||||||
|
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.dollyZoom(currentFov)
|
||||||
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
|
} else if (frameWaitOnFinish > 0) {
|
||||||
|
frameWaitOnFinish--
|
||||||
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
|
} else {
|
||||||
|
// Once the target FOV is reached, switch to the orthographic camera
|
||||||
|
// Needs to wait a couple frames after the FOV animation is complete
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
this.isFovAnimationInProgress = false
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animateFovChange() // Start the animation
|
||||||
|
})
|
||||||
|
animateToPerspective = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
this.isFovAnimationInProgress = true
|
||||||
|
// Immediately set the camera to perspective with a very low FOV
|
||||||
|
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
|
||||||
|
this.lastPerspectiveFov = 4
|
||||||
|
let currentFov = 4
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
|
||||||
|
this.usePerspectiveCamera()
|
||||||
|
|
||||||
|
const animateFovChange = () => {
|
||||||
|
if (this.camera instanceof OrthographicCamera) return
|
||||||
|
if (this.camera.fov < targetFov) {
|
||||||
|
// Increase the FOV
|
||||||
|
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
|
||||||
|
// this.camera.fov = currentFov
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.dollyZoom(currentFov)
|
||||||
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
|
} else {
|
||||||
|
// Set the flag to false as the FOV animation is complete
|
||||||
|
this.isFovAnimationInProgress = false
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animateFovChange() // Start the animation
|
||||||
|
})
|
||||||
|
|
||||||
|
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
||||||
|
setReactCameraPropertiesCallback = (
|
||||||
|
cb: (a: ReactCameraProperties) => void
|
||||||
|
) => {
|
||||||
|
this.reactCameraPropertiesCallback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
deferReactUpdate = throttle((a: ReactCameraProperties) => {
|
||||||
|
this.reactCameraPropertiesCallback(a)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
onCameraChange = () => {
|
||||||
|
const distance = this.target.distanceTo(this.camera.position)
|
||||||
|
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
|
||||||
|
this.camera.far = distance * 2
|
||||||
|
this.camera.near = distance / 10
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
}
|
||||||
|
|
||||||
|
throttledUpdateEngineCamera({
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
position: this.camera.position,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
isPerspective: this.isPerspective,
|
||||||
|
target: this.target,
|
||||||
|
})
|
||||||
|
this.deferReactUpdate({
|
||||||
|
type: this.isPerspective ? 'perspective' : 'orthographic',
|
||||||
|
[this.isPerspective ? 'fov' : 'zoom']:
|
||||||
|
this.camera instanceof PerspectiveCamera
|
||||||
|
? this.camera.fov
|
||||||
|
: this.camera.zoom,
|
||||||
|
position: [
|
||||||
|
roundOff(this.camera.position.x, 2),
|
||||||
|
roundOff(this.camera.position.y, 2),
|
||||||
|
roundOff(this.camera.position.z, 2),
|
||||||
|
],
|
||||||
|
quaternion: [
|
||||||
|
roundOff(this.camera.quaternion.x, 2),
|
||||||
|
roundOff(this.camera.quaternion.y, 2),
|
||||||
|
roundOff(this.camera.quaternion.z, 2),
|
||||||
|
roundOff(this.camera.quaternion.w, 2),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently duplicated, delete one
|
||||||
|
function calculateNearFarFromFOV(fov: number) {
|
||||||
|
const nearFarRatio = (fov - 3) / (45 - 3)
|
||||||
|
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||||
|
const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||||
|
return { z_near: 0.1, z_far }
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently duplicated, delete one
|
||||||
|
function convertThreeCamValuesToEngineCam({
|
||||||
|
target,
|
||||||
|
position,
|
||||||
|
quaternion,
|
||||||
|
zoom,
|
||||||
|
isPerspective,
|
||||||
|
}: ThreeCamValues): {
|
||||||
|
center: Vector3
|
||||||
|
up: Vector3
|
||||||
|
vantage: Vector3
|
||||||
|
} {
|
||||||
|
// Something to consider is that the orbit controls have a target,
|
||||||
|
// we're kind of deriving the target/lookAtVector here when it might not be needed
|
||||||
|
// leaving for now since it's working but maybe revisit later
|
||||||
|
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
|
||||||
|
|
||||||
|
const lookAtVector = new Vector3(0, 0, -1)
|
||||||
|
.applyEuler(euler)
|
||||||
|
.normalize()
|
||||||
|
.add(position)
|
||||||
|
|
||||||
|
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
|
||||||
|
if (isPerspective) {
|
||||||
|
return {
|
||||||
|
center: target,
|
||||||
|
up: upVector,
|
||||||
|
vantage: position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295
|
||||||
|
const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom
|
||||||
|
const direction = lookAtVector.clone().sub(position).normalize()
|
||||||
|
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
|
||||||
|
return {
|
||||||
|
center: lookAtVector,
|
||||||
|
up: upVector,
|
||||||
|
vantage: newVantage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure function helpers
|
||||||
|
|
||||||
|
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
|
||||||
|
// Direction from position to target, normalized.
|
||||||
|
let direction = new Vector3().subVectors(target, position).normalize()
|
||||||
|
|
||||||
|
// Calculate a new "effective" up vector that is orthogonal to the direction.
|
||||||
|
// This step ensures that the up vector does not affect the direction the camera is looking.
|
||||||
|
let right = new Vector3().crossVectors(direction, up).normalize()
|
||||||
|
let orthogonalUp = new Vector3().crossVectors(right, direction).normalize()
|
||||||
|
|
||||||
|
// Create a lookAt matrix using the position, and the recalculated orthogonal up vector.
|
||||||
|
let lookAtMatrix = new Matrix4()
|
||||||
|
lookAtMatrix.lookAt(position, target, orthogonalUp)
|
||||||
|
|
||||||
|
// Create a quaternion from the lookAt matrix.
|
||||||
|
let quaternion = new Quaternion().setFromRotationMatrix(lookAtMatrix)
|
||||||
|
|
||||||
|
return quaternion
|
||||||
|
}
|
@ -4,11 +4,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import {
|
import { DEBUG_SHOW_BOTH_SCENES, sceneInfra } from './sceneInfra'
|
||||||
DEBUG_SHOW_BOTH_SCENES,
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
ReactCameraProperties,
|
|
||||||
sceneInfra,
|
|
||||||
} from './sceneInfra'
|
|
||||||
import { throttle } from 'lib/utils'
|
import { throttle } from 'lib/utils'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
@ -18,7 +15,7 @@ function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
|||||||
const { state } = useModelingContext()
|
const { state } = useModelingContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
|
sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => {
|
||||||
setIsCamMoving(isMoving)
|
setIsCamMoving(isMoving)
|
||||||
setIsTween(isTween)
|
setIsTween(isTween)
|
||||||
})
|
})
|
||||||
@ -52,7 +49,8 @@ export const ClientSideScene = ({
|
|||||||
// Listen for changes to the camera controls setting
|
// Listen for changes to the camera controls setting
|
||||||
// and update the client-side scene's controls accordingly.
|
// and update the client-side scene's controls accordingly.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
|
sceneInfra.camControls.interactionGuards =
|
||||||
|
cameraMouseDragGuards[cameraControls]
|
||||||
}, [cameraControls])
|
}, [cameraControls])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.updateOtherSelectionColors(
|
sceneInfra.updateOtherSelectionColors(
|
||||||
@ -93,7 +91,7 @@ export const ClientSideScene = ({
|
|||||||
|
|
||||||
const throttled = throttle((a: ReactCameraProperties) => {
|
const throttled = throttle((a: ReactCameraProperties) => {
|
||||||
if (a.type === 'perspective' && a.fov) {
|
if (a.type === 'perspective' && a.fov) {
|
||||||
sceneInfra.dollyZoom(a.fov)
|
sceneInfra.camControls.dollyZoom(a.fov)
|
||||||
}
|
}
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
|
|
||||||
@ -107,7 +105,7 @@ export const CamDebugSettings = () => {
|
|||||||
const [fov, setFov] = useState(12)
|
const [fov, setFov] = useState(12)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
|
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||||
}, [sceneInfra])
|
}, [sceneInfra])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (camSettings.type === 'perspective' && camSettings.fov) {
|
if (camSettings.type === 'perspective' && camSettings.fov) {
|
||||||
@ -124,9 +122,9 @@ export const CamDebugSettings = () => {
|
|||||||
checked={camSettings.type === 'perspective'}
|
checked={camSettings.type === 'perspective'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (camSettings.type === 'perspective') {
|
if (camSettings.type === 'perspective') {
|
||||||
sceneInfra.useOrthographicCamera()
|
sceneInfra.camControls.useOrthographicCamera()
|
||||||
} else {
|
} else {
|
||||||
sceneInfra.usePerspectiveCamera()
|
sceneInfra.camControls.usePerspectiveCamera()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -156,7 +154,7 @@ export const CamDebugSettings = () => {
|
|||||||
value={camSettings.fov}
|
value={camSettings.fov}
|
||||||
className="text-black w-16"
|
className="text-black w-16"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
sceneInfra.setCam({
|
sceneInfra.camControls.setCam({
|
||||||
...camSettings,
|
...camSettings,
|
||||||
fov: parseFloat(e.target.value),
|
fov: parseFloat(e.target.value),
|
||||||
})
|
})
|
||||||
@ -173,7 +171,7 @@ export const CamDebugSettings = () => {
|
|||||||
value={camSettings.zoom}
|
value={camSettings.zoom}
|
||||||
className="text-black w-16"
|
className="text-black w-16"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
sceneInfra.setCam({
|
sceneInfra.camControls.setCam({
|
||||||
...camSettings,
|
...camSettings,
|
||||||
zoom: parseFloat(e.target.value),
|
zoom: parseFloat(e.target.value),
|
||||||
})
|
})
|
||||||
@ -194,7 +192,7 @@ export const CamDebugSettings = () => {
|
|||||||
value={camSettings.position[0]}
|
value={camSettings.position[0]}
|
||||||
className="text-black w-16"
|
className="text-black w-16"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
sceneInfra.setCam({
|
sceneInfra.camControls.setCam({
|
||||||
...camSettings,
|
...camSettings,
|
||||||
position: [
|
position: [
|
||||||
parseFloat(e.target.value),
|
parseFloat(e.target.value),
|
||||||
@ -214,7 +212,7 @@ export const CamDebugSettings = () => {
|
|||||||
value={camSettings.position[1]}
|
value={camSettings.position[1]}
|
||||||
className="text-black w-16"
|
className="text-black w-16"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
sceneInfra.setCam({
|
sceneInfra.camControls.setCam({
|
||||||
...camSettings,
|
...camSettings,
|
||||||
position: [
|
position: [
|
||||||
camSettings.position[0],
|
camSettings.position[0],
|
||||||
@ -234,7 +232,7 @@ export const CamDebugSettings = () => {
|
|||||||
value={camSettings.position[2]}
|
value={camSettings.position[2]}
|
||||||
className="text-black w-16"
|
className="text-black w-16"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
sceneInfra.setCam({
|
sceneInfra.camControls.setCam({
|
||||||
...camSettings,
|
...camSettings,
|
||||||
position: [
|
position: [
|
||||||
camSettings.position[0],
|
camSettings.position[0],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Quaternion } from 'three'
|
import { Quaternion } from 'three'
|
||||||
import { isQuaternionVertical } from './sceneInfra'
|
import { isQuaternionVertical } from './helpers'
|
||||||
|
|
||||||
describe('isQuaternionVertical', () => {
|
describe('isQuaternionVertical', () => {
|
||||||
it('should identify vertical quaternions', () => {
|
it('should identify vertical quaternions', () => {
|
@ -1,3 +1,4 @@
|
|||||||
|
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
import {
|
import {
|
||||||
GridHelper,
|
GridHelper,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
@ -5,6 +6,8 @@ import {
|
|||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
Group,
|
Group,
|
||||||
Mesh,
|
Mesh,
|
||||||
|
Quaternion,
|
||||||
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
|
|
||||||
export function createGridHelper({
|
export function createGridHelper({
|
||||||
@ -31,3 +34,9 @@ export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
|||||||
|
|
||||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
||||||
(group.position.distanceTo(cam.position) * cam.fov) / 4000
|
(group.position.distanceTo(cam.position) * cam.fov) / 4000
|
||||||
|
|
||||||
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
// no x or y components means it's vertical
|
||||||
|
return compareVec2Epsilon2([v.x, v.y], [0, 0])
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
Quaternion,
|
Quaternion,
|
||||||
Scene,
|
Scene,
|
||||||
Shape,
|
Shape,
|
||||||
|
SphereGeometry,
|
||||||
Vector2,
|
Vector2,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
@ -24,7 +25,6 @@ import {
|
|||||||
defaultPlaneColor,
|
defaultPlaneColor,
|
||||||
getSceneScale,
|
getSceneScale,
|
||||||
INTERSECTION_PLANE_LAYER,
|
INTERSECTION_PLANE_LAYER,
|
||||||
isQuaternionVertical,
|
|
||||||
RAYCASTABLE_PLANE,
|
RAYCASTABLE_PLANE,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
SKETCH_GROUP_SEGMENTS,
|
SKETCH_GROUP_SEGMENTS,
|
||||||
@ -34,6 +34,7 @@ import {
|
|||||||
Y_AXIS,
|
Y_AXIS,
|
||||||
YZ_PLANE,
|
YZ_PLANE,
|
||||||
} from './sceneInfra'
|
} from './sceneInfra'
|
||||||
|
import { isQuaternionVertical } from './helpers'
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
getTangentialArcToInfo,
|
getTangentialArcToInfo,
|
||||||
@ -85,6 +86,7 @@ export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
|||||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||||
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||||
'tangential-arc-to-segment-body-dashed'
|
'tangential-arc-to-segment-body-dashed'
|
||||||
|
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
|
||||||
|
|
||||||
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
||||||
// That mostly mean sketch elements.
|
// That mostly mean sketch elements.
|
||||||
@ -98,17 +100,17 @@ class SceneEntities {
|
|||||||
currentSketchQuaternion: Quaternion | null = null
|
currentSketchQuaternion: Quaternion | null = null
|
||||||
constructor() {
|
constructor() {
|
||||||
this.scene = sceneInfra?.scene
|
this.scene = sceneInfra?.scene
|
||||||
sceneInfra?.setOnCamChange(this.onCamChange)
|
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
onCamChange = () => {
|
onCamChange = () => {
|
||||||
const orthoFactor = orthoScale(sceneInfra.camera)
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
|
|
||||||
Object.values(this.activeSegments).forEach((segment) => {
|
Object.values(this.activeSegments).forEach((segment) => {
|
||||||
const factor =
|
const factor =
|
||||||
sceneInfra.camera instanceof OrthographicCamera
|
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||||
? orthoFactor
|
? orthoFactor
|
||||||
: perspScale(sceneInfra.camera, segment)
|
: perspScale(sceneInfra.camControls.camera, segment)
|
||||||
if (
|
if (
|
||||||
segment.userData.from &&
|
segment.userData.from &&
|
||||||
segment.userData.to &&
|
segment.userData.to &&
|
||||||
@ -139,9 +141,9 @@ class SceneEntities {
|
|||||||
})
|
})
|
||||||
if (this.axisGroup) {
|
if (this.axisGroup) {
|
||||||
const factor =
|
const factor =
|
||||||
sceneInfra.camera instanceof OrthographicCamera
|
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||||
? orthoFactor
|
? orthoFactor
|
||||||
: perspScale(sceneInfra.camera, this.axisGroup)
|
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
||||||
const x = this.axisGroup.getObjectByName(X_AXIS)
|
const x = this.axisGroup.getObjectByName(X_AXIS)
|
||||||
x?.scale.set(1, factor, 1)
|
x?.scale.set(1, factor, 1)
|
||||||
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
||||||
@ -150,7 +152,12 @@ class SceneEntities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createIntersectionPlane() {
|
createIntersectionPlane() {
|
||||||
const planeGeometry = new PlaneGeometry(100000, 100000)
|
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
|
||||||
|
console.warn('createIntersectionPlane called when it already exists')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hundredM = 1000000
|
||||||
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
||||||
const planeMaterial = new MeshBasicMaterial({
|
const planeMaterial = new MeshBasicMaterial({
|
||||||
color: 0xff0000,
|
color: 0xff0000,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
@ -195,11 +202,12 @@ class SceneEntities {
|
|||||||
|
|
||||||
this.axisGroup = new Group()
|
this.axisGroup = new Group()
|
||||||
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
|
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
|
||||||
|
gridHelper.position.z = -0.01
|
||||||
gridHelper.renderOrder = -3 // is this working?
|
gridHelper.renderOrder = -3 // is this working?
|
||||||
gridHelper.name = 'gridHelper'
|
gridHelper.name = 'gridHelper'
|
||||||
const sceneScale = getSceneScale(
|
const sceneScale = getSceneScale(
|
||||||
sceneInfra.camera,
|
sceneInfra.camControls.camera,
|
||||||
sceneInfra.controls.target
|
sceneInfra.camControls.target
|
||||||
)
|
)
|
||||||
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
||||||
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
|
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
|
||||||
@ -233,22 +241,17 @@ class SceneEntities {
|
|||||||
ast,
|
ast,
|
||||||
// is draft line assumes the last segment is a draft line, and mods it as the user moves the mouse
|
// is draft line assumes the last segment is a draft line, and mods it as the user moves the mouse
|
||||||
draftSegment,
|
draftSegment,
|
||||||
|
skipListeners,
|
||||||
}: {
|
}: {
|
||||||
sketchPathToNode: PathToNode
|
sketchPathToNode: PathToNode
|
||||||
ast?: Program
|
ast?: Program
|
||||||
draftSegment?: DraftSegment
|
draftSegment?: DraftSegment
|
||||||
|
skipListeners?: boolean
|
||||||
}) {
|
}) {
|
||||||
sceneInfra.resetMouseListeners()
|
if (!skipListeners) {
|
||||||
this.createIntersectionPlane()
|
sceneInfra.resetMouseListeners()
|
||||||
const distance = sceneInfra.controls.target.distanceTo(
|
|
||||||
sceneInfra.camera.position
|
|
||||||
)
|
|
||||||
// TODO this should probably be distance to the sketch group, more important after sketch on face
|
|
||||||
// since sketches won't always so close to the origin
|
|
||||||
// is this the best place to adjust camera far?
|
|
||||||
if (sceneInfra.camera.far < distance * 1.5) {
|
|
||||||
sceneInfra.camera.far = distance * 2
|
|
||||||
}
|
}
|
||||||
|
this.createIntersectionPlane()
|
||||||
|
|
||||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||||
this.prepareTruncatedMemoryAndAst(
|
this.prepareTruncatedMemoryAndAst(
|
||||||
@ -280,11 +283,11 @@ class SceneEntities {
|
|||||||
sketchGroup.position[1],
|
sketchGroup.position[1],
|
||||||
sketchGroup.position[2]
|
sketchGroup.position[2]
|
||||||
)
|
)
|
||||||
const orthoFactor = orthoScale(sceneInfra.camera)
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
const factor =
|
const factor =
|
||||||
sceneInfra.camera instanceof OrthographicCamera
|
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||||
? orthoFactor
|
? orthoFactor
|
||||||
: perspScale(sceneInfra.camera, dummy)
|
: perspScale(sceneInfra.camControls.camera, dummy)
|
||||||
sketchGroup.value.forEach((segment, index) => {
|
sketchGroup.value.forEach((segment, index) => {
|
||||||
let segPathToNode = getNodePathFromSourceRange(
|
let segPathToNode = getNodePathFromSourceRange(
|
||||||
draftSegment ? truncatedAst : kclManager.ast,
|
draftSegment ? truncatedAst : kclManager.ast,
|
||||||
@ -329,11 +332,60 @@ class SceneEntities {
|
|||||||
this.currentSketchQuaternion
|
this.currentSketchQuaternion
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||||
|
|
||||||
this.scene.add(group)
|
this.scene.add(group)
|
||||||
if (!draftSegment) {
|
if (!draftSegment && !skipListeners) {
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onDrag: (args) => {
|
onDragEnd: async () => {
|
||||||
|
if (addingNewSegmentStatus !== 'nothing') {
|
||||||
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
this.setupSketch({ sketchPathToNode })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrag: async (args) => {
|
||||||
if (args.event.which !== 1) return
|
if (args.event.which !== 1) return
|
||||||
|
const group = getParentGroup(args.object, [EXTRA_SEGMENT_HANDLE])
|
||||||
|
if (group?.name === EXTRA_SEGMENT_HANDLE) {
|
||||||
|
const segGroup = getParentGroup(args.object)
|
||||||
|
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
|
||||||
|
const pathToNodeIndex = pathToNode.findIndex(
|
||||||
|
(x) => x[1] === 'PipeExpression'
|
||||||
|
)
|
||||||
|
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
|
||||||
|
if (addingNewSegmentStatus === 'nothing') {
|
||||||
|
const prevSegment = sketchGroup.value[pipeIndex - 2]
|
||||||
|
const yo = addNewSketchLn({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
to: [args.intersection2d.x, args.intersection2d.y],
|
||||||
|
from: [prevSegment.from[0], prevSegment.from[1]],
|
||||||
|
fnName:
|
||||||
|
prevSegment.type === 'TangentialArcTo'
|
||||||
|
? 'tangentialArcTo'
|
||||||
|
: 'line',
|
||||||
|
pathToNode: pathToNode,
|
||||||
|
})
|
||||||
|
addingNewSegmentStatus = 'pending'
|
||||||
|
await kclManager.executeAstMock(yo.modifiedAst, {
|
||||||
|
updates: 'code',
|
||||||
|
})
|
||||||
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
this.setupSketch({ sketchPathToNode, skipListeners: true })
|
||||||
|
addingNewSegmentStatus = 'added'
|
||||||
|
} else if (addingNewSegmentStatus === 'added') {
|
||||||
|
const pathToNodeForNewSegment = pathToNode.slice(
|
||||||
|
0,
|
||||||
|
pathToNodeIndex
|
||||||
|
)
|
||||||
|
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
|
||||||
|
this.onDragSegment({
|
||||||
|
...args,
|
||||||
|
sketchPathToNode: pathToNodeForNewSegment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
...args,
|
...args,
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
@ -394,7 +446,7 @@ class SceneEntities {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else if (draftSegment && !skipListeners) {
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onDrag: () => {},
|
onDrag: () => {},
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
@ -451,7 +503,7 @@ class SceneEntities {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sceneInfra.controls.enableRotate = false
|
sceneInfra.camControls.enableRotate = false
|
||||||
}
|
}
|
||||||
updateAstAndRejigSketch = async (
|
updateAstAndRejigSketch = async (
|
||||||
sketchPathToNode: PathToNode,
|
sketchPathToNode: PathToNode,
|
||||||
@ -503,6 +555,7 @@ class SceneEntities {
|
|||||||
variableDeclarationName: string
|
variableDeclarationName: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
if (object.name === STRAIGHT_SEGMENT_BODY) return
|
||||||
const group = getParentGroup(object)
|
const group = getParentGroup(object)
|
||||||
if (!group) return
|
if (!group) return
|
||||||
const pathToNode: PathToNode = JSON.parse(
|
const pathToNode: PathToNode = JSON.parse(
|
||||||
@ -554,7 +607,7 @@ class SceneEntities {
|
|||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
const sketchGroup = programMemory.root[variableDeclarationName]
|
const sketchGroup = programMemory.root[variableDeclarationName]
|
||||||
.value as Path[]
|
.value as Path[]
|
||||||
const orthoFactor = orthoScale(sceneInfra.camera)
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
sketchGroup.forEach((segment, index) => {
|
sketchGroup.forEach((segment, index) => {
|
||||||
const segPathToNode = getNodePathFromSourceRange(
|
const segPathToNode = getNodePathFromSourceRange(
|
||||||
modifiedAst,
|
modifiedAst,
|
||||||
@ -570,9 +623,9 @@ class SceneEntities {
|
|||||||
// const prevSegment = sketchGroup.slice(index - 1)[0]
|
// const prevSegment = sketchGroup.slice(index - 1)[0]
|
||||||
const type = group?.userData?.type
|
const type = group?.userData?.type
|
||||||
const factor =
|
const factor =
|
||||||
sceneInfra.camera instanceof OrthographicCamera
|
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||||
? orthoFactor
|
? orthoFactor
|
||||||
: perspScale(sceneInfra.camera, group)
|
: perspScale(sceneInfra.camControls.camera, group)
|
||||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||||
this.updateTangentialArcToSegment({
|
this.updateTangentialArcToSegment({
|
||||||
prevSegment: sketchGroup[index - 1],
|
prevSegment: sketchGroup[index - 1],
|
||||||
@ -609,9 +662,7 @@ class SceneEntities {
|
|||||||
group.userData.from = from
|
group.userData.from = from
|
||||||
group.userData.to = to
|
group.userData.to = to
|
||||||
group.userData.prevSegment = prevSegment
|
group.userData.prevSegment = prevSegment
|
||||||
const arrowGroup = group.children.find(
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
(child) => child.userData.type === ARROWHEAD
|
|
||||||
) as Group
|
|
||||||
|
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
|
||||||
@ -686,9 +737,7 @@ class SceneEntities {
|
|||||||
const shape = new Shape()
|
const shape = new Shape()
|
||||||
shape.moveTo(0, -0.08 * scale)
|
shape.moveTo(0, -0.08 * scale)
|
||||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||||
const arrowGroup = group.children.find(
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
(child) => child.userData.type === ARROWHEAD
|
|
||||||
) as Group
|
|
||||||
|
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
|
||||||
@ -701,6 +750,32 @@ class SceneEntities {
|
|||||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||||
arrowGroup.scale.set(scale, scale, scale)
|
arrowGroup.scale.set(scale, scale, scale)
|
||||||
|
|
||||||
|
// TODO this should be created in setupSketch, not updateStraightSegment
|
||||||
|
// it should only be updated here
|
||||||
|
const extraSegmentHandle = (group.getObjectByName(EXTRA_SEGMENT_HANDLE) ||
|
||||||
|
(() => {
|
||||||
|
const mat = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
|
const sphereMesh = new Mesh(new SphereGeometry(0.6, 12, 12), mat)
|
||||||
|
|
||||||
|
const handleGroup = new Group()
|
||||||
|
handleGroup.userData.type = EXTRA_SEGMENT_HANDLE
|
||||||
|
handleGroup.name = EXTRA_SEGMENT_HANDLE
|
||||||
|
handleGroup.add(sphereMesh)
|
||||||
|
handleGroup.layers.set(SKETCH_LAYER)
|
||||||
|
handleGroup.traverse((child) => {
|
||||||
|
child.layers.set(SKETCH_LAYER)
|
||||||
|
})
|
||||||
|
return handleGroup
|
||||||
|
})()) as Group
|
||||||
|
|
||||||
|
extraSegmentHandle.position.set(
|
||||||
|
from[0] + 0.08 * (to[0] - from[0]),
|
||||||
|
from[1] + 0.08 * (to[1] - from[1]),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
extraSegmentHandle.scale.set(scale, scale, scale)
|
||||||
|
group.add(extraSegmentHandle)
|
||||||
|
|
||||||
const straightSegmentBody = group.children.find(
|
const straightSegmentBody = group.children.find(
|
||||||
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
||||||
) as Mesh
|
) as Mesh
|
||||||
@ -729,10 +804,10 @@ class SceneEntities {
|
|||||||
}
|
}
|
||||||
async animateAfterSketch() {
|
async animateAfterSketch() {
|
||||||
if (isReducedMotion()) {
|
if (isReducedMotion()) {
|
||||||
sceneInfra.usePerspectiveCamera()
|
sceneInfra.camControls.usePerspectiveCamera()
|
||||||
} else {
|
return
|
||||||
await sceneInfra.animateToPerspective()
|
|
||||||
}
|
}
|
||||||
|
await sceneInfra.camControls.animateToPerspective()
|
||||||
}
|
}
|
||||||
removeSketchGrid() {
|
removeSketchGrid() {
|
||||||
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
||||||
@ -764,7 +839,7 @@ class SceneEntities {
|
|||||||
reject()
|
reject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sceneInfra.controls.enableRotate = true
|
sceneInfra.camControls.enableRotate = true
|
||||||
this.activeSegments = {}
|
this.activeSegments = {}
|
||||||
// maybe should reset onMove etc handlers
|
// maybe should reset onMove etc handlers
|
||||||
if (shouldResolve) resolve(true)
|
if (shouldResolve) resolve(true)
|
||||||
@ -797,9 +872,8 @@ class SceneEntities {
|
|||||||
onClick: (args) => {
|
onClick: (args) => {
|
||||||
if (!args || !args.object) return
|
if (!args || !args.object) return
|
||||||
if (args.event.which !== 1) return
|
if (args.event.which !== 1) return
|
||||||
const { object, intersection } = args
|
const { intersection } = args
|
||||||
const type = object?.userData?.type || ''
|
const type = intersection.object.name || ''
|
||||||
console.log('intersection.normal?.z', intersection)
|
|
||||||
const posNorm = Number(intersection.normal?.z) > 0
|
const posNorm = Number(intersection.normal?.z) > 0
|
||||||
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
||||||
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
AmbientLight,
|
AmbientLight,
|
||||||
Color,
|
Color,
|
||||||
Euler,
|
|
||||||
GridHelper,
|
GridHelper,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
OrthographicCamera,
|
OrthographicCamera,
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
Quaternion,
|
|
||||||
Scene,
|
Scene,
|
||||||
Vector3,
|
Vector3,
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
@ -20,32 +18,26 @@ import {
|
|||||||
Intersection,
|
Intersection,
|
||||||
Object3D,
|
Object3D,
|
||||||
Object3DEventMap,
|
Object3DEventMap,
|
||||||
|
BoxGeometry,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
|
||||||
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
|
||||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { deg2Rad } from 'lib/utils2d'
|
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls'
|
|
||||||
import { SourceRange } from 'lang/wasm'
|
import { SourceRange } from 'lang/wasm'
|
||||||
import { Axis } from 'lib/selections'
|
import { Axis } from 'lib/selections'
|
||||||
import { BaseUnit, SETTINGS_PERSIST_KEY } from 'machines/settingsMachine'
|
import { BaseUnit, SETTINGS_PERSIST_KEY } from 'machines/settingsMachine'
|
||||||
|
import { CameraControls } from './CameraControls'
|
||||||
|
|
||||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||||
|
|
||||||
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
||||||
// if it were 64, that would feel like it's something in the engine where a random
|
// if it were 64, that would feel like it's something in the engine where a random
|
||||||
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
||||||
const ZOOM_MAGIC_NUMBER = 63.5
|
export const ZOOM_MAGIC_NUMBER = 63.5
|
||||||
const FRAMES_TO_ANIMATE_IN = 30
|
|
||||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
|
||||||
|
|
||||||
export const INTERSECTION_PLANE_LAYER = 1
|
export const INTERSECTION_PLANE_LAYER = 1
|
||||||
export const SKETCH_LAYER = 2
|
export const SKETCH_LAYER = 2
|
||||||
const DEBUG_SHOW_INTERSECTION_PLANE = false
|
export const DEBUG_SHOW_INTERSECTION_PLANE = false
|
||||||
export const DEBUG_SHOW_BOTH_SCENES = false
|
export const DEBUG_SHOW_BOTH_SCENES = false
|
||||||
|
|
||||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||||
@ -57,100 +49,6 @@ export const AXIS_GROUP = 'axisGroup'
|
|||||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||||
export const ARROWHEAD = 'arrowhead'
|
export const ARROWHEAD = 'arrowhead'
|
||||||
|
|
||||||
const tempQuaternion = new Quaternion() // just used for maths
|
|
||||||
|
|
||||||
interface ThreeCamValues {
|
|
||||||
position: Vector3
|
|
||||||
quaternion: Quaternion
|
|
||||||
zoom: number
|
|
||||||
isPerspective: boolean
|
|
||||||
target: Vector3
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastCmdDelay = 50
|
|
||||||
|
|
||||||
let lastCmd: EngineCommand | null = null
|
|
||||||
let lastCmdTime: number = Date.now()
|
|
||||||
let lastCmdTimeoutId: number | null = null
|
|
||||||
|
|
||||||
const sendLastReliableChannel = () => {
|
|
||||||
if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) {
|
|
||||||
engineCommandManager.sendSceneCommand(lastCmd, true)
|
|
||||||
lastCmdTime = Date.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
|
|
||||||
const cmd: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
...convertThreeCamValuesToEngineCam(threeValues),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand(cmd)
|
|
||||||
lastCmd = cmd
|
|
||||||
lastCmdTime = Date.now()
|
|
||||||
|
|
||||||
if (lastCmdTimeoutId !== null) {
|
|
||||||
clearTimeout(lastCmdTimeoutId)
|
|
||||||
}
|
|
||||||
lastCmdTimeoutId = setTimeout(
|
|
||||||
sendLastReliableChannel,
|
|
||||||
lastCmdDelay
|
|
||||||
) as any as number
|
|
||||||
}, 1000 / 30)
|
|
||||||
|
|
||||||
let lastPerspectiveCmd: EngineCommand | null = null
|
|
||||||
let lastPerspectiveCmdTime: number = Date.now()
|
|
||||||
let lastPerspectiveCmdTimeoutId: number | null = null
|
|
||||||
|
|
||||||
const sendLastPerspectiveReliableChannel = () => {
|
|
||||||
if (
|
|
||||||
lastPerspectiveCmd &&
|
|
||||||
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
|
|
||||||
) {
|
|
||||||
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
|
|
||||||
lastPerspectiveCmdTime = Date.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const throttledUpdateEngineFov = throttle(
|
|
||||||
(vals: {
|
|
||||||
position: Vector3
|
|
||||||
quaternion: Quaternion
|
|
||||||
zoom: number
|
|
||||||
fov: number
|
|
||||||
target: Vector3
|
|
||||||
}) => {
|
|
||||||
const cmd: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_perspective_settings',
|
|
||||||
...convertThreeCamValuesToEngineCam({
|
|
||||||
...vals,
|
|
||||||
isPerspective: true,
|
|
||||||
}),
|
|
||||||
fov_y: vals.fov,
|
|
||||||
...calculateNearFarFromFOV(vals.fov),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand(cmd)
|
|
||||||
lastPerspectiveCmd = cmd
|
|
||||||
lastPerspectiveCmdTime = Date.now()
|
|
||||||
if (lastPerspectiveCmdTimeoutId !== null) {
|
|
||||||
clearTimeout(lastPerspectiveCmdTimeoutId)
|
|
||||||
}
|
|
||||||
lastPerspectiveCmdTimeoutId = setTimeout(
|
|
||||||
sendLastPerspectiveReliableChannel,
|
|
||||||
lastCmdDelay
|
|
||||||
) as any as number
|
|
||||||
},
|
|
||||||
1000 / 15
|
|
||||||
)
|
|
||||||
|
|
||||||
interface BaseCallbackArgs2 {
|
interface BaseCallbackArgs2 {
|
||||||
object: any
|
object: any
|
||||||
event: any
|
event: any
|
||||||
@ -178,46 +76,37 @@ interface onMoveCallbackArgs {
|
|||||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReactCameraProperties =
|
|
||||||
| {
|
|
||||||
type: 'perspective'
|
|
||||||
fov?: number
|
|
||||||
position: [number, number, number]
|
|
||||||
quaternion: [number, number, number, number]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'orthographic'
|
|
||||||
zoom?: number
|
|
||||||
position: [number, number, number]
|
|
||||||
quaternion: [number, number, number, number]
|
|
||||||
}
|
|
||||||
|
|
||||||
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
||||||
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
|
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
|
||||||
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
|
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
|
||||||
class SceneInfra {
|
class SceneInfra {
|
||||||
static instance: SceneInfra
|
static instance: SceneInfra
|
||||||
scene: Scene
|
scene: Scene
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
|
||||||
renderer: WebGLRenderer
|
renderer: WebGLRenderer
|
||||||
controls: OrbitControls
|
camControls: CameraControls
|
||||||
isPerspective = true
|
isPerspective = true
|
||||||
fov = 45
|
fov = 45
|
||||||
fovBeforeAnimate = 45
|
fovBeforeAnimate = 45
|
||||||
isFovAnimationInProgress = false
|
isFovAnimationInProgress = false
|
||||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
|
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
||||||
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
||||||
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {}
|
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {}
|
||||||
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {}
|
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {}
|
||||||
setCallbacks = (callbacks: {
|
setCallbacks = (callbacks: {
|
||||||
|
onDragStart?: (arg: OnDragCallbackArgs) => void
|
||||||
|
onDragEnd?: (arg: OnDragCallbackArgs) => void
|
||||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||||
onMove?: (arg: onMoveCallbackArgs) => void
|
onMove?: (arg: onMoveCallbackArgs) => void
|
||||||
onClick?: (arg?: OnClickCallbackArgs) => void
|
onClick?: (arg?: OnClickCallbackArgs) => void
|
||||||
onMouseEnter?: (arg: BaseCallbackArgs2) => void
|
onMouseEnter?: (arg: BaseCallbackArgs2) => void
|
||||||
onMouseLeave?: (arg: BaseCallbackArgs2) => void
|
onMouseLeave?: (arg: BaseCallbackArgs2) => void
|
||||||
}) => {
|
}) => {
|
||||||
|
console.trace('setting callbacks')
|
||||||
|
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
|
||||||
|
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
|
||||||
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
||||||
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
||||||
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
||||||
@ -227,6 +116,8 @@ class SceneInfra {
|
|||||||
}
|
}
|
||||||
resetMouseListeners = () => {
|
resetMouseListeners = () => {
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
onDragStart: () => {},
|
||||||
|
onDragEnd: () => {},
|
||||||
onDrag: () => {},
|
onDrag: () => {},
|
||||||
onMove: () => {},
|
onMove: () => {},
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
@ -256,55 +147,18 @@ class SceneInfra {
|
|||||||
selectedObject: null | any = null
|
selectedObject: null | any = null
|
||||||
mouseDownVector: null | Vector2 = null
|
mouseDownVector: null | Vector2 = null
|
||||||
|
|
||||||
// reacts hooks into some of this singleton's properties
|
|
||||||
reactCameraProperties: ReactCameraProperties = {
|
|
||||||
type: 'perspective',
|
|
||||||
fov: 12,
|
|
||||||
position: [0, 0, 0],
|
|
||||||
quaternion: [0, 0, 0, 1],
|
|
||||||
}
|
|
||||||
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
|
||||||
setReactCameraPropertiesCallback = (
|
|
||||||
cb: (a: ReactCameraProperties) => void
|
|
||||||
) => {
|
|
||||||
this.reactCameraPropertiesCallback = cb
|
|
||||||
}
|
|
||||||
setCam = (camProps: ReactCameraProperties) => {
|
|
||||||
if (
|
|
||||||
camProps.type === 'perspective' &&
|
|
||||||
this.camera instanceof OrthographicCamera
|
|
||||||
) {
|
|
||||||
this.usePerspectiveCamera()
|
|
||||||
} else if (
|
|
||||||
camProps.type === 'orthographic' &&
|
|
||||||
this.camera instanceof PerspectiveCamera
|
|
||||||
) {
|
|
||||||
this.useOrthographicCamera()
|
|
||||||
}
|
|
||||||
this.camera.position.set(...camProps.position)
|
|
||||||
this.camera.quaternion.set(...camProps.quaternion)
|
|
||||||
if (
|
|
||||||
camProps.type === 'perspective' &&
|
|
||||||
this.camera instanceof PerspectiveCamera
|
|
||||||
) {
|
|
||||||
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
|
|
||||||
// at the same time
|
|
||||||
} else if (
|
|
||||||
camProps.type === 'orthographic' &&
|
|
||||||
this.camera instanceof OrthographicCamera
|
|
||||||
) {
|
|
||||||
this.camera.zoom = camProps.zoom || 1
|
|
||||||
}
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
this.controls.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// SCENE
|
// SCENE
|
||||||
this.scene = new Scene()
|
this.scene = new Scene()
|
||||||
this.scene.background = new Color(0x000000)
|
this.scene.background = new Color(0x000000)
|
||||||
this.scene.background = null
|
this.scene.background = null
|
||||||
|
|
||||||
|
// RENDERER
|
||||||
|
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
|
||||||
// CAMERA
|
// CAMERA
|
||||||
const camHeightDistanceRatio = 0.5
|
const camHeightDistanceRatio = 0.5
|
||||||
const baseUnit: BaseUnit =
|
const baseUnit: BaseUnit =
|
||||||
@ -315,25 +169,19 @@ class SceneInfra {
|
|||||||
const ang = Math.atan(camHeightDistanceRatio)
|
const ang = Math.atan(camHeightDistanceRatio)
|
||||||
const x = Math.cos(ang) * length
|
const x = Math.cos(ang) * length
|
||||||
const y = Math.sin(ang) * length
|
const y = Math.sin(ang) * length
|
||||||
this.camera = this.createPerspectiveCamera()
|
|
||||||
this.camera.position.set(0, -x, y)
|
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
|
||||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
|
||||||
|
|
||||||
// RENDERER
|
this.camControls = new CameraControls(false, this.renderer.domElement)
|
||||||
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
this.camControls.camera.position.set(0, -x, y)
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
|
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// RAYCASTERS
|
// RAYCASTERS
|
||||||
this.raycaster.layers.enable(SKETCH_LAYER)
|
this.raycaster.layers.enable(SKETCH_LAYER)
|
||||||
this.raycaster.layers.disable(0)
|
this.raycaster.layers.disable(0)
|
||||||
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// CONTROLS
|
|
||||||
this.controls = this.setupOrbitControls()
|
|
||||||
|
|
||||||
// GRID
|
// GRID
|
||||||
const size = 100
|
const size = 100
|
||||||
const divisions = 10
|
const divisions = 10
|
||||||
@ -353,415 +201,40 @@ class SceneInfra {
|
|||||||
|
|
||||||
SceneInfra.instance = this
|
SceneInfra.instance = this
|
||||||
}
|
}
|
||||||
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
|
||||||
() => {}
|
|
||||||
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
|
|
||||||
this._isCamMovingCallback = cb
|
|
||||||
}
|
|
||||||
private _onCamChange: () => void = () => {}
|
|
||||||
setOnCamChange(cb: () => void) {
|
|
||||||
this._onCamChange = cb
|
|
||||||
}
|
|
||||||
setInteractionGuards = (guard: MouseGuard) => {
|
|
||||||
this.interactionGuards = guard
|
|
||||||
// setMouseGuards is oun patch-package patch to orbit controls
|
|
||||||
// see patches/three+0.160.0.patch
|
|
||||||
;(this.controls as any).setMouseGuards(guard)
|
|
||||||
}
|
|
||||||
private createPerspectiveCamera = () => {
|
|
||||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
|
||||||
this.camera = new PerspectiveCamera(
|
|
||||||
this.fov,
|
|
||||||
window.innerWidth / window.innerHeight,
|
|
||||||
z_near,
|
|
||||||
z_far
|
|
||||||
)
|
|
||||||
this.camera.up.set(0, 0, 1)
|
|
||||||
this.camera.layers.enable(SKETCH_LAYER)
|
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
|
||||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
|
||||||
|
|
||||||
return this.camera
|
|
||||||
}
|
|
||||||
setupOrbitControls = (target?: [number, number, number]): OrbitControls => {
|
|
||||||
if (this.controls) this.controls.dispose()
|
|
||||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
|
||||||
if (target) {
|
|
||||||
// if we're swapping from perspective to orthographic,
|
|
||||||
// we'll need to recreate the orbit controls
|
|
||||||
// and most likely want the target to be the same
|
|
||||||
this.controls.target.set(...target)
|
|
||||||
}
|
|
||||||
this.controls.update()
|
|
||||||
this.controls.addEventListener('change', this.onCameraChange)
|
|
||||||
// debounce is needed because the start and end events are fired too often for zoom on scroll
|
|
||||||
let debounceTimer = 0
|
|
||||||
const handleStart = () => {
|
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
|
||||||
this._isCamMovingCallback(true, false)
|
|
||||||
}
|
|
||||||
const handleEnd = () => {
|
|
||||||
debounceTimer = setTimeout(() => {
|
|
||||||
this._isCamMovingCallback(false, false)
|
|
||||||
}, 400) as any as number
|
|
||||||
}
|
|
||||||
this.controls.addEventListener('start', handleStart)
|
|
||||||
this.controls.addEventListener('end', handleEnd)
|
|
||||||
|
|
||||||
// setMouseGuards is oun patch-package patch to orbit controls
|
|
||||||
// see patches/three+0.160.0.patch
|
|
||||||
;(this.controls as any).setMouseGuards(this.interactionGuards)
|
|
||||||
return this.controls
|
|
||||||
}
|
|
||||||
onStreamStart = () => this.onCameraChange()
|
|
||||||
|
|
||||||
deferReactUpdate = throttle((a: ReactCameraProperties) => {
|
|
||||||
this.reactCameraPropertiesCallback(a)
|
|
||||||
}, 200)
|
|
||||||
|
|
||||||
onCameraChange = () => {
|
onCameraChange = () => {
|
||||||
const scale = getSceneScale(this.camera, this.controls.target)
|
const scale = getSceneScale(
|
||||||
|
this.camControls.camera,
|
||||||
|
this.camControls.target
|
||||||
|
)
|
||||||
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
|
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
|
||||||
const axisGroup = this.scene
|
const axisGroup = this.scene
|
||||||
.getObjectByName(AXIS_GROUP)
|
.getObjectByName(AXIS_GROUP)
|
||||||
?.getObjectByName('gridHelper')
|
?.getObjectByName('gridHelper')
|
||||||
planesGroup && planesGroup.scale.set(scale, scale, scale)
|
planesGroup && planesGroup.scale.set(scale, scale, scale)
|
||||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
|
|
||||||
throttledUpdateEngineCamera({
|
|
||||||
quaternion: this.camera.quaternion,
|
|
||||||
position: this.camera.position,
|
|
||||||
zoom: this.camera.zoom,
|
|
||||||
isPerspective: this.isPerspective,
|
|
||||||
target: this.controls.target,
|
|
||||||
})
|
|
||||||
this.deferReactUpdate({
|
|
||||||
type:
|
|
||||||
this.camera instanceof PerspectiveCamera
|
|
||||||
? 'perspective'
|
|
||||||
: 'orthographic',
|
|
||||||
[this.camera instanceof PerspectiveCamera ? 'fov' : 'zoom']:
|
|
||||||
this.camera instanceof PerspectiveCamera
|
|
||||||
? this.camera.fov
|
|
||||||
: this.camera.zoom,
|
|
||||||
position: [
|
|
||||||
roundOff(this.camera.position.x, 2),
|
|
||||||
roundOff(this.camera.position.y, 2),
|
|
||||||
roundOff(this.camera.position.z, 2),
|
|
||||||
],
|
|
||||||
quaternion: [
|
|
||||||
roundOff(this.camera.quaternion.x, 2),
|
|
||||||
roundOff(this.camera.quaternion.y, 2),
|
|
||||||
roundOff(this.camera.quaternion.z, 2),
|
|
||||||
roundOff(this.camera.quaternion.w, 2),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
this._onCamChange()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onWindowResize = () => {
|
onWindowResize = () => {
|
||||||
if (this.camera instanceof PerspectiveCamera) {
|
|
||||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
|
||||||
} else if (this.camera instanceof OrthographicCamera) {
|
|
||||||
const aspect = window.innerWidth / window.innerHeight
|
|
||||||
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
|
||||||
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
|
||||||
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
|
|
||||||
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
|
|
||||||
}
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
animate = () => {
|
animate = () => {
|
||||||
requestAnimationFrame(this.animate)
|
requestAnimationFrame(this.animate)
|
||||||
TWEEN.update() // This will update all tweens during the animation loop
|
TWEEN.update() // This will update all tweens during the animation loop
|
||||||
if (!this.isFovAnimationInProgress)
|
if (!this.isFovAnimationInProgress) {
|
||||||
this.renderer.render(this.scene, this.camera)
|
// console.log('animation frame', this.cameraControls.camera)
|
||||||
}
|
this.camControls.update()
|
||||||
async tweenCameraToQuaternion(
|
this.renderer.render(this.scene, this.camControls.camera)
|
||||||
targetQuaternion: Quaternion,
|
|
||||||
duration = 500,
|
|
||||||
toOrthographic = true
|
|
||||||
): Promise<void> {
|
|
||||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
|
||||||
let _duration = duration
|
|
||||||
if (isVertical) {
|
|
||||||
_duration = duration * 0.6
|
|
||||||
await this._tweenCameraToQuaternion(new Quaternion(), _duration, false)
|
|
||||||
}
|
}
|
||||||
await this._tweenCameraToQuaternion(
|
|
||||||
targetQuaternion,
|
|
||||||
_duration,
|
|
||||||
toOrthographic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_tweenCameraToQuaternion(
|
|
||||||
targetQuaternion: Quaternion,
|
|
||||||
duration = 500,
|
|
||||||
toOrthographic = false
|
|
||||||
): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const camera = this.camera
|
|
||||||
this._isCamMovingCallback(true, true)
|
|
||||||
const initialQuaternion = camera.quaternion.clone()
|
|
||||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
|
||||||
let tweenEnd = isVertical ? 0.99 : 1
|
|
||||||
const controlsTarget = this.controls.target.clone()
|
|
||||||
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
|
|
||||||
|
|
||||||
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
|
|
||||||
const currentQ = tempQuaternion.slerpQuaternions(
|
|
||||||
initialQuaternion,
|
|
||||||
targetQuaternion,
|
|
||||||
animationProgress
|
|
||||||
)
|
|
||||||
if (this.camera instanceof PerspectiveCamera)
|
|
||||||
// changing the camera position back when it's orthographic doesn't do anything
|
|
||||||
// and it messes up animating back to perspective later
|
|
||||||
this.camera.position
|
|
||||||
.set(0, 0, 1)
|
|
||||||
.applyQuaternion(currentQ)
|
|
||||||
.multiplyScalar(initialDistance)
|
|
||||||
.add(controlsTarget)
|
|
||||||
|
|
||||||
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
|
|
||||||
this.camera.quaternion.copy(currentQ)
|
|
||||||
this.controls.target.copy(controlsTarget)
|
|
||||||
this.controls.update()
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onComplete = async () => {
|
|
||||||
if (isReducedMotion() && toOrthographic) {
|
|
||||||
cameraAtTime(0.99)
|
|
||||||
this.useOrthographicCamera()
|
|
||||||
} else if (toOrthographic) {
|
|
||||||
await this.animateToOrthographic()
|
|
||||||
}
|
|
||||||
if (isVertical) cameraAtTime(1)
|
|
||||||
this.camera.up.set(0, 0, 1)
|
|
||||||
this.controls.enableRotate = false
|
|
||||||
this._isCamMovingCallback(false, true)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReducedMotion()) {
|
|
||||||
onComplete()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
new TWEEN.Tween({ t: 0 })
|
|
||||||
.to({ t: tweenEnd }, duration)
|
|
||||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
|
||||||
.onUpdate(({ t }) => cameraAtTime(t))
|
|
||||||
.onComplete(onComplete)
|
|
||||||
.start()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animateToOrthographic = () =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
this.isFovAnimationInProgress = true
|
|
||||||
let currentFov = this.fov
|
|
||||||
this.fovBeforeAnimate = this.fov
|
|
||||||
|
|
||||||
const targetFov = 4
|
|
||||||
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
|
|
||||||
let frameWaitOnFinish = 10
|
|
||||||
|
|
||||||
const animateFovChange = () => {
|
|
||||||
if (this.camera instanceof PerspectiveCamera) {
|
|
||||||
if (this.camera.fov > targetFov) {
|
|
||||||
// Decrease the FOV
|
|
||||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
this.dollyZoom(currentFov)
|
|
||||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
|
||||||
} else if (frameWaitOnFinish > 0) {
|
|
||||||
frameWaitOnFinish--
|
|
||||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
|
||||||
} else {
|
|
||||||
// Once the target FOV is reached, switch to the orthographic camera
|
|
||||||
// Needs to wait a couple frames after the FOV animation is complete
|
|
||||||
this.useOrthographicCamera()
|
|
||||||
this.isFovAnimationInProgress = false
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animateFovChange() // Start the animation
|
|
||||||
})
|
|
||||||
|
|
||||||
animateToPerspective = () =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
this.isFovAnimationInProgress = true
|
|
||||||
// Immediately set the camera to perspective with a very low FOV
|
|
||||||
this.fov = 4
|
|
||||||
let currentFov = 4
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
const targetFov = this.fovBeforeAnimate // Target FOV for perspective
|
|
||||||
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
|
|
||||||
this.usePerspectiveCamera()
|
|
||||||
|
|
||||||
const animateFovChange = () => {
|
|
||||||
if (this.camera instanceof OrthographicCamera) return
|
|
||||||
if (this.camera.fov < targetFov) {
|
|
||||||
// Increase the FOV
|
|
||||||
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
|
|
||||||
// this.camera.fov = currentFov
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
this.dollyZoom(currentFov)
|
|
||||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
|
||||||
} else {
|
|
||||||
// Set the flag to false as the FOV animation is complete
|
|
||||||
this.isFovAnimationInProgress = false
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animateFovChange() // Start the animation
|
|
||||||
})
|
|
||||||
dispose = () => {
|
dispose = () => {
|
||||||
// Dispose of scene resources, renderer, and controls
|
// Dispose of scene resources, renderer, and controls
|
||||||
this.renderer.dispose()
|
this.renderer.dispose()
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
// Dispose of any other resources like geometries, materials, textures
|
// Dispose of any other resources like geometries, materials, textures
|
||||||
}
|
}
|
||||||
|
|
||||||
useOrthographicCamera = () => {
|
|
||||||
this.isPerspective = false
|
|
||||||
const { x: px, y: py, z: pz } = this.camera.position
|
|
||||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
|
||||||
const { x: tx, y: ty, z: tz } = this.controls.target
|
|
||||||
const aspect = window.innerWidth / window.innerHeight
|
|
||||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
|
||||||
this.camera = new OrthographicCamera(
|
|
||||||
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
|
||||||
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
|
||||||
ORTHOGRAPHIC_CAMERA_SIZE,
|
|
||||||
-ORTHOGRAPHIC_CAMERA_SIZE,
|
|
||||||
z_near,
|
|
||||||
z_far
|
|
||||||
)
|
|
||||||
this.camera.up.set(0, 0, 1)
|
|
||||||
this.camera.layers.enable(SKETCH_LAYER)
|
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
|
||||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
|
||||||
this.camera.position.set(px, py, pz)
|
|
||||||
const distance = this.camera.position.distanceTo(new Vector3(tx, ty, tz))
|
|
||||||
const fovFactor = 45 / this.fov
|
|
||||||
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
|
|
||||||
|
|
||||||
this.setupOrbitControls([tx, ty, tz])
|
|
||||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
this.controls.update()
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_set_orthographic',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
usePerspectiveCamera = () => {
|
|
||||||
this.isPerspective = true
|
|
||||||
const { x: px, y: py, z: pz } = this.camera.position
|
|
||||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
|
||||||
const { x: tx, y: ty, z: tz } = this.controls.target
|
|
||||||
const zoom = this.camera.zoom
|
|
||||||
this.camera = this.createPerspectiveCamera()
|
|
||||||
|
|
||||||
this.camera.position.set(px, py, pz)
|
|
||||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
|
||||||
const zoomFudgeFactor = 2280
|
|
||||||
const distance = zoomFudgeFactor / (zoom * this.fov)
|
|
||||||
const direction = new Vector3().subVectors(
|
|
||||||
this.camera.position,
|
|
||||||
this.controls.target
|
|
||||||
)
|
|
||||||
direction.normalize()
|
|
||||||
this.camera.position
|
|
||||||
.copy(this.controls.target)
|
|
||||||
.addScaledVector(direction, distance)
|
|
||||||
|
|
||||||
this.setupOrbitControls([tx, ty, tz])
|
|
||||||
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_set_perspective',
|
|
||||||
parameters: {
|
|
||||||
fov_y: this.camera.fov,
|
|
||||||
...calculateNearFarFromFOV(this.fov),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.onCameraChange()
|
|
||||||
return this.camera
|
|
||||||
}
|
|
||||||
|
|
||||||
dollyZoom = (newFov: number) => {
|
|
||||||
if (!(this.camera instanceof PerspectiveCamera)) {
|
|
||||||
console.warn('Dolly zoom is only applicable to perspective cameras.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.fov = newFov
|
|
||||||
|
|
||||||
// Calculate the direction vector from the camera towards the controls target
|
|
||||||
const direction = new Vector3()
|
|
||||||
.subVectors(this.controls.target, this.camera.position)
|
|
||||||
.normalize()
|
|
||||||
|
|
||||||
// Calculate the distance to the controls target before changing the FOV
|
|
||||||
const distanceBefore = this.camera.position.distanceTo(this.controls.target)
|
|
||||||
|
|
||||||
// Calculate the scale factor for the new FOV compared to the old one
|
|
||||||
// This needs to be calculated before updating the camera's FOV
|
|
||||||
const oldFov = this.camera.fov
|
|
||||||
|
|
||||||
const viewHeightFactor = (fov: number) => {
|
|
||||||
/* *
|
|
||||||
/|
|
|
||||||
/ |
|
|
||||||
/ |
|
|
||||||
/ |
|
|
||||||
/ | viewHeight/2
|
|
||||||
/ |
|
|
||||||
/ |
|
|
||||||
/↙️fov/2 |
|
|
||||||
/________|
|
|
||||||
\ |
|
|
||||||
\._._._.|
|
|
||||||
*/
|
|
||||||
return Math.tan(deg2Rad(fov / 2))
|
|
||||||
}
|
|
||||||
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
|
|
||||||
|
|
||||||
this.camera.fov = newFov
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
|
|
||||||
const distanceAfter = distanceBefore * scaleFactor
|
|
||||||
|
|
||||||
const newPosition = this.controls.target
|
|
||||||
.clone()
|
|
||||||
.add(direction.multiplyScalar(-distanceAfter))
|
|
||||||
this.camera.position.copy(newPosition)
|
|
||||||
|
|
||||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
|
||||||
this.camera.near = z_near
|
|
||||||
this.camera.far = z_far
|
|
||||||
|
|
||||||
throttledUpdateEngineFov({
|
|
||||||
fov: newFov,
|
|
||||||
position: newPosition,
|
|
||||||
quaternion: this.camera.quaternion,
|
|
||||||
zoom: this.camera.zoom,
|
|
||||||
target: this.controls.target,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
getPlaneIntersectPoint = (): {
|
getPlaneIntersectPoint = (): {
|
||||||
intersection2d?: Vector2
|
intersection2d?: Vector2
|
||||||
intersectPoint: Vector3
|
intersectPoint: Vector3
|
||||||
@ -769,7 +242,7 @@ class SceneInfra {
|
|||||||
} | null => {
|
} | null => {
|
||||||
this.planeRaycaster.setFromCamera(
|
this.planeRaycaster.setFromCamera(
|
||||||
this.currentMouseVector,
|
this.currentMouseVector,
|
||||||
sceneInfra.camera
|
sceneInfra.camControls.camera
|
||||||
)
|
)
|
||||||
const planeIntersects = this.planeRaycaster.intersectObjects(
|
const planeIntersects = this.planeRaycaster.intersectObjects(
|
||||||
this.scene.children,
|
this.scene.children,
|
||||||
@ -907,7 +380,7 @@ class SceneInfra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the center point
|
// Check the center point
|
||||||
this.raycaster.setFromCamera(mouseDownVector, this.camera)
|
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
||||||
updateClosestIntersection(
|
updateClosestIntersection(
|
||||||
this.raycaster.intersectObjects(this.scene.children, true)
|
this.raycaster.intersectObjects(this.scene.children, true)
|
||||||
)
|
)
|
||||||
@ -922,7 +395,7 @@ class SceneInfra {
|
|||||||
mouseDownVector.x + offsetX,
|
mouseDownVector.x + offsetX,
|
||||||
mouseDownVector.y - offsetY
|
mouseDownVector.y - offsetY
|
||||||
)
|
)
|
||||||
this.raycaster.setFromCamera(ringVector, this.camera)
|
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
||||||
updateClosestIntersection(
|
updateClosestIntersection(
|
||||||
this.raycaster.intersectObjects(this.scene.children, true)
|
this.raycaster.intersectObjects(this.scene.children, true)
|
||||||
)
|
)
|
||||||
@ -956,8 +429,13 @@ class SceneInfra {
|
|||||||
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
if (this.selected.hasBeenDragged) {
|
if (this.selected.hasBeenDragged) {
|
||||||
// this is where we could fire a onDragEnd event
|
// TODO do the types properly here
|
||||||
// console.log('onDragEnd', this.selected)
|
this.onDragEndCallback({
|
||||||
|
object: this.selected.object,
|
||||||
|
event,
|
||||||
|
intersection2d: planeIntersectPoint?.intersection2d,
|
||||||
|
...planeIntersectPoint,
|
||||||
|
} as any)
|
||||||
} else if (planeIntersectPoint) {
|
} else if (planeIntersectPoint) {
|
||||||
// fire onClick event as there was no drags
|
// fire onClick event as there was no drags
|
||||||
this.onClickCallback({
|
this.onClickCallback({
|
||||||
@ -1015,7 +493,10 @@ class SceneInfra {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
planesGroup.layers.enable(SKETCH_LAYER)
|
planesGroup.layers.enable(SKETCH_LAYER)
|
||||||
const sceneScale = getSceneScale(this.camera, this.controls.target)
|
const sceneScale = getSceneScale(
|
||||||
|
this.camControls.camera,
|
||||||
|
this.camControls.target
|
||||||
|
)
|
||||||
planesGroup.scale.set(sceneScale, sceneScale, sceneScale)
|
planesGroup.scale.set(sceneScale, sceneScale, sceneScale)
|
||||||
this.scene.add(planesGroup)
|
this.scene.add(planesGroup)
|
||||||
}
|
}
|
||||||
@ -1050,52 +531,6 @@ class SceneInfra {
|
|||||||
|
|
||||||
export const sceneInfra = new SceneInfra()
|
export const sceneInfra = new SceneInfra()
|
||||||
|
|
||||||
function convertThreeCamValuesToEngineCam({
|
|
||||||
target,
|
|
||||||
position,
|
|
||||||
quaternion,
|
|
||||||
zoom,
|
|
||||||
isPerspective,
|
|
||||||
}: ThreeCamValues): {
|
|
||||||
center: Vector3
|
|
||||||
up: Vector3
|
|
||||||
vantage: Vector3
|
|
||||||
} {
|
|
||||||
// Something to consider is that the orbit controls have a target,
|
|
||||||
// we're kind of deriving the target/lookAtVector here when it might not be needed
|
|
||||||
// leaving for now since it's working but maybe revisit later
|
|
||||||
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
|
|
||||||
|
|
||||||
const lookAtVector = new Vector3(0, 0, -1)
|
|
||||||
.applyEuler(euler)
|
|
||||||
.normalize()
|
|
||||||
.add(position)
|
|
||||||
|
|
||||||
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
|
|
||||||
if (isPerspective) {
|
|
||||||
return {
|
|
||||||
center: target,
|
|
||||||
up: upVector,
|
|
||||||
vantage: position,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const zoomFactor = -ZOOM_MAGIC_NUMBER / zoom
|
|
||||||
const direction = lookAtVector.clone().sub(position).normalize()
|
|
||||||
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
|
|
||||||
return {
|
|
||||||
center: lookAtVector,
|
|
||||||
up: upVector,
|
|
||||||
vantage: newVantage,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateNearFarFromFOV(fov: number) {
|
|
||||||
const nearFarRatio = (fov - 3) / (45 - 3)
|
|
||||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
|
||||||
const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
|
||||||
return { z_near: 0.1, z_far }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSceneScale(
|
export function getSceneScale(
|
||||||
camera: PerspectiveCamera | OrthographicCamera,
|
camera: PerspectiveCamera | OrthographicCamera,
|
||||||
target: Vector3
|
target: Vector3
|
||||||
@ -1131,12 +566,6 @@ function baseUnitTomm(baseUnit: BaseUnit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isQuaternionVertical(q: Quaternion) {
|
|
||||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
|
||||||
// no x or y components means it's vertical
|
|
||||||
return compareVec2Epsilon2([v.x, v.y], [0, 0])
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DefaultPlane =
|
export type DefaultPlane =
|
||||||
| 'xy-default-plane'
|
| 'xy-default-plane'
|
||||||
| 'xz-default-plane'
|
| 'xz-default-plane'
|
||||||
|
@ -81,6 +81,7 @@ export function straightSegment({
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
}
|
}
|
||||||
|
group.name = STRAIGHT_SEGMENT
|
||||||
|
|
||||||
const arrowGroup = createArrowhead(scale)
|
const arrowGroup = createArrowhead(scale)
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
@ -169,6 +170,7 @@ export function tangentialArcToSegment({
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
}
|
}
|
||||||
|
group.name = TANGENTIAL_ARC_TO_SEGMENT
|
||||||
|
|
||||||
const arrowGroup = createArrowhead(scale)
|
const arrowGroup = createArrowhead(scale)
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
@ -87,7 +87,7 @@ export function useCalc({
|
|||||||
inputRef: React.RefObject<HTMLInputElement>
|
inputRef: React.RefObject<HTMLInputElement>
|
||||||
valueNode: Value | null
|
valueNode: Value | null
|
||||||
calcResult: string
|
calcResult: string
|
||||||
prevVariables: PrevVariable<any>[]
|
prevVariables: PrevVariable<unknown>[]
|
||||||
newVariableName: string
|
newVariableName: string
|
||||||
isNewVariableNameUnique: boolean
|
isNewVariableNameUnique: boolean
|
||||||
newVariableInsertIndex: number
|
newVariableInsertIndex: number
|
||||||
|
@ -4,7 +4,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
|
|||||||
import { throttle, isReducedMotion } from 'lib/utils'
|
import { throttle, isReducedMotion } from 'lib/utils'
|
||||||
|
|
||||||
const updateDollyZoom = throttle(
|
const updateDollyZoom = throttle(
|
||||||
(newFov: number) => sceneInfra.dollyZoom(newFov),
|
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||||
1000 / 15
|
1000 / 15
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,19 +15,19 @@ export const CamToggle = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
engineCommandManager.waitForReady.then(async () => {
|
engineCommandManager.waitForReady.then(async () => {
|
||||||
sceneInfra.dollyZoom(fov)
|
sceneInfra.camControls.dollyZoom(fov)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleCamera = () => {
|
const toggleCamera = () => {
|
||||||
if (isPerspective) {
|
if (isPerspective) {
|
||||||
isReducedMotion()
|
isReducedMotion()
|
||||||
? sceneInfra.useOrthographicCamera()
|
? sceneInfra.camControls.useOrthographicCamera()
|
||||||
: sceneInfra.animateToOrthographic()
|
: sceneInfra.camControls.animateToOrthographic()
|
||||||
} else {
|
} else {
|
||||||
isReducedMotion()
|
isReducedMotion()
|
||||||
? sceneInfra.usePerspectiveCamera()
|
? sceneInfra.camControls.usePerspectiveCamera()
|
||||||
: sceneInfra.animateToPerspective()
|
: sceneInfra.camControls.animateToPerspective()
|
||||||
}
|
}
|
||||||
setIsPerspective(!isPerspective)
|
setIsPerspective(!isPerspective)
|
||||||
}
|
}
|
||||||
@ -60,9 +60,9 @@ export const CamToggle = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (enableRotate) {
|
if (enableRotate) {
|
||||||
sceneInfra.controls.enableRotate = false
|
sceneInfra.camControls.enableRotate = false
|
||||||
} else {
|
} else {
|
||||||
sceneInfra.controls.enableRotate = true
|
sceneInfra.camControls.enableRotate = true
|
||||||
}
|
}
|
||||||
setEnableRotate(!enableRotate)
|
setEnableRotate(!enableRotate)
|
||||||
}}
|
}}
|
||||||
|
@ -57,12 +57,11 @@ export const CommandBarProvider = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
</CommandsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
@ -84,17 +83,25 @@ const CommandBar = () => {
|
|||||||
if (commandBarState.matches('Review')) {
|
if (commandBarState.matches('Review')) {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
|
||||||
commandBarSend({
|
const currentArgName = entries[entries.length - 1][0]
|
||||||
type: commandBarState.matches('Review')
|
const currentArg = {
|
||||||
? 'Edit argument'
|
name: currentArgName,
|
||||||
: 'Change current argument',
|
...entries[entries.length - 1][1],
|
||||||
data: {
|
}
|
||||||
arg: {
|
|
||||||
name: entries[entries.length - 1][0],
|
if (commandBarState.matches('Review')) {
|
||||||
...entries[entries.length - 1][1],
|
commandBarSend({
|
||||||
|
type: 'Edit argument',
|
||||||
|
data: {
|
||||||
|
arg: currentArg,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
} else {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Remove argument',
|
||||||
|
data: { [currentArgName]: currentArg },
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarSend({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
@ -117,6 +124,11 @@ const CommandBar = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => console.log(commandBarState.context.argumentsToSubmit),
|
||||||
|
[commandBarState.context.argumentsToSubmit]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|
@ -4,6 +4,7 @@ import CommandBarSelectionInput from './CommandBarSelectionInput'
|
|||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import CommandBarHeader from './CommandBarHeader'
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
|
import CommandBarKclInput from './CommandBarKclInput'
|
||||||
|
|
||||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
@ -17,10 +18,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
|||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Submit argument',
|
type: 'Submit argument',
|
||||||
data: {
|
data: {
|
||||||
[currentArgument.name]:
|
[currentArgument.name]: data,
|
||||||
currentArgument.inputType === 'number'
|
|
||||||
? parseFloat((data as string) || '0')
|
|
||||||
: data,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -68,6 +66,10 @@ function ArgumentInput({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'kcl':
|
||||||
|
return (
|
||||||
|
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<CommandBarBasicInput
|
<CommandBarBasicInput
|
||||||
|
@ -9,7 +9,7 @@ function CommandBarBasicInput({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
arg: CommandArgument<unknown> & {
|
arg: CommandArgument<unknown> & {
|
||||||
inputType: 'number' | 'string'
|
inputType: 'string'
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
@ -18,7 +18,6 @@ function CommandBarBasicInput({
|
|||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const inputType = arg.inputType === 'number' ? 'number' : 'text'
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@ -40,9 +39,9 @@ function CommandBarBasicInput({
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
id="arg-form"
|
id="arg-form"
|
||||||
name={inputType}
|
name={arg.inputType}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type={inputType}
|
type={arg.inputType}
|
||||||
required
|
required
|
||||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
placeholder="Enter a value"
|
placeholder="Enter a value"
|
||||||
|
@ -4,6 +4,9 @@ import React, { ReactNode, useState } from 'react'
|
|||||||
import { ActionButton } from '../ActionButton'
|
import { ActionButton } from '../ActionButton'
|
||||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
|
||||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
@ -45,6 +48,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
parseInt(b.keys[0], 10) - 1
|
parseInt(b.keys[0], 10) - 1
|
||||||
]
|
]
|
||||||
const arg = selectedCommand?.args[argName]
|
const arg = selectedCommand?.args[argName]
|
||||||
|
if (!argName || !arg) return
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Change current argument',
|
type: 'Change current argument',
|
||||||
data: { arg: { ...arg, name: argName } },
|
data: { arg: { ...arg, name: argName } },
|
||||||
@ -59,7 +63,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
selectedCommand &&
|
selectedCommand &&
|
||||||
argumentsToSubmit && (
|
argumentsToSubmit && (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 text-sm flex gap-4 items-start">
|
<div className="group px-4 text-sm flex gap-4 items-start">
|
||||||
<div className="flex flex-1 flex-wrap gap-2">
|
<div className="flex flex-1 flex-wrap gap-2">
|
||||||
<p
|
<p
|
||||||
data-command-name={selectedCommand?.name}
|
data-command-name={selectedCommand?.name}
|
||||||
@ -91,25 +95,50 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<span className="capitalize">{argName}</span>
|
||||||
{argumentsToSubmit[argName] ? (
|
{argumentsToSubmit[argName] ? (
|
||||||
arg.inputType === 'selection' ? (
|
arg.inputType === 'selection' ? (
|
||||||
getSelectionTypeDisplayText(
|
getSelectionTypeDisplayText(
|
||||||
argumentsToSubmit[argName] as Selections
|
argumentsToSubmit[argName] as Selections
|
||||||
)
|
)
|
||||||
|
) : arg.inputType === 'kcl' ? (
|
||||||
|
roundOff(
|
||||||
|
Number(
|
||||||
|
(argumentsToSubmit[argName] as KclCommandValue)
|
||||||
|
.valueCalculated
|
||||||
|
),
|
||||||
|
4
|
||||||
|
)
|
||||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
||||||
JSON.stringify(argumentsToSubmit[argName])
|
JSON.stringify(argumentsToSubmit[argName])
|
||||||
) : (
|
) : (
|
||||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
||||||
)
|
)
|
||||||
) : (
|
) : null}
|
||||||
<em>{argName}</em>
|
|
||||||
)}
|
|
||||||
{showShortcuts && (
|
{showShortcuts && (
|
||||||
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
||||||
<span className="sr-only">Hotkey: </span>
|
<span className="sr-only">Hotkey: </span>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
{arg.inputType === 'kcl' &&
|
||||||
|
!!argumentsToSubmit[argName] &&
|
||||||
|
'variableName' in
|
||||||
|
(argumentsToSubmit[argName] as KclCommandValue) && (
|
||||||
|
<>
|
||||||
|
<CustomIcon name="make-variable" className="w-4 h-4" />
|
||||||
|
<Tooltip position="blockEnd">
|
||||||
|
New variable:{' '}
|
||||||
|
{
|
||||||
|
(
|
||||||
|
argumentsToSubmit[
|
||||||
|
argName
|
||||||
|
] as KclExpressionWithVariable
|
||||||
|
).variableName
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
17
src/components/CommandBar/CommandBarKclInput.module.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.editor {
|
||||||
|
@apply text-base flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor :global(.cm-editor) {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor :global(.cm-line)::selection {
|
||||||
|
@apply px-1;
|
||||||
|
@apply text-chalkboard-100;
|
||||||
|
@apply bg-energy-10/50;
|
||||||
|
}
|
||||||
|
:global(.dark) .editor :global(.cm-line)::selection {
|
||||||
|
@apply text-energy-10;
|
||||||
|
@apply bg-energy-10/20;
|
||||||
|
}
|
221
src/components/CommandBar/CommandBarKclInput.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { Completion } from '@codemirror/autocomplete'
|
||||||
|
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
|
||||||
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||||
|
import { getSystemTheme } from 'lib/theme'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
import { varMentions } from 'lib/varCompletionExtension'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import styles from './CommandBarKclInput.module.css'
|
||||||
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
|
|
||||||
|
function CommandBarKclInput({
|
||||||
|
arg,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument<unknown> & {
|
||||||
|
inputType: 'kcl'
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (event: unknown) => void
|
||||||
|
}) {
|
||||||
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||||
|
arg.name
|
||||||
|
] as KclCommandValue | undefined
|
||||||
|
const { settings } = useGlobalStateContext()
|
||||||
|
const defaultValue = (arg.defaultValue as string) || ''
|
||||||
|
const [value, setValue] = useState(
|
||||||
|
previouslySetValue?.valueText || defaultValue || ''
|
||||||
|
)
|
||||||
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
|
previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
)
|
||||||
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
prevVariables,
|
||||||
|
calcResult,
|
||||||
|
newVariableInsertIndex,
|
||||||
|
valueNode,
|
||||||
|
newVariableName,
|
||||||
|
setNewVariableName,
|
||||||
|
isNewVariableNameUnique,
|
||||||
|
} = useCalculateKclExpression({
|
||||||
|
value,
|
||||||
|
initialVariableName:
|
||||||
|
previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
? previouslySetValue.variableName
|
||||||
|
: arg.name,
|
||||||
|
})
|
||||||
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
|
label: v.key,
|
||||||
|
detail: String(roundOff(v.value as number)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { setContainer } = useCodeMirror({
|
||||||
|
container: editorRef.current,
|
||||||
|
value,
|
||||||
|
indentWithTab: false,
|
||||||
|
basicSetup: false,
|
||||||
|
autoFocus: true,
|
||||||
|
selection: {
|
||||||
|
anchor: 0,
|
||||||
|
head:
|
||||||
|
previouslySetValue && 'valueText' in previouslySetValue
|
||||||
|
? previouslySetValue.valueText.length
|
||||||
|
: defaultValue.length,
|
||||||
|
},
|
||||||
|
accessKey: 'command-bar',
|
||||||
|
theme:
|
||||||
|
settings.context.theme === 'system'
|
||||||
|
? getSystemTheme()
|
||||||
|
: settings.context.theme,
|
||||||
|
extensions: [
|
||||||
|
EditorView.domEventHandlers({
|
||||||
|
keydown: (event) => {
|
||||||
|
if (event.key === 'Backspace' && value === '') {
|
||||||
|
event.preventDefault()
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
varMentions(varMentionData),
|
||||||
|
EditorState.transactionFilter.of((tr) => {
|
||||||
|
if (tr.newDoc.lines > 1) {
|
||||||
|
handleSubmit()
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return tr
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChange: (newValue) => setValue(newValue),
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
setContainer(editorRef.current)
|
||||||
|
}
|
||||||
|
}, [arg, editorRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCanSubmit(
|
||||||
|
calcResult !== 'NAN' && (!createNewVariable || isNewVariableNameUnique)
|
||||||
|
)
|
||||||
|
}, [calcResult, createNewVariable, isNewVariableNameUnique])
|
||||||
|
|
||||||
|
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e?.preventDefault()
|
||||||
|
if (!canSubmit || valueNode === null) return
|
||||||
|
|
||||||
|
onSubmit(
|
||||||
|
createNewVariable
|
||||||
|
? ({
|
||||||
|
valueAst: valueNode,
|
||||||
|
valueText: value,
|
||||||
|
valueCalculated: calcResult,
|
||||||
|
variableName: newVariableName,
|
||||||
|
insertIndex: newVariableInsertIndex,
|
||||||
|
variableIdentifierAst: createIdentifier(newVariableName),
|
||||||
|
variableDeclarationAst: createVariableDeclaration(
|
||||||
|
newVariableName,
|
||||||
|
valueNode
|
||||||
|
),
|
||||||
|
} satisfies KclCommandValue)
|
||||||
|
: ({
|
||||||
|
valueAst: valueNode,
|
||||||
|
valueText: value,
|
||||||
|
valueCalculated: calcResult,
|
||||||
|
} satisfies KclCommandValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit} data-can-submit={canSubmit}>
|
||||||
|
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
|
||||||
|
<span className="capitalize text-chalkboard-80 dark:text-chalkboard-20">
|
||||||
|
{arg.name}
|
||||||
|
</span>
|
||||||
|
<div ref={editorRef} className={styles.editor} />
|
||||||
|
<CustomIcon
|
||||||
|
name="equal"
|
||||||
|
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
calcResult === 'NAN'
|
||||||
|
? 'text-destroy-80 dark:text-destroy-40'
|
||||||
|
: 'text-energy-60 dark:text-energy-20'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{calcResult === 'NAN'
|
||||||
|
? "Can't calculate"
|
||||||
|
: roundOff(Number(calcResult), 4)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{createNewVariable ? (
|
||||||
|
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
|
||||||
|
<label
|
||||||
|
htmlFor="variable-name"
|
||||||
|
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
||||||
|
>
|
||||||
|
Variable name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="variable-name"
|
||||||
|
name="variable-name"
|
||||||
|
className="flex-1 border-none bg-transparent"
|
||||||
|
placeholder="Variable name"
|
||||||
|
value={newVariableName}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setNewVariableName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.currentTarget.value === '' && e.key === 'Backspace') {
|
||||||
|
setCreateNewVariable(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
isNewVariableNameUnique
|
||||||
|
? 'text-energy-60 dark:text-energy-20'
|
||||||
|
: 'text-destroy-60 dark:text-destroy-40'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-between gap-2 px-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setCreateNewVariable(true)}
|
||||||
|
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
|
||||||
|
>
|
||||||
|
<CustomIcon name="plus" className="w-5 h-5" />
|
||||||
|
Create new variable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarKclInput
|
@ -14,7 +14,18 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
|
[
|
||||||
|
'alt+1',
|
||||||
|
'alt+2',
|
||||||
|
'alt+3',
|
||||||
|
'alt+4',
|
||||||
|
'alt+5',
|
||||||
|
'alt+6',
|
||||||
|
'alt+7',
|
||||||
|
'alt+8',
|
||||||
|
'alt+9',
|
||||||
|
'alt+0',
|
||||||
|
],
|
||||||
(_, b) => {
|
(_, b) => {
|
||||||
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
||||||
if (!selectedCommand?.args) return
|
if (!selectedCommand?.args) return
|
||||||
|
@ -18,10 +18,12 @@ export type CustomIconName =
|
|||||||
| 'horizontal'
|
| 'horizontal'
|
||||||
| 'horizontalDash'
|
| 'horizontalDash'
|
||||||
| 'line'
|
| 'line'
|
||||||
|
| 'make-variable'
|
||||||
| 'move'
|
| 'move'
|
||||||
| 'network'
|
| 'network'
|
||||||
| 'networkCrossedOut'
|
| 'networkCrossedOut'
|
||||||
| 'parallel'
|
| 'parallel'
|
||||||
|
| 'plus'
|
||||||
| 'search'
|
| 'search'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
| 'sketch'
|
| 'sketch'
|
||||||
@ -336,6 +338,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'make-variable':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4.07178 6.57735L9.99998 3.1547L15.9282 6.57735V13.4227L9.99998 16.8453L4.07178 13.4227V6.57735ZM9.99998 2L16.9282 6V14L9.99998 18L3.07178 14V6L9.99998 2ZM9.45068 6.854C9.20802 6.798 8.97468 6.78867 8.75068 6.826C8.39602 6.90067 8.06468 7.04533 7.75668 7.26C7.73802 7.26933 7.72402 7.27867 7.71468 7.288C7.45335 7.484 7.24802 7.694 7.09868 7.918C6.96802 8.09533 6.86068 8.282 6.77668 8.478C6.69268 8.65533 6.63668 8.814 6.60868 8.954C6.60868 9.00067 6.62268 9.038 6.65068 9.066L6.69268 9.108H6.95868C7.13602 9.108 7.23402 9.09867 7.25268 9.08C7.28068 9.052 7.30868 8.982 7.33668 8.87C7.45802 8.52467 7.65402 8.212 7.92468 7.932C8.13002 7.72667 8.36802 7.58667 8.63868 7.512C8.83468 7.456 9.02602 7.456 9.21268 7.512C9.40868 7.57733 9.53002 7.68467 9.57668 7.834C9.62335 7.96467 9.61402 8.198 9.54868 8.534L8.77868 11.614C8.65735 11.9593 8.47535 12.216 8.23268 12.384C8.10202 12.4587 7.97602 12.4913 7.85468 12.482C7.68668 12.482 7.53735 12.4307 7.40668 12.328L7.36468 12.286L7.42068 12.272C7.50468 12.244 7.57002 12.216 7.61668 12.188C7.93402 12.02 8.10668 11.7493 8.13468 11.376C8.15335 11.1053 8.05535 10.9187 7.84068 10.816C7.60735 10.6853 7.34135 10.69 7.04268 10.83C6.73468 10.9793 6.54802 11.2547 6.48268 11.656C6.45468 11.8893 6.47335 12.1087 6.53868 12.314C6.56668 12.4073 6.60868 12.4913 6.66468 12.566C6.92602 12.986 7.32268 13.182 7.85468 13.154C8.31202 13.126 8.72268 12.8787 9.08668 12.412L9.12868 12.37L9.21268 12.496C9.44602 12.8133 9.80068 13.0233 10.2767 13.126C10.5474 13.1633 10.79 13.1633 11.0047 13.126C11.6954 12.9767 12.2507 12.58 12.6707 11.936C12.6894 11.9173 12.7034 11.894 12.7127 11.866C12.9553 11.474 13.0767 11.18 13.0767 10.984C13.0767 10.9373 13.0674 10.9047 13.0487 10.886C13.0207 10.8673 12.918 10.858 12.7407 10.858C12.61 10.858 12.526 10.8627 12.4887 10.872C12.442 10.8813 12.4047 10.9327 12.3767 11.026C12.2834 11.3807 12.092 11.7073 11.8027 12.006C11.56 12.23 11.3174 12.3793 11.0747 12.454C11.0094 12.4727 10.9067 12.482 10.7667 12.482C10.6174 12.482 10.5194 12.4727 10.4727 12.454C10.314 12.398 10.1974 12.3 10.1227 12.16C10.0667 12.0573 10.062 11.8613 10.1087 11.572C10.1087 11.5347 10.132 11.4367 10.1787 11.278C10.58 9.542 10.8274 8.55733 10.9207 8.324C11.0887 7.88533 11.3127 7.61467 11.5927 7.512C11.6114 7.50267 11.63 7.498 11.6487 7.498C11.8914 7.43267 12.0967 7.47467 12.2647 7.624L12.3207 7.68L12.2087 7.722C11.8354 7.85267 11.6207 8.128 11.5647 8.548C11.5367 8.76267 11.5927 8.94 11.7327 9.08C11.77 9.11733 11.8167 9.15 11.8727 9.178C12.1714 9.32733 12.4887 9.28067 12.8247 9.038C12.9367 8.954 13.03 8.83267 13.1047 8.674C13.282 8.26333 13.2774 7.87133 13.0907 7.498C12.9787 7.26467 12.7874 7.078 12.5167 6.938C12.162 6.77933 11.8074 6.76533 11.4527 6.896C11.1447 7.01733 10.8787 7.20867 10.6547 7.47L10.5707 7.582C10.552 7.582 10.524 7.554 10.4867 7.498C10.2627 7.17133 9.91735 6.95667 9.45068 6.854Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'move':
|
case 'move':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -400,6 +418,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'plus':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'search':
|
case 'search':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -13,7 +13,7 @@ type OutputTypeKey = OutputFormat['type']
|
|||||||
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
||||||
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||||
|
|
||||||
interface ExportButtonProps extends React.PropsWithChildren {
|
export interface ExportButtonProps extends React.PropsWithChildren {
|
||||||
className?: {
|
className?: {
|
||||||
button?: string
|
button?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
|
@ -213,7 +213,7 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
const quaternion = getSketchQuaternion(pathToNode, normal)
|
const quaternion = getSketchQuaternion(pathToNode, normal)
|
||||||
await sceneInfra.tweenCameraToQuaternion(quaternion)
|
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
sketchNormalBackUp: normal,
|
sketchNormalBackUp: normal,
|
||||||
@ -227,7 +227,7 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchPathToNode || [],
|
sketchPathToNode || [],
|
||||||
sketchNormalBackUp
|
sketchNormalBackUp
|
||||||
)
|
)
|
||||||
await sceneInfra.tweenCameraToQuaternion(quaternion)
|
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
|
||||||
},
|
},
|
||||||
'Get horizontal info': async ({
|
'Get horizontal info': async ({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -374,6 +374,7 @@ export const ModelingMachineProvider = ({
|
|||||||
send: modelingSend,
|
send: modelingSend,
|
||||||
actor: modelingActor,
|
actor: modelingActor,
|
||||||
commandBarConfig: modelingMachineConfig,
|
commandBarConfig: modelingMachineConfig,
|
||||||
|
allCommandsRequireNetwork: true,
|
||||||
onCancel: () => modelingSend({ type: 'Cancel' }),
|
onCancel: () => modelingSend({ type: 'Cancel' }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ const overallConnectionStateIcon: Record<
|
|||||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NetworkHealthIndicator = () => {
|
export function useNetworkStatus() {
|
||||||
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
|
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
|
||||||
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
||||||
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
||||||
@ -118,18 +118,18 @@ export const NetworkHealthIndicator = () => {
|
|||||||
}, [hasIssues, internetConnected])
|
}, [hasIssues, internetConnected])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cb1 = () => {
|
const onlineCallback = () => {
|
||||||
setSteps(initialConnectingTypeGroupState)
|
setSteps(initialConnectingTypeGroupState)
|
||||||
setInternetConnected(true)
|
setInternetConnected(true)
|
||||||
}
|
}
|
||||||
const cb2 = () => {
|
const offlineCallback = () => {
|
||||||
setInternetConnected(false)
|
setInternetConnected(false)
|
||||||
}
|
}
|
||||||
window.addEventListener('online', cb1)
|
window.addEventListener('online', onlineCallback)
|
||||||
window.addEventListener('offline', cb2)
|
window.addEventListener('offline', offlineCallback)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('online', cb1)
|
window.removeEventListener('online', onlineCallback)
|
||||||
window.removeEventListener('offline', cb2)
|
window.removeEventListener('offline', offlineCallback)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -183,6 +183,30 @@ export const NetworkHealthIndicator = () => {
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasIssues,
|
||||||
|
overallState,
|
||||||
|
internetConnected,
|
||||||
|
steps,
|
||||||
|
issues,
|
||||||
|
error,
|
||||||
|
setHasCopied,
|
||||||
|
hasCopied,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetworkHealthIndicator = () => {
|
||||||
|
const {
|
||||||
|
hasIssues,
|
||||||
|
overallState,
|
||||||
|
internetConnected,
|
||||||
|
steps,
|
||||||
|
issues,
|
||||||
|
error,
|
||||||
|
setHasCopied,
|
||||||
|
hasCopied,
|
||||||
|
} = useNetworkStatus()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
|
@ -3,8 +3,9 @@ import { BrowserRouter } from 'react-router-dom'
|
|||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar/CommandBar'
|
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { ExportButtonProps } from './ExportButton'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -37,15 +38,22 @@ const projectWellFormed = {
|
|||||||
},
|
},
|
||||||
} satisfies ProjectWithEntryPointMetadata
|
} satisfies ProjectWithEntryPointMetadata
|
||||||
|
|
||||||
|
const mockExportButton = vi.fn()
|
||||||
|
vi.mock('/src/components/ExportButton', () => ({
|
||||||
|
// engineCommandManager method call in ExportButton causes vitest to hang
|
||||||
|
ExportButton: (props: ExportButtonProps) => {
|
||||||
|
mockExportButton(props)
|
||||||
|
return <button>Fake export button</button>
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<GlobalStateProvider>
|
||||||
<GlobalStateProvider>
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
</GlobalStateProvider>
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -62,11 +70,9 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders app name if given no project', () => {
|
test('Renders app name if given no project', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<GlobalStateProvider>
|
||||||
<GlobalStateProvider>
|
<ProjectSidebarMenu />
|
||||||
<ProjectSidebarMenu />
|
</GlobalStateProvider>
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,14 +84,9 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders as a link if set to do so', () => {
|
test('Renders as a link if set to do so', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<GlobalStateProvider>
|
||||||
<GlobalStateProvider>
|
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
|
||||||
<ProjectSidebarMenu
|
</GlobalStateProvider>
|
||||||
project={projectWellFormed}
|
|
||||||
renderAsLink={true}
|
|
||||||
/>
|
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
|
|||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
useCalc,
|
|
||||||
CalcResult,
|
CalcResult,
|
||||||
CreateNewVariable,
|
CreateNewVariable,
|
||||||
} from './AvailableVarsHelpers'
|
} from './AvailableVarsHelpers'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
|
||||||
type ModalResolve = {
|
type ModalResolve = {
|
||||||
value: string
|
value: string
|
||||||
@ -55,7 +55,7 @@ export const SetAngleLengthModal = ({
|
|||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
inputRef,
|
inputRef,
|
||||||
newVariableInsertIndex,
|
newVariableInsertIndex,
|
||||||
} = useCalc({
|
} = useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName: valueName,
|
initialVariableName: valueName,
|
||||||
})
|
})
|
||||||
|
@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
|
|||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
useCalc,
|
|
||||||
CalcResult,
|
CalcResult,
|
||||||
CreateNewVariable,
|
CreateNewVariable,
|
||||||
} from './AvailableVarsHelpers'
|
} from './AvailableVarsHelpers'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
|
||||||
type ModalResolve = {
|
type ModalResolve = {
|
||||||
value: string
|
value: string
|
||||||
@ -59,7 +59,7 @@ export const GetInfoModal = ({
|
|||||||
newVariableName,
|
newVariableName,
|
||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
newVariableInsertIndex,
|
newVariableInsertIndex,
|
||||||
} = useCalc({ value: value, initialVariableName })
|
} = useCalculateKclExpression({ value: value, initialVariableName })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
import { CreateNewVariable } from './AvailableVarsHelpers'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { type InstanceProps, create } from 'react-modal-promise'
|
import { type InstanceProps, create } from 'react-modal-promise'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
|
||||||
type ModalResolve = { variableName: string }
|
type ModalResolve = { variableName: string }
|
||||||
type ModalReject = boolean
|
type ModalReject = boolean
|
||||||
@ -25,7 +26,7 @@ export const SetVarNameModal = ({
|
|||||||
valueName,
|
valueName,
|
||||||
}: SetVarNameModalProps) => {
|
}: SetVarNameModalProps) => {
|
||||||
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
||||||
useCalc({ value: '', initialVariableName: valueName })
|
useCalculateKclExpression({ value: '', initialVariableName: valueName })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
@ -16,6 +16,7 @@ import { engineCommandManager } from '../lang/std/engineConnection'
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useKclContext } from 'lang/KclSingleton'
|
import { useKclContext } from 'lang/KclSingleton'
|
||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
|
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }: { className?: string }) => {
|
export const Stream = ({ className = '' }: { className?: string }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@ -38,6 +39,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
const cameraControls = settings?.context?.cameraControls
|
const cameraControls = settings?.context?.cameraControls
|
||||||
const { state } = useModelingContext()
|
const { state } = useModelingContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
|
const { overallState } = useNetworkStatus()
|
||||||
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -164,6 +167,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||||
/>
|
/>
|
||||||
<ClientSideScene cameraControls={settings.context.cameraControls} />
|
<ClientSideScene cameraControls={settings.context.cameraControls} />
|
||||||
|
{!isNetworkOkay && !isLoading && (
|
||||||
|
<div className="text-center absolute inset-0">
|
||||||
|
<Loading>
|
||||||
|
<span data-testid="loading-stream">Stream disconnected</span>
|
||||||
|
</Loading>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
<Loading>
|
<Loading>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import ReactCodeMirror, {
|
import ReactCodeMirror, {
|
||||||
Extension,
|
Extension,
|
||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
@ -11,7 +12,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { processCodeMirrorRanges } from 'lib/selections'
|
import { processCodeMirrorRanges } from 'lib/selections'
|
||||||
@ -25,11 +26,14 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import interact from '@replit/codemirror-interact'
|
import interact from '@replit/codemirror-interact'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
|
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -75,6 +79,28 @@ export const TextEditor = ({
|
|||||||
}))
|
}))
|
||||||
const { code, errors } = useKclContext()
|
const { code, errors } = useKclContext()
|
||||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||||
|
const { overallState } = useNetworkStatus()
|
||||||
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
|
||||||
|
window.addEventListener('online', onlineCallback)
|
||||||
|
return () => window.removeEventListener('online', onlineCallback)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useHotkeys('mod+z', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (editorView) {
|
||||||
|
undo(editorView)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
useHotkeys('mod+shift+z', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (editorView) {
|
||||||
|
redo(editorView)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
context: { selectionRanges, selectionRangeTypeMap },
|
context: { selectionRanges, selectionRangeTypeMap },
|
||||||
@ -85,6 +111,9 @@ export const TextEditor = ({
|
|||||||
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
||||||
useGlobalStateContext()
|
useGlobalStateContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { project },
|
||||||
|
} = useFileContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
@ -107,7 +136,7 @@ export const TextEditor = ({
|
|||||||
}, [setIsKclLspServerReady])
|
}, [setIsKclLspServerReady])
|
||||||
|
|
||||||
// Here we initialize the plugin which will start the client.
|
// Here we initialize the plugin which will start the client.
|
||||||
// When we have multi-file support the name of the file will be a dep of
|
// Now that we have multi-file support the name of the file is a dep of
|
||||||
// this use memo, as well as the directory structure, which I think is
|
// this use memo, as well as the directory structure, which I think is
|
||||||
// a good setup because it will restart the client but not the server :)
|
// a good setup because it will restart the client but not the server :)
|
||||||
// We do not want to restart the server, its just wasteful.
|
// We do not want to restart the server, its just wasteful.
|
||||||
@ -163,11 +192,12 @@ export const TextEditor = ({
|
|||||||
plugin = lsp
|
plugin = lsp
|
||||||
}
|
}
|
||||||
return plugin
|
return plugin
|
||||||
}, [copilotLspClient, isCopilotLspServerReady])
|
}, [copilotLspClient, isCopilotLspServerReady, project])
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (newCode: string) => {
|
const onChange = async (newCode: string) => {
|
||||||
kclManager.setCodeAndExecute(newCode)
|
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
||||||
|
else kclManager.setCode(newCode)
|
||||||
} //, []);
|
} //, []);
|
||||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||||
if (!editorView) {
|
if (!editorView) {
|
||||||
|
@ -142,7 +142,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="externalLink"
|
Element="externalLink"
|
||||||
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
to="https://github.com/KittyCAD/modeling-app/issues/new/choose"
|
||||||
icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
|
icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
|
||||||
className="border-transparent dark:border-transparent"
|
className="border-transparent dark:border-transparent"
|
||||||
>
|
>
|
||||||
|
@ -7,6 +7,12 @@ import { authMachine } from 'machines/authMachine'
|
|||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { homeMachine } from 'machines/homeMachine'
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||||
|
import {
|
||||||
|
NetworkHealthState,
|
||||||
|
useNetworkStatus,
|
||||||
|
} from 'components/NetworkHealthIndicator'
|
||||||
|
import { useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -24,6 +30,7 @@ interface UseStateMachineCommandsArgs<
|
|||||||
send: Function
|
send: Function
|
||||||
actor?: InterpreterFrom<T>
|
actor?: InterpreterFrom<T>
|
||||||
commandBarConfig?: CommandSetConfig<T, S>
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
|
allCommandsRequireNetwork?: boolean
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,12 +43,21 @@ export default function useStateMachineCommands<
|
|||||||
send,
|
send,
|
||||||
actor,
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
|
allCommandsRequireNetwork = false,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T, S>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { overallState } = useNetworkStatus()
|
||||||
|
const { isExecuting } = useKclContext()
|
||||||
|
const { isStreamReady } = useStore((s) => ({
|
||||||
|
isStreamReady: s.isStreamReady,
|
||||||
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const disableAllButtons =
|
||||||
|
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||||
const newCommands = state.nextEvents
|
const newCommands = state.nextEvents
|
||||||
|
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
.map((type) =>
|
.map((type) =>
|
||||||
createMachineCommand<T, S>({
|
createMachineCommand<T, S>({
|
||||||
@ -64,5 +80,5 @@ export default function useStateMachineCommands<
|
|||||||
data: { commands: newCommands },
|
data: { commands: newCommands },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state, overallState, isExecuting, isStreamReady])
|
||||||
}
|
}
|
||||||
|
@ -239,8 +239,8 @@ class KclManager {
|
|||||||
const currentExecutionId = executionId || Date.now()
|
const currentExecutionId = executionId || Date.now()
|
||||||
this._cancelTokens.set(currentExecutionId, false)
|
this._cancelTokens.set(currentExecutionId, false)
|
||||||
|
|
||||||
await this.ensureWasmInit()
|
|
||||||
this.isExecuting = true
|
this.isExecuting = true
|
||||||
|
await this.ensureWasmInit()
|
||||||
const { logs, errors, programMemory } = await executeAst({
|
const { logs, errors, programMemory } = await executeAst({
|
||||||
ast,
|
ast,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { parse, recast, initPromise } from './wasm'
|
import { parse, recast, initPromise, Identifier } from './wasm'
|
||||||
import {
|
import {
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -90,7 +90,17 @@ describe('Testing createPipeExpression', () => {
|
|||||||
describe('Testing findUniqueName', () => {
|
describe('Testing findUniqueName', () => {
|
||||||
it('should find a unique name', () => {
|
it('should find a unique name', () => {
|
||||||
const result = findUniqueName(
|
const result = findUniqueName(
|
||||||
'yo01 yo02 yo03 yo04 yo05 yo06 yo07 yo08 yo09',
|
JSON.stringify([
|
||||||
|
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
|
||||||
|
] satisfies Identifier[]),
|
||||||
'yo',
|
'yo',
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
@ -162,18 +162,32 @@ export function findUniqueName(
|
|||||||
pad = 3,
|
pad = 3,
|
||||||
index = 1
|
index = 1
|
||||||
): string {
|
): string {
|
||||||
let searchStr = ''
|
let searchStr: string = typeof ast === 'string' ? ast : JSON.stringify(ast)
|
||||||
if (typeof ast === 'string') {
|
const indexStr = String(index).padStart(pad, '0')
|
||||||
searchStr = ast
|
|
||||||
} else {
|
const endingDigitsMatcher = /\d+$/
|
||||||
searchStr = JSON.stringify(ast)
|
const nameEndsInDigits = name.match(endingDigitsMatcher)
|
||||||
|
let nameIsInString = searchStr.includes(`:"${name}"`)
|
||||||
|
|
||||||
|
if (nameEndsInDigits !== null) {
|
||||||
|
// base case: name is unique and ends in digits
|
||||||
|
if (!nameIsInString) return name
|
||||||
|
|
||||||
|
// recursive case: name is not unique and ends in digits
|
||||||
|
const newPad = nameEndsInDigits[1].length
|
||||||
|
const newIndex = parseInt(nameEndsInDigits[1]) + 1
|
||||||
|
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
||||||
|
|
||||||
|
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
||||||
}
|
}
|
||||||
const indexStr = `${index}`.padStart(pad, '0')
|
|
||||||
const newName = `${name}${indexStr}`
|
const newName = `${name}${indexStr}`
|
||||||
const isInString = searchStr.includes(newName)
|
nameIsInString = searchStr.includes(`:"${newName}"`)
|
||||||
if (!isInString) {
|
|
||||||
return newName
|
// base case: name is unique and does not end in digits
|
||||||
}
|
if (!nameIsInString) return newName
|
||||||
|
|
||||||
|
// recursive case: name is not unique and does not end in digits
|
||||||
return findUniqueName(searchStr, name, pad, index + 1)
|
return findUniqueName(searchStr, name, pad, index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +287,7 @@ export function extrudeSketch(
|
|||||||
node: Program,
|
node: Program,
|
||||||
pathToNode: PathToNode,
|
pathToNode: PathToNode,
|
||||||
shouldPipe = true,
|
shouldPipe = true,
|
||||||
distance = 4
|
distance = createLiteral(4) as Value
|
||||||
): {
|
): {
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
@ -299,7 +313,7 @@ export function extrudeSketch(
|
|||||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||||
|
|
||||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
createLiteral(distance),
|
distance,
|
||||||
shouldPipe
|
shouldPipe
|
||||||
? createPipeSubstitution()
|
? createPipeSubstitution()
|
||||||
: {
|
: {
|
||||||
|
@ -996,9 +996,6 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEngineConnectionOpen: () => {
|
onEngineConnectionOpen: () => {
|
||||||
this.resolveReady()
|
|
||||||
setIsStreamReady(true)
|
|
||||||
|
|
||||||
// Make the axis gizmo.
|
// Make the axis gizmo.
|
||||||
// We do this after the connection opened to avoid a race condition.
|
// We do this after the connection opened to avoid a race condition.
|
||||||
// Connected opened is the last thing that happens when the stream
|
// Connected opened is the last thing that happens when the stream
|
||||||
@ -1017,9 +1014,11 @@ export class EngineCommandManager {
|
|||||||
gizmo_mode: true,
|
gizmo_mode: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
sceneInfra.onStreamStart()
|
sceneInfra.camControls.onCameraChange()
|
||||||
|
|
||||||
this.initPlanes().then(() => {
|
this.initPlanes().then(() => {
|
||||||
|
this.resolveReady()
|
||||||
|
setIsStreamReady(true)
|
||||||
executeCode(undefined, true)
|
executeCode(undefined, true)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -181,6 +181,10 @@ export const line: SketchLineHelper = {
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
'PipeExpression'
|
'PipeExpression'
|
||||||
)
|
)
|
||||||
|
const { node: callExpression } = getNodeFromPath<
|
||||||
|
PipeExpression | CallExpression
|
||||||
|
>(_node, pathToNode, 'CallExpression')
|
||||||
|
|
||||||
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||||
_node,
|
_node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -190,6 +194,38 @@ export const line: SketchLineHelper = {
|
|||||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||||
|
|
||||||
|
const isAddingSegmentBetween =
|
||||||
|
callExpression.type === 'CallExpression' &&
|
||||||
|
callExpression.start >= pipe.start &&
|
||||||
|
callExpression.end <= pipe.end
|
||||||
|
if (
|
||||||
|
isAddingSegmentBetween &&
|
||||||
|
!createCallback &&
|
||||||
|
pipe.type === 'PipeExpression'
|
||||||
|
) {
|
||||||
|
const callExp = createCallExpression('line', [
|
||||||
|
createArrayExpression([newXVal, newYVal]),
|
||||||
|
createPipeSubstitution(),
|
||||||
|
])
|
||||||
|
const pathToNodeIndex = pathToNode.findIndex(
|
||||||
|
(x) => x[1] === 'PipeExpression'
|
||||||
|
)
|
||||||
|
const pipeIndex = pathToNode[pathToNodeIndex + 1][0]
|
||||||
|
if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') {
|
||||||
|
console.warn('pipeIndex is undefined')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pipe.body = [
|
||||||
|
...pipe.body.slice(0, pipeIndex),
|
||||||
|
callExp,
|
||||||
|
...pipe.body.slice(pipeIndex),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst: _node,
|
||||||
|
pathToNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||||
const { callExp, valueUsedInTransform } = createCallback(
|
const { callExp, valueUsedInTransform } = createCallback(
|
||||||
@ -1011,15 +1047,6 @@ export function changeSketchArguments(
|
|||||||
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateLineFnCallArgs {
|
|
||||||
node: Program
|
|
||||||
programMemory: ProgramMemory
|
|
||||||
to: [number, number]
|
|
||||||
from: [number, number]
|
|
||||||
fnName: ToolTip
|
|
||||||
pathToNode: PathToNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compareVec2Epsilon(
|
export function compareVec2Epsilon(
|
||||||
vec1: [number, number],
|
vec1: [number, number],
|
||||||
vec2: [number, number],
|
vec2: [number, number],
|
||||||
@ -1044,6 +1071,15 @@ export function compareVec2Epsilon2(
|
|||||||
return distance < compareEpsilon
|
return distance < compareEpsilon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateLineFnCallArgs {
|
||||||
|
node: Program
|
||||||
|
programMemory: ProgramMemory
|
||||||
|
to: [number, number]
|
||||||
|
from: [number, number]
|
||||||
|
fnName: ToolTip
|
||||||
|
pathToNode: PathToNode
|
||||||
|
}
|
||||||
|
|
||||||
export function addNewSketchLn({
|
export function addNewSketchLn({
|
||||||
node: _node,
|
node: _node,
|
||||||
programMemory: previousProgramMemory,
|
programMemory: previousProgramMemory,
|
||||||
|
@ -39,30 +39,36 @@ export interface MouseGuard {
|
|||||||
rotate: MouseGuardHandler
|
rotate: MouseGuardHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const butName = (e: React.MouseEvent) => ({
|
||||||
|
middle: !!(e.buttons & 4),
|
||||||
|
right: !!(e.buttons & 2),
|
||||||
|
left: !!(e.buttons & 1),
|
||||||
|
})
|
||||||
|
|
||||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||||
KittyCAD: {
|
KittyCAD: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Right click + Shift + drag or middle click + drag',
|
description: 'Right click + Shift + drag or middle click + drag',
|
||||||
callback: (e) =>
|
callback: (e) =>
|
||||||
(e.button === 1 && noModifiersPressed(e)) ||
|
(butName(e).middle && noModifiersPressed(e)) ||
|
||||||
(e.button === 2 && e.shiftKey),
|
(butName(e).right && e.shiftKey),
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel or Right click + Ctrl + drag',
|
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||||
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey,
|
||||||
scrollCallback: () => true,
|
scrollCallback: () => true,
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Right click + drag',
|
description: 'Right click + drag',
|
||||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
callback: (e) => butName(e).right && noModifiersPressed(e),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OnShape: {
|
OnShape: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Right click + Ctrl + drag or middle click + drag',
|
description: 'Right click + Ctrl + drag or middle click + drag',
|
||||||
callback: (e) =>
|
callback: (e) =>
|
||||||
(e.button === 2 && e.ctrlKey) ||
|
(butName(e).right && e.ctrlKey) ||
|
||||||
(e.button === 1 && noModifiersPressed(e)),
|
(butName(e).middle && noModifiersPressed(e)),
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel',
|
description: 'Scroll wheel',
|
||||||
@ -71,77 +77,77 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
|||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Right click + drag',
|
description: 'Right click + drag',
|
||||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
callback: (e) => butName(e).right && noModifiersPressed(e),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'Trackpad Friendly': {
|
'Trackpad Friendly': {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Left click + Alt + Shift + drag or middle click + drag',
|
description: 'Left click + Alt + Shift + drag or middle click + drag',
|
||||||
callback: (e) =>
|
callback: (e) =>
|
||||||
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
|
(butName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
|
||||||
(e.button === 1 && noModifiersPressed(e)),
|
(butName(e).middle && noModifiersPressed(e)),
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel or Left click + Alt + OS + drag',
|
description: 'Scroll wheel or Left click + Alt + OS + drag',
|
||||||
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
|
dragCallback: (e) => butName(e).left && e.altKey && e.metaKey,
|
||||||
scrollCallback: () => true,
|
scrollCallback: () => true,
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Left click + Alt + drag',
|
description: 'Left click + Alt + drag',
|
||||||
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
|
callback: (e) => butName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
|
||||||
lenientDragStartButton: 0,
|
lenientDragStartButton: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Solidworks: {
|
Solidworks: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Right click + Ctrl + drag',
|
description: 'Right click + Ctrl + drag',
|
||||||
callback: (e) => e.button === 2 && e.ctrlKey,
|
callback: (e) => butName(e).right && e.ctrlKey,
|
||||||
lenientDragStartButton: 2,
|
lenientDragStartButton: 2,
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel or Middle click + Shift + drag',
|
description: 'Scroll wheel or Middle click + Shift + drag',
|
||||||
dragCallback: (e) => e.button === 1 && e.shiftKey,
|
dragCallback: (e) => butName(e).middle && e.shiftKey,
|
||||||
scrollCallback: () => true,
|
scrollCallback: () => true,
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Middle click + drag',
|
description: 'Middle click + drag',
|
||||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NX: {
|
NX: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Middle click + Shift + drag',
|
description: 'Middle click + Shift + drag',
|
||||||
callback: (e) => e.button === 1 && e.shiftKey,
|
callback: (e) => butName(e).middle && e.shiftKey,
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
dragCallback: (e) => butName(e).middle && e.ctrlKey,
|
||||||
scrollCallback: () => true,
|
scrollCallback: () => true,
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Middle click + drag',
|
description: 'Middle click + drag',
|
||||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Creo: {
|
Creo: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Middle click + Shift + drag',
|
description: 'Middle click + Shift + drag',
|
||||||
callback: (e) => e.button === 1 && e.shiftKey,
|
callback: (e) => butName(e).middle && e.shiftKey,
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
dragCallback: (e) => butName(e).middle && e.ctrlKey,
|
||||||
scrollCallback: () => true,
|
scrollCallback: () => true,
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Middle click + drag',
|
description: 'Middle click + drag',
|
||||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AutoCAD: {
|
AutoCAD: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Middle click + drag',
|
description: 'Middle click + drag',
|
||||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
description: 'Scroll wheel',
|
description: 'Scroll wheel',
|
||||||
@ -150,7 +156,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
|||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
description: 'Middle click + Shift + drag',
|
description: 'Middle click + Shift + drag',
|
||||||
callback: (e) => e.button === 1 && e.shiftKey,
|
callback: (e) => butName(e).middle && e.shiftKey,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CommandSetConfig } from 'lib/commandTypes'
|
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export type ModelingCommandSchema = {
|
|||||||
Extrude: {
|
Extrude: {
|
||||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
distance: number
|
distance: KclCommandValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +50,8 @@ export const modelingMachineConfig: CommandSetConfig<
|
|||||||
// })),
|
// })),
|
||||||
// },
|
// },
|
||||||
distance: {
|
distance: {
|
||||||
inputType: 'number',
|
inputType: 'kcl',
|
||||||
defaultValue: 5,
|
defaultValue: '5 + 7',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -7,10 +7,23 @@ import {
|
|||||||
InterpreterFrom,
|
InterpreterFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { Selection } from './selections'
|
import { Selection } from './selections'
|
||||||
|
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
||||||
|
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
|
const INPUT_TYPES = ['options', 'string', 'kcl', 'selection'] as const
|
||||||
|
export interface KclExpression {
|
||||||
|
valueAst: Value
|
||||||
|
valueText: string
|
||||||
|
valueCalculated: string
|
||||||
|
}
|
||||||
|
export interface KclExpressionWithVariable extends KclExpression {
|
||||||
|
variableName: string
|
||||||
|
variableDeclarationAst: VariableDeclaration
|
||||||
|
variableIdentifierAst: Identifier
|
||||||
|
insertIndex: number
|
||||||
|
}
|
||||||
|
export type KclCommandValue = KclExpression | KclExpressionWithVariable
|
||||||
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
||||||
|
|
||||||
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
||||||
@ -82,20 +95,24 @@ export type CommandArgumentConfig<
|
|||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required: boolean
|
||||||
skip?: true
|
skip?: true
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options:
|
options:
|
||||||
| CommandArgumentOption<OutputType>[]
|
| CommandArgumentOption<OutputType>[]
|
||||||
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
||||||
|
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
selectionTypes: Selection['type'][]
|
selectionTypes: Selection['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
}
|
}
|
||||||
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
|
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type CommandArgument<
|
export type CommandArgument<
|
||||||
@ -106,11 +123,11 @@ export type CommandArgument<
|
|||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required: boolean
|
||||||
skip?: true
|
skip?: true
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options: CommandArgumentOption<OutputType>[]
|
options: CommandArgumentOption<OutputType>[]
|
||||||
|
defaultValue?: OutputType
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
@ -118,7 +135,11 @@ export type CommandArgument<
|
|||||||
actor: InterpreterFrom<T>
|
actor: InterpreterFrom<T>
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
}
|
}
|
||||||
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
|
defaultValue?: OutputType
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type CommandArgumentWithName<
|
export type CommandArgumentWithName<
|
||||||
|
15
src/lib/commandUtils.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Some command argument payloads are objects with a value field that is a KCL expression.
|
||||||
|
// That object also contains some metadata about what to do with the KCL expression,
|
||||||
|
// such as whether we need to create a new variable for it.
|
||||||
|
// This function extracts the value field from those arg payloads and returns
|
||||||
|
// The arg object with all its field as natural values that the command to be executed will expect.
|
||||||
|
export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(args).map(([key, value]) => {
|
||||||
|
if (value !== null && typeof value === 'object' && 'value' in value) {
|
||||||
|
return [key, value.value]
|
||||||
|
}
|
||||||
|
return [key, value]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
@ -97,7 +97,7 @@ function buildCommandArguments<
|
|||||||
|
|
||||||
for (const arg in args) {
|
for (const arg in args) {
|
||||||
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||||
const newArg = buildCommandArgument(argConfig, state, actor)
|
const newArg = buildCommandArgument(argConfig, arg, state, actor)
|
||||||
newArgs[arg] = newArg
|
newArgs[arg] = newArg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +109,7 @@ function buildCommandArgument<
|
|||||||
T extends AnyStateMachine
|
T extends AnyStateMachine
|
||||||
>(
|
>(
|
||||||
arg: CommandArgumentConfig<O, T>,
|
arg: CommandArgumentConfig<O, T>,
|
||||||
|
argName: string,
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
actor?: InterpreterFrom<T>
|
actor?: InterpreterFrom<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
@ -116,10 +117,6 @@ function buildCommandArgument<
|
|||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
skip: arg.skip,
|
skip: arg.skip,
|
||||||
defaultValue:
|
|
||||||
arg.defaultValue instanceof Function
|
|
||||||
? arg.defaultValue(state.context)
|
|
||||||
: arg.defaultValue,
|
|
||||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
if (arg.inputType === 'options') {
|
if (arg.inputType === 'options') {
|
||||||
@ -136,6 +133,10 @@ function buildCommandArgument<
|
|||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
|
defaultValue:
|
||||||
|
arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(state.context)
|
||||||
|
: arg.defaultValue,
|
||||||
options,
|
options,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||||
} else if (arg.inputType === 'selection') {
|
} else if (arg.inputType === 'selection') {
|
||||||
@ -149,9 +150,19 @@ function buildCommandArgument<
|
|||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
actor,
|
actor,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
|
} else if (arg.inputType === 'kcl') {
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
defaultValue: arg.defaultValue,
|
||||||
|
...baseCommandArgument,
|
||||||
|
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
|
defaultValue:
|
||||||
|
arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(state.context)
|
||||||
|
: arg.defaultValue,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export function isTauri(): boolean {
|
export function isTauri(): boolean {
|
||||||
if (typeof window !== 'undefined') {
|
if (globalThis.window && typeof globalThis.window !== 'undefined') {
|
||||||
return '__TAURI__' in window
|
return '__TAURI__' in globalThis.window
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
126
src/lib/useCalculateKclExpression.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { findUniqueName } from 'lang/modifyAst'
|
||||||
|
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
||||||
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
|
import { Value, parse } from 'lang/wasm'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { executeAst } from 'useStore'
|
||||||
|
|
||||||
|
const isValidVariableName = (name: string) =>
|
||||||
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a value and a possible variablename,
|
||||||
|
* return helpers for calculating the value and inserting it into the code
|
||||||
|
* as well as information about the variables that are available
|
||||||
|
*/
|
||||||
|
export function useCalculateKclExpression({
|
||||||
|
value,
|
||||||
|
initialVariableName: valueName = '',
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
initialVariableName?: string
|
||||||
|
}): {
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>
|
||||||
|
valueNode: Value | null
|
||||||
|
calcResult: string
|
||||||
|
prevVariables: PrevVariable<unknown>[]
|
||||||
|
newVariableName: string
|
||||||
|
isNewVariableNameUnique: boolean
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
setNewVariableName: (a: string) => void
|
||||||
|
} {
|
||||||
|
const { programMemory } = useKclContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
|
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [availableVarInfo, setAvailableVarInfo] = useState<
|
||||||
|
ReturnType<typeof findAllPreviousVariables>
|
||||||
|
>({
|
||||||
|
variables: [],
|
||||||
|
insertIndex: 0,
|
||||||
|
bodyPath: [],
|
||||||
|
})
|
||||||
|
const [valueNode, setValueNode] = useState<Value | null>(null)
|
||||||
|
const [calcResult, setCalcResult] = useState('NAN')
|
||||||
|
const [newVariableName, setNewVariableName] = useState('')
|
||||||
|
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current && inputRef.current.focus()
|
||||||
|
inputRef.current &&
|
||||||
|
inputRef.current.setSelectionRange(0, String(value).length)
|
||||||
|
}, 100)
|
||||||
|
setNewVariableName(findUniqueName(kclManager.ast, valueName))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allVarNames = Object.keys(programMemory.root)
|
||||||
|
if (
|
||||||
|
allVarNames.includes(newVariableName) ||
|
||||||
|
newVariableName === '' ||
|
||||||
|
!isValidVariableName(newVariableName)
|
||||||
|
) {
|
||||||
|
setIsNewVariableNameUnique(false)
|
||||||
|
} else {
|
||||||
|
setIsNewVariableNameUnique(true)
|
||||||
|
}
|
||||||
|
}, [newVariableName])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!programMemory || !selectionRange) return
|
||||||
|
const varInfo = findAllPreviousVariables(
|
||||||
|
kclManager.ast,
|
||||||
|
kclManager.programMemory,
|
||||||
|
selectionRange
|
||||||
|
)
|
||||||
|
setAvailableVarInfo(varInfo)
|
||||||
|
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const execAstAndSetResult = async () => {
|
||||||
|
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({
|
||||||
|
ast,
|
||||||
|
engineCommandManager,
|
||||||
|
useFakeExecutor: true,
|
||||||
|
programMemoryOverride: JSON.parse(
|
||||||
|
JSON.stringify(kclManager.programMemory)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
const resultDeclaration = ast.body.find(
|
||||||
|
(a) =>
|
||||||
|
a.type === 'VariableDeclaration' &&
|
||||||
|
a.declarations?.[0]?.id?.name === '__result__'
|
||||||
|
)
|
||||||
|
const init =
|
||||||
|
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||||
|
resultDeclaration?.declarations?.[0]?.init
|
||||||
|
const result = programMemory?.root?.__result__?.value
|
||||||
|
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||||
|
init && setValueNode(init)
|
||||||
|
}
|
||||||
|
execAstAndSetResult().catch(() => {
|
||||||
|
setCalcResult('NAN')
|
||||||
|
setValueNode(null)
|
||||||
|
})
|
||||||
|
}, [value, availableVarInfo])
|
||||||
|
|
||||||
|
return {
|
||||||
|
valueNode,
|
||||||
|
calcResult,
|
||||||
|
prevVariables: availableVarInfo.variables,
|
||||||
|
newVariableInsertIndex: availableVarInfo.insertIndex,
|
||||||
|
newVariableName,
|
||||||
|
isNewVariableNameUnique,
|
||||||
|
setNewVariableName,
|
||||||
|
inputRef,
|
||||||
|
}
|
||||||
|
}
|
23
src/lib/usePreviousVarMentions.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { CompletionContext } from '@codemirror/autocomplete'
|
||||||
|
import { usePreviousVariables } from './usePreviousVariables'
|
||||||
|
|
||||||
|
/// Basically a fork of the `mentions` extension https://github.com/uiwjs/react-codemirror/blob/master/extensions/mentions/src/index.ts
|
||||||
|
/// But it matches on any word, not just the `@` symbol
|
||||||
|
export function usePreviousVarMentions(context: CompletionContext) {
|
||||||
|
const previousVariables = usePreviousVariables()
|
||||||
|
const data = previousVariables.variables.map((variable) => {
|
||||||
|
return {
|
||||||
|
label: variable.key,
|
||||||
|
detail: variable.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let word = context.matchBefore(/^\w*$/)
|
||||||
|
if (!word) return null
|
||||||
|
if (word && word.from === word.to && !context.explicit) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from: word?.from!,
|
||||||
|
options: [...data],
|
||||||
|
}
|
||||||
|
}
|
30
src/lib/usePreviousVariables.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { findAllPreviousVariables } from 'lang/queryAst'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function usePreviousVariables() {
|
||||||
|
const { programMemory, code } = useKclContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
|
const selectionRange = context.selectionRanges.codeBasedSelections[0]
|
||||||
|
?.range || [code.length, code.length]
|
||||||
|
const [previousVariablesInfo, setPreviousVariablesInfo] = useState<
|
||||||
|
ReturnType<typeof findAllPreviousVariables>
|
||||||
|
>({
|
||||||
|
variables: [],
|
||||||
|
insertIndex: 0,
|
||||||
|
bodyPath: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!programMemory || !selectionRange) return
|
||||||
|
const varInfo = findAllPreviousVariables(
|
||||||
|
kclManager.ast,
|
||||||
|
kclManager.programMemory,
|
||||||
|
selectionRange
|
||||||
|
)
|
||||||
|
setPreviousVariablesInfo(varInfo)
|
||||||
|
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||||
|
|
||||||
|
return previousVariablesInfo
|
||||||
|
}
|
28
src/lib/varCompletionExtension.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Extension } from '@codemirror/state'
|
||||||
|
import {
|
||||||
|
CompletionContext,
|
||||||
|
autocompletion,
|
||||||
|
Completion,
|
||||||
|
} from '@codemirror/autocomplete'
|
||||||
|
|
||||||
|
/// Basically a fork of the `mentions` extension https://github.com/uiwjs/react-codemirror/blob/master/extensions/mentions/src/index.ts
|
||||||
|
/// But it matches on any word, not just the `@` symbol
|
||||||
|
export function varMentions(data: Completion[] = []): Extension {
|
||||||
|
return autocompletion({
|
||||||
|
override: [
|
||||||
|
(context: CompletionContext) => {
|
||||||
|
let word = context.matchBefore(/(\w+)?/)
|
||||||
|
if (!word) return null
|
||||||
|
if (word && word.from === word.to && !context.explicit) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from: word?.from!,
|
||||||
|
options: [...data],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const varMentionsView: Extension = [varMentions()]
|
@ -3,12 +3,14 @@ import {
|
|||||||
Command,
|
Command,
|
||||||
CommandArgument,
|
CommandArgument,
|
||||||
CommandArgumentWithName,
|
CommandArgumentWithName,
|
||||||
|
KclCommandValue,
|
||||||
} from 'lib/commandTypes'
|
} from 'lib/commandTypes'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
|
|
||||||
export const commandBarMachine = createMachine(
|
export const commandBarMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSkWB6coHI16ByVBACBt8oHFAtHma0qR6iemlKsLGHppg61cJWDY+wzRqEDrGJs35IZ4AIV5eGswGabG9FJPqdRQo0NUqiBhmESjpFZrghcjYPiQB4bTEEU8NbImyFiaQ1hCw0M2Eja8mEVBmn5KCQOSVdL6SvvAISw87qzDPDKBQ-IdB8npkWH0FoJR8lcOKKeWgHAWNmslaxEcjKmXMmtSM1lbIqJHvuQi2x-KuPMPsQKb1HaBT6roWwqlNDBOYlYyWi1loX2ifEu6lQpDWlUNaHRsxUjSCFBsGY1TdB2i5BkBQgdhYmUqfbTI4JPr9lUHIdIxFCi6M-h6bBWcMz6xrjWbe7xG6Rj7pHAZ3lUgSLcD6NIApFj7jEVMrICkbA0VLjyHWd4vBAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSjHFPHQNFCIzk3jWRURJIAQNvlA4oFo8zWlSPUT00pVhYw9NMHWrhKwbH2GaNQgdYxNm-JDPAxCvLw1mAzTY3opJ9TqKFehqlUTMMwiUdIrNA5gMbB8IhQlh5bkWFsVwyJshYmkNYQs9DNhI2vJhFQZp+SgkDklXS+kr7wHUZA+G-UzwygUPyLB+xApFh9BaCU6hpSLGqNKDheC5yBlsfNCORlTLmTWpGaytl+G0wwhoEU7ilDWnMN4zGhRPqO0Cn1XQthVKaBsbNZK9iYlLUyhfBJKSR7OH2FYAi1oDGzFSNIIUGwZiVD0BKTCSkFCB2FiZRpIlMjgk+v2VQch0jEUKIYz+HoOSaA0IeJRO8m4OImXLTQjDYRpAFIsfckillZAUjYGipceQ6zvF4IAA */
|
||||||
context: {
|
context: {
|
||||||
commands: [] as Command[],
|
commands: [] as Command[],
|
||||||
selectedCommand: undefined as Command | undefined,
|
selectedCommand: undefined as Command | undefined,
|
||||||
@ -143,7 +145,7 @@ export const commandBarMachine = createMachine(
|
|||||||
'Change current argument': {
|
'Change current argument': {
|
||||||
target: 'Gathering arguments',
|
target: 'Gathering arguments',
|
||||||
internal: true,
|
internal: true,
|
||||||
actions: ['Set current argument'],
|
actions: ['Remove current argument and set a new one'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'Deselect command': {
|
'Deselect command': {
|
||||||
@ -172,17 +174,7 @@ export const commandBarMachine = createMachine(
|
|||||||
|
|
||||||
'Remove argument': {
|
'Remove argument': {
|
||||||
target: 'Review',
|
target: 'Review',
|
||||||
actions: [
|
actions: ['Remove argument'],
|
||||||
assign({
|
|
||||||
argumentsToSubmit: (context, event) => {
|
|
||||||
const argName = Object.keys(event.data)[0]
|
|
||||||
const { argumentsToSubmit } = context
|
|
||||||
const newArgumentsToSubmit = { ...argumentsToSubmit }
|
|
||||||
newArgumentsToSubmit[argName] = undefined
|
|
||||||
return newArgumentsToSubmit
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'Edit argument': {
|
'Edit argument': {
|
||||||
@ -272,7 +264,7 @@ export const commandBarMachine = createMachine(
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'Change current argument'
|
type: 'Change current argument'
|
||||||
data: { arg: CommandArgumentWithName<unknown> }
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
predictableActionArguments: true,
|
predictableActionArguments: true,
|
||||||
@ -283,19 +275,17 @@ export const commandBarMachine = createMachine(
|
|||||||
'Execute command': (context, event) => {
|
'Execute command': (context, event) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!selectedCommand) return
|
if (!selectedCommand) return
|
||||||
if (selectedCommand?.args) {
|
if (
|
||||||
selectedCommand?.onSubmit(
|
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||||
event.type === 'Submit command' ||
|
event.type === 'done.invoke.validateArguments'
|
||||||
event.type === 'done.invoke.validateArguments'
|
) {
|
||||||
? event.data
|
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data))
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
selectedCommand?.onSubmit()
|
selectedCommand?.onSubmit()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Set current argument to first non-skippable': assign({
|
'Set current argument to first non-skippable': assign({
|
||||||
currentArgument: (context, event) => {
|
currentArgument: (context) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||||
|
|
||||||
@ -331,6 +321,15 @@ export const commandBarMachine = createMachine(
|
|||||||
'Clear current argument': assign({
|
'Clear current argument': assign({
|
||||||
currentArgument: undefined,
|
currentArgument: undefined,
|
||||||
}),
|
}),
|
||||||
|
'Remove argument': assign({
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
if (event.type !== 'Remove argument') return context.argumentsToSubmit
|
||||||
|
const argToRemove = Object.values(event.data)[0]
|
||||||
|
// Extract all but the argument to remove and return it
|
||||||
|
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
}),
|
||||||
'Set current argument': assign({
|
'Set current argument': assign({
|
||||||
currentArgument: (context, event) => {
|
currentArgument: (context, event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@ -338,13 +337,34 @@ export const commandBarMachine = createMachine(
|
|||||||
return event.data.arg
|
return event.data.arg
|
||||||
case 'Edit argument':
|
case 'Edit argument':
|
||||||
return event.data.arg
|
return event.data.arg
|
||||||
case 'Change current argument':
|
|
||||||
return event.data.arg
|
|
||||||
default:
|
default:
|
||||||
return context.currentArgument
|
return context.currentArgument
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
'Remove current argument and set a new one': assign({
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
if (event.type !== 'Change current argument')
|
||||||
|
return context.currentArgument
|
||||||
|
return Object.values(event.data)[0]
|
||||||
|
},
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
if (
|
||||||
|
event.type !== 'Change current argument' ||
|
||||||
|
!context.currentArgument
|
||||||
|
)
|
||||||
|
return context.argumentsToSubmit
|
||||||
|
const { name, required } = context.currentArgument
|
||||||
|
if (required)
|
||||||
|
return {
|
||||||
|
[name]: undefined,
|
||||||
|
...context.argumentsToSubmit,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { [name]: _, ...rest } = context.argumentsToSubmit
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
}),
|
||||||
'Clear argument data': assign({
|
'Clear argument data': assign({
|
||||||
selectedCommand: undefined,
|
selectedCommand: undefined,
|
||||||
currentArgument: undefined,
|
currentArgument: undefined,
|
||||||
@ -378,7 +398,8 @@ export const commandBarMachine = createMachine(
|
|||||||
if (!command.args) return {}
|
if (!command.args) return {}
|
||||||
const args: { [x: string]: unknown } = {}
|
const args: { [x: string]: unknown } = {}
|
||||||
for (const [argName, arg] of Object.entries(command.args)) {
|
for (const [argName, arg] of Object.entries(command.args)) {
|
||||||
args[argName] = arg.skip ? arg.defaultValue : undefined
|
args[argName] =
|
||||||
|
arg.skip && 'defaultValue' in arg ? arg.defaultValue : undefined
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
},
|
},
|
||||||
@ -406,8 +427,12 @@ export const commandBarMachine = createMachine(
|
|||||||
let argConfig = context.selectedCommand!.args![argName]
|
let argConfig = context.selectedCommand!.args![argName]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(argConfig.defaultValue &&
|
('defaultValue' in argConfig &&
|
||||||
typeof arg !== typeof argConfig.defaultValue) ||
|
argConfig.defaultValue &&
|
||||||
|
typeof arg !== typeof argConfig.defaultValue &&
|
||||||
|
argConfig.inputType !== 'kcl') ||
|
||||||
|
(argConfig.inputType === 'kcl' &&
|
||||||
|
!(arg as Partial<KclCommandValue>).valueAst) ||
|
||||||
('options' in argConfig &&
|
('options' in argConfig &&
|
||||||
typeof arg !== typeof argConfig.options[0].value)
|
typeof arg !== typeof argConfig.options[0].value)
|
||||||
) {
|
) {
|
||||||
|
@ -732,15 +732,32 @@ export const modelingMachine = createMachine(
|
|||||||
'AST extrude': (_, event) => {
|
'AST extrude': (_, event) => {
|
||||||
if (!event.data) return
|
if (!event.data) return
|
||||||
const { selection, distance } = event.data
|
const { selection, distance } = event.data
|
||||||
|
let ast = kclManager.ast
|
||||||
|
if (
|
||||||
|
'variableName' in distance &&
|
||||||
|
distance.variableName &&
|
||||||
|
distance.insertIndex !== undefined
|
||||||
|
) {
|
||||||
|
console.log('adding variable!', distance)
|
||||||
|
const newBody = [...ast.body]
|
||||||
|
newBody.splice(
|
||||||
|
distance.insertIndex,
|
||||||
|
0,
|
||||||
|
distance.variableDeclarationAst
|
||||||
|
)
|
||||||
|
ast.body = newBody
|
||||||
|
}
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
kclManager.ast,
|
ast,
|
||||||
selection.codeBasedSelections[0].range
|
selection.codeBasedSelections[0].range
|
||||||
)
|
)
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
kclManager.ast,
|
ast,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
true,
|
true,
|
||||||
distance
|
'variableName' in distance
|
||||||
|
? distance.variableIdentifierAst
|
||||||
|
: distance.valueAst
|
||||||
)
|
)
|
||||||
// TODO not handling focusPath correctly I think
|
// TODO not handling focusPath correctly I think
|
||||||
kclManager.updateAst(modifiedAst, true, {
|
kclManager.updateAst(modifiedAst, true, {
|
||||||
|
12
src/wasm-lib/Cargo.lock
generated
@ -64,9 +64,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.5"
|
version = "0.8.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d"
|
checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
@ -937,7 +937,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"expectorate",
|
"expectorate",
|
||||||
@ -954,9 +954,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "834580a8bd697658876ed8c9f7727e49f01d34f5b859ca921ac5b99ffc6adf77"
|
checksum = "5d58c9464a20d3ece3260838ad7c2f504c925a852cfdad13f0ea2e9e4bb8a859"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -1904,7 +1904,7 @@ dependencies = [
|
|||||||
"criterion",
|
"criterion",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"databake",
|
"databake",
|
||||||
"derive-docs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"derive-docs 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"expectorate",
|
"expectorate",
|
||||||
"futures",
|
"futures",
|
||||||
"gltf-json",
|
"gltf-json",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -233,6 +233,7 @@ fn do_stdlib_inner(
|
|||||||
let ret_ty_string = if ret_ty_string.starts_with("Box <") {
|
let ret_ty_string = if ret_ty_string.starts_with("Box <") {
|
||||||
ret_ty_string
|
ret_ty_string
|
||||||
.trim_start_matches("Box <")
|
.trim_start_matches("Box <")
|
||||||
|
.trim_end_matches(' ')
|
||||||
.trim_end_matches('>')
|
.trim_end_matches('>')
|
||||||
.trim()
|
.trim()
|
||||||
.to_string()
|
.to_string()
|
||||||
|
@ -17,7 +17,7 @@ async-trait = "0.1.77"
|
|||||||
clap = { version = "4.5.0", features = ["cargo", "derive", "env", "unicode"], optional = true }
|
clap = { version = "4.5.0", features = ["cargo", "derive", "env", "unicode"], optional = true }
|
||||||
dashmap = "5.5.3"
|
dashmap = "5.5.3"
|
||||||
databake = { version = "0.1.7", features = ["derive"] }
|
databake = { version = "0.1.7", features = ["derive"] }
|
||||||
derive-docs = { version = "0.1.6" }
|
derive-docs = { version = "0.1.8" }
|
||||||
#derive-docs = { path = "../derive-docs" }
|
#derive-docs = { path = "../derive-docs" }
|
||||||
futures = { version = "0.3.30" }
|
futures = { version = "0.3.30" }
|
||||||
gltf-json = "1.4.0"
|
gltf-json = "1.4.0"
|
||||||
|
@ -240,6 +240,8 @@ pub struct Face {
|
|||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
/// The tag of the face.
|
/// The tag of the face.
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
/// The original sketch group id of the object we are sketching on.
|
||||||
|
pub sketch_group_id: uuid::Uuid,
|
||||||
/// What should the face’s X axis be?
|
/// What should the face’s X axis be?
|
||||||
pub x_axis: Point3d,
|
pub x_axis: Point3d,
|
||||||
/// What should the face’s Y axis be?
|
/// What should the face’s Y axis be?
|
||||||
@ -799,6 +801,11 @@ pub enum Path {
|
|||||||
/// arc's direction
|
/// arc's direction
|
||||||
ccw: bool,
|
ccw: bool,
|
||||||
},
|
},
|
||||||
|
/// A arc that is tangential to the last path segment
|
||||||
|
TangentialArc {
|
||||||
|
#[serde(flatten)]
|
||||||
|
base: BasePath,
|
||||||
|
},
|
||||||
/// A path that is horizontal.
|
/// A path that is horizontal.
|
||||||
Horizontal {
|
Horizontal {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -830,6 +837,7 @@ impl Path {
|
|||||||
Path::AngledLineTo { base, .. } => base.geo_meta.id,
|
Path::AngledLineTo { base, .. } => base.geo_meta.id,
|
||||||
Path::Base { base } => base.geo_meta.id,
|
Path::Base { base } => base.geo_meta.id,
|
||||||
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
|
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
|
||||||
|
Path::TangentialArc { base } => base.geo_meta.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -840,6 +848,7 @@ impl Path {
|
|||||||
Path::AngledLineTo { base, .. } => base.name.clone(),
|
Path::AngledLineTo { base, .. } => base.name.clone(),
|
||||||
Path::Base { base } => base.name.clone(),
|
Path::Base { base } => base.name.clone(),
|
||||||
Path::TangentialArcTo { base, .. } => base.name.clone(),
|
Path::TangentialArcTo { base, .. } => base.name.clone(),
|
||||||
|
Path::TangentialArc { base } => base.name.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,6 +859,7 @@ impl Path {
|
|||||||
Path::AngledLineTo { base, .. } => base,
|
Path::AngledLineTo { base, .. } => base,
|
||||||
Path::Base { base } => base,
|
Path::Base { base } => base,
|
||||||
Path::TangentialArcTo { base, .. } => base,
|
Path::TangentialArcTo { base, .. } => base,
|
||||||
|
Path::TangentialArc { base } => base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -860,6 +870,7 @@ impl Path {
|
|||||||
Path::AngledLineTo { base, .. } => Some(base),
|
Path::AngledLineTo { base, .. } => Some(base),
|
||||||
Path::Base { base } => Some(base),
|
Path::Base { base } => Some(base),
|
||||||
Path::TangentialArcTo { base, .. } => Some(base),
|
Path::TangentialArcTo { base, .. } => Some(base),
|
||||||
|
Path::TangentialArc { base } => Some(base),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -871,6 +882,7 @@ impl Path {
|
|||||||
pub enum ExtrudeSurface {
|
pub enum ExtrudeSurface {
|
||||||
/// An extrude plane.
|
/// An extrude plane.
|
||||||
ExtrudePlane(ExtrudePlane),
|
ExtrudePlane(ExtrudePlane),
|
||||||
|
ExtrudeArc(ExtrudeArc),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An extruded plane.
|
/// An extruded plane.
|
||||||
@ -891,28 +903,50 @@ pub struct ExtrudePlane {
|
|||||||
pub geo_meta: GeoMeta,
|
pub geo_meta: GeoMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An extruded arc.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ExtrudeArc {
|
||||||
|
/// The position.
|
||||||
|
pub position: Position,
|
||||||
|
/// The rotation.
|
||||||
|
pub rotation: Rotation,
|
||||||
|
/// The face id for the extrude plane.
|
||||||
|
pub face_id: uuid::Uuid,
|
||||||
|
/// The name.
|
||||||
|
pub name: String,
|
||||||
|
/// Metadata.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub geo_meta: GeoMeta,
|
||||||
|
}
|
||||||
|
|
||||||
impl ExtrudeSurface {
|
impl ExtrudeSurface {
|
||||||
pub fn get_id(&self) -> uuid::Uuid {
|
pub fn get_id(&self) -> uuid::Uuid {
|
||||||
match self {
|
match self {
|
||||||
ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
|
ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
|
||||||
|
ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_name(&self) -> String {
|
pub fn get_name(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
ExtrudeSurface::ExtrudePlane(ep) => ep.name.to_string(),
|
ExtrudeSurface::ExtrudePlane(ep) => ep.name.to_string(),
|
||||||
|
ExtrudeSurface::ExtrudeArc(ea) => ea.name.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_position(&self) -> Position {
|
pub fn get_position(&self) -> Position {
|
||||||
match self {
|
match self {
|
||||||
ExtrudeSurface::ExtrudePlane(ep) => ep.position,
|
ExtrudeSurface::ExtrudePlane(ep) => ep.position,
|
||||||
|
ExtrudeSurface::ExtrudeArc(ea) => ea.position,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rotation(&self) -> Rotation {
|
pub fn get_rotation(&self) -> Rotation {
|
||||||
match self {
|
match self {
|
||||||
ExtrudeSurface::ExtrudePlane(ep) => ep.rotation,
|
ExtrudeSurface::ExtrudePlane(ep) => ep.rotation,
|
||||||
|
ExtrudeSurface::ExtrudeArc(ea) => ea.rotation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,13 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut sketch_group = *sketch_group.clone();
|
||||||
|
|
||||||
|
// If we were sketching on a face, we need the original face id.
|
||||||
|
if let SketchSurface::Face(face) = sketch_group.on {
|
||||||
|
sketch_group.id = face.sketch_group_id;
|
||||||
|
}
|
||||||
|
|
||||||
let solid3d_info = args
|
let solid3d_info = args
|
||||||
.send_modeling_cmd(
|
.send_modeling_cmd(
|
||||||
id,
|
id,
|
||||||
@ -115,17 +122,34 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
|
|||||||
let mut new_value: Vec<ExtrudeSurface> = Vec::new();
|
let mut new_value: Vec<ExtrudeSurface> = Vec::new();
|
||||||
for path in sketch_group.value.iter() {
|
for path in sketch_group.value.iter() {
|
||||||
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
|
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
|
||||||
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
|
match path {
|
||||||
position: sketch_group.position, // TODO should be for the extrude surface
|
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => {
|
||||||
rotation: sketch_group.rotation, // TODO should be for the extrude surface
|
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
|
||||||
face_id: *actual_face_id,
|
position: sketch_group.position, // TODO should be for the extrude surface
|
||||||
name: path.get_base().name.clone(),
|
rotation: sketch_group.rotation, // TODO should be for the extrude surface
|
||||||
geo_meta: GeoMeta {
|
face_id: *actual_face_id,
|
||||||
id: path.get_base().geo_meta.id,
|
name: path.get_base().name.clone(),
|
||||||
metadata: path.get_base().geo_meta.metadata.clone(),
|
geo_meta: GeoMeta {
|
||||||
},
|
id: path.get_base().geo_meta.id,
|
||||||
});
|
metadata: path.get_base().geo_meta.metadata.clone(),
|
||||||
new_value.push(extrude_surface);
|
},
|
||||||
|
});
|
||||||
|
new_value.push(extrude_surface);
|
||||||
|
}
|
||||||
|
Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
|
||||||
|
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
|
||||||
|
position: sketch_group.position, // TODO should be for the extrude surface
|
||||||
|
rotation: sketch_group.rotation, // TODO should be for the extrude surface
|
||||||
|
face_id: *actual_face_id,
|
||||||
|
name: path.get_base().name.clone(),
|
||||||
|
geo_meta: GeoMeta {
|
||||||
|
id: path.get_base().geo_meta.id,
|
||||||
|
metadata: path.get_base().geo_meta.metadata.clone(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
new_value.push(extrude_surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -849,15 +849,23 @@ async fn start_sketch_on_face(
|
|||||||
.value
|
.value
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|extrude_surface| match extrude_surface {
|
.find_map(|extrude_surface| match extrude_surface {
|
||||||
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => Some(extrude_plane.face_id),
|
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => {
|
||||||
ExtrudeSurface::ExtrudePlane(_) => None,
|
Some(Ok(extrude_plane.face_id))
|
||||||
|
}
|
||||||
|
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == *s => {
|
||||||
|
Some(Err(KclError::Type(KclErrorDetails {
|
||||||
|
message: format!("Cannot sketch on a non-planar surface: `{}`", tag),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails {
|
||||||
message: format!("Expected a face with the tag `{}`", tag),
|
message: format!("Expected a face with the tag `{}`", tag),
|
||||||
source_ranges: vec![args.source_range],
|
source_ranges: vec![args.source_range],
|
||||||
})
|
})
|
||||||
})?,
|
})??,
|
||||||
SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
|
SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails {
|
||||||
message: "Expected a start face to sketch on".to_string(),
|
message: "Expected a start face to sketch on".to_string(),
|
||||||
@ -888,6 +896,7 @@ async fn start_sketch_on_face(
|
|||||||
Ok(Box::new(Face {
|
Ok(Box::new(Face {
|
||||||
id,
|
id,
|
||||||
value: tag.to_string(),
|
value: tag.to_string(),
|
||||||
|
sketch_group_id: extrude_group.id,
|
||||||
// TODO: get this from the extrude plane data.
|
// TODO: get this from the extrude plane data.
|
||||||
x_axis: extrude_group.x_axis,
|
x_axis: extrude_group.x_axis,
|
||||||
y_axis: extrude_group.y_axis,
|
y_axis: extrude_group.y_axis,
|
||||||
@ -1328,11 +1337,14 @@ async fn inner_tangential_arc(
|
|||||||
|
|
||||||
let to = [from.x + to[0], from.y + to[1]];
|
let to = [from.x + to[0], from.y + to[1]];
|
||||||
|
|
||||||
let current_path = Path::ToPoint {
|
let current_path = Path::TangentialArc {
|
||||||
base: BasePath {
|
base: BasePath {
|
||||||
from: from.into(),
|
from: from.into(),
|
||||||
to,
|
to,
|
||||||
name: "".to_string(),
|
name: match data {
|
||||||
|
TangentialArcData::PointWithTag { tag, .. } => tag.to_string(),
|
||||||
|
TangentialArcData::Point(_) | TangentialArcData::RadiusAndOffset { .. } => "".to_string(),
|
||||||
|
},
|
||||||
geo_meta: GeoMeta {
|
geo_meta: GeoMeta {
|
||||||
id,
|
id,
|
||||||
metadata: args.source_range.into(),
|
metadata: args.source_range.into(),
|
||||||
|
@ -169,6 +169,40 @@ const part002 = startSketchOn(part001, "END")
|
|||||||
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999);
|
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn serial_test_sketch_on_face_end_negative_extrude() {
|
||||||
|
let code = r#"fn cube = (pos, scale) => {
|
||||||
|
const sg = startSketchOn('XY')
|
||||||
|
|> startProfileAt(pos, %)
|
||||||
|
|> line([0, scale], %)
|
||||||
|
|> line([scale, 0], %)
|
||||||
|
|> line([0, -scale], %)
|
||||||
|
|
||||||
|
return sg
|
||||||
|
}
|
||||||
|
const part001 = cube([0,0], 20)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(20, %)
|
||||||
|
|
||||||
|
const part002 = startSketchOn(part001, "END")
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([0, 10], %)
|
||||||
|
|> line([10, 0], %)
|
||||||
|
|> line([0, -10], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(-5, %)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
twenty_twenty::assert_image(
|
||||||
|
"tests/executor/outputs/sketch_on_face_end_negative_extrude.png",
|
||||||
|
&result,
|
||||||
|
0.999,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_execute_with_function_sketch() {
|
async fn serial_test_execute_with_function_sketch() {
|
||||||
let code = r#"fn box = (h, l, w) => {
|
let code = r#"fn box = (h, l, w) => {
|
||||||
@ -1138,3 +1172,74 @@ const myCube = cube([0,0], 10)
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, 1.0);
|
twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn serial_test_error_sketch_on_arc_face() {
|
||||||
|
let code = r#"fn cube = (pos, scale) => {
|
||||||
|
const sg = startSketchOn('XY')
|
||||||
|
|> startProfileAt(pos, %)
|
||||||
|
|> tangentialArc({ to: [0, scale], tag: "here" }, %)
|
||||||
|
|> line([scale, 0], %)
|
||||||
|
|> line([0, -scale], %)
|
||||||
|
|
||||||
|
return sg
|
||||||
|
}
|
||||||
|
const part001 = cube([0, 0], 20)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(20, %)
|
||||||
|
|
||||||
|
const part002 = startSketchOn(part001, "here")
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([5, 0], %)
|
||||||
|
|> line([5, 5], %)
|
||||||
|
|> line([0, 5], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(1, %)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
result.err().unwrap().to_string(),
|
||||||
|
r#"type: KclErrorDetails { source_ranges: [SourceRange([294, 324])], message: "Cannot sketch on a non-planar surface: `here`" }"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn serial_test_sketch_on_face_of_face() {
|
||||||
|
let code = r#"fn cube = (pos, scale) => {
|
||||||
|
const sg = startSketchOn('XY')
|
||||||
|
|> startProfileAt(pos, %)
|
||||||
|
|> line([0, scale], %)
|
||||||
|
|> line([scale, 0], %)
|
||||||
|
|> line([0, -scale], %)
|
||||||
|
|
||||||
|
return sg
|
||||||
|
}
|
||||||
|
const part001 = cube([0,0], 20)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(20, %)
|
||||||
|
|
||||||
|
const part002 = startSketchOn(part001, "end")
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([0, 10], %)
|
||||||
|
|> line([10, 0], %)
|
||||||
|
|> line([0, -10], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(5, %)
|
||||||
|
|
||||||
|
const part003 = startSketchOn(part002, "end")
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([0, 5], %)
|
||||||
|
|> line([5, 0], %)
|
||||||
|
|> line([0, -5], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(5, %)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_face.png", &result, 1.0);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 109 KiB |
BIN
src/wasm-lib/tests/executor/outputs/sketch_on_face_of_face.png
Normal file
After Width: | Height: | Size: 111 KiB |
124
yarn.lock
@ -2943,11 +2943,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@xstate/machine-extractor" "^0.16.0"
|
"@xstate/machine-extractor" "^0.16.0"
|
||||||
|
|
||||||
"@yarnpkg/lockfile@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
|
||||||
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
|
|
||||||
|
|
||||||
acorn-jsx@^5.3.2:
|
acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||||
@ -3243,11 +3238,6 @@ asynckit@^0.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
at-least-node@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
|
||||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
|
||||||
|
|
||||||
autoprefixer@^10.4.13:
|
autoprefixer@^10.4.13:
|
||||||
version "10.4.14"
|
version "10.4.14"
|
||||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
|
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
|
||||||
@ -3670,7 +3660,7 @@ chromium-bidi@0.4.16:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mitt "3.0.0"
|
mitt "3.0.0"
|
||||||
|
|
||||||
ci-info@^3.2.0, ci-info@^3.7.0:
|
ci-info@^3.2.0:
|
||||||
version "3.9.0"
|
version "3.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
|
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
|
||||||
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
|
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
|
||||||
@ -4890,13 +4880,6 @@ find-up@^6.3.0:
|
|||||||
locate-path "^7.1.0"
|
locate-path "^7.1.0"
|
||||||
path-exists "^5.0.0"
|
path-exists "^5.0.0"
|
||||||
|
|
||||||
find-yarn-workspace-root@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
|
|
||||||
integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
|
|
||||||
dependencies:
|
|
||||||
micromatch "^4.0.2"
|
|
||||||
|
|
||||||
flat-cache@^3.0.4:
|
flat-cache@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
||||||
@ -4991,16 +4974,6 @@ fs-extra@^8.1.0:
|
|||||||
jsonfile "^4.0.0"
|
jsonfile "^4.0.0"
|
||||||
universalify "^0.1.0"
|
universalify "^0.1.0"
|
||||||
|
|
||||||
fs-extra@^9.0.0:
|
|
||||||
version "9.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
|
||||||
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
|
||||||
dependencies:
|
|
||||||
at-least-node "^1.0.0"
|
|
||||||
graceful-fs "^4.2.0"
|
|
||||||
jsonfile "^6.0.1"
|
|
||||||
universalify "^2.0.0"
|
|
||||||
|
|
||||||
fs-minipass@^2.0.0:
|
fs-minipass@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||||
@ -5341,7 +5314,7 @@ got@^13.0.0:
|
|||||||
p-cancelable "^3.0.0"
|
p-cancelable "^3.0.0"
|
||||||
responselike "^3.0.0"
|
responselike "^3.0.0"
|
||||||
|
|
||||||
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.9:
|
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.9:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
@ -5703,11 +5676,6 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag "^1.0.0"
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
is-docker@^2.0.0:
|
|
||||||
version "2.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
|
||||||
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
|
||||||
|
|
||||||
is-extglob@^2.1.1:
|
is-extglob@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||||
@ -5870,13 +5838,6 @@ is-weakset@^2.0.1:
|
|||||||
call-bind "^1.0.2"
|
call-bind "^1.0.2"
|
||||||
get-intrinsic "^1.1.1"
|
get-intrinsic "^1.1.1"
|
||||||
|
|
||||||
is-wsl@^2.1.1:
|
|
||||||
version "2.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
|
||||||
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
|
||||||
dependencies:
|
|
||||||
is-docker "^2.0.0"
|
|
||||||
|
|
||||||
isarray@^2.0.5:
|
isarray@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||||
@ -6106,16 +6067,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||||
|
|
||||||
json-stable-stringify@^1.0.2:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454"
|
|
||||||
integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==
|
|
||||||
dependencies:
|
|
||||||
call-bind "^1.0.5"
|
|
||||||
isarray "^2.0.5"
|
|
||||||
jsonify "^0.0.1"
|
|
||||||
object-keys "^1.1.1"
|
|
||||||
|
|
||||||
json5@^1.0.2:
|
json5@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||||
@ -6140,20 +6091,6 @@ jsonfile@^4.0.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs "^4.1.6"
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
jsonfile@^6.0.1:
|
|
||||||
version "6.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
|
||||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
|
||||||
dependencies:
|
|
||||||
universalify "^2.0.0"
|
|
||||||
optionalDependencies:
|
|
||||||
graceful-fs "^4.1.6"
|
|
||||||
|
|
||||||
jsonify@^0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
|
|
||||||
integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
|
|
||||||
|
|
||||||
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
|
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
|
||||||
version "3.3.5"
|
version "3.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
|
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
|
||||||
@ -6171,13 +6108,6 @@ keyv@^4.5.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
json-buffer "3.0.1"
|
json-buffer "3.0.1"
|
||||||
|
|
||||||
klaw-sync@^6.0.0:
|
|
||||||
version "6.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
|
|
||||||
integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
|
|
||||||
dependencies:
|
|
||||||
graceful-fs "^4.1.11"
|
|
||||||
|
|
||||||
ky@^0.33.0:
|
ky@^0.33.0:
|
||||||
version "0.33.3"
|
version "0.33.3"
|
||||||
resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543"
|
resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543"
|
||||||
@ -6421,7 +6351,7 @@ meshoptimizer@~0.18.1:
|
|||||||
resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8"
|
resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8"
|
||||||
integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==
|
integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==
|
||||||
|
|
||||||
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
micromatch@^4.0.4, micromatch@^4.0.5:
|
||||||
version "4.0.5"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||||
@ -6850,14 +6780,6 @@ onetime@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn "^4.0.0"
|
mimic-fn "^4.0.0"
|
||||||
|
|
||||||
open@^7.4.2:
|
|
||||||
version "7.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
|
||||||
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
|
||||||
dependencies:
|
|
||||||
is-docker "^2.0.0"
|
|
||||||
is-wsl "^2.1.1"
|
|
||||||
|
|
||||||
openapi-types@^12.0.0:
|
openapi-types@^12.0.0:
|
||||||
version "12.1.3"
|
version "12.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
|
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
|
||||||
@ -7003,27 +6925,6 @@ parse-ms@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
||||||
integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
|
integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
|
||||||
|
|
||||||
patch-package@^8.0.0:
|
|
||||||
version "8.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
|
|
||||||
integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
|
|
||||||
dependencies:
|
|
||||||
"@yarnpkg/lockfile" "^1.1.0"
|
|
||||||
chalk "^4.1.2"
|
|
||||||
ci-info "^3.7.0"
|
|
||||||
cross-spawn "^7.0.3"
|
|
||||||
find-yarn-workspace-root "^2.0.0"
|
|
||||||
fs-extra "^9.0.0"
|
|
||||||
json-stable-stringify "^1.0.2"
|
|
||||||
klaw-sync "^6.0.0"
|
|
||||||
minimist "^1.2.6"
|
|
||||||
open "^7.4.2"
|
|
||||||
rimraf "^2.6.3"
|
|
||||||
semver "^7.5.3"
|
|
||||||
slash "^2.0.0"
|
|
||||||
tmp "^0.0.33"
|
|
||||||
yaml "^2.2.2"
|
|
||||||
|
|
||||||
path-exists@^4.0.0:
|
path-exists@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||||
@ -7721,7 +7622,7 @@ rgb2hex@0.2.5:
|
|||||||
resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.5.tgz#f82230cd3ab1364fa73c99be3a691ed688f8dbdc"
|
resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.5.tgz#f82230cd3ab1364fa73c99be3a691ed688f8dbdc"
|
||||||
integrity sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==
|
integrity sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==
|
||||||
|
|
||||||
rimraf@2, rimraf@^2.6.3:
|
rimraf@2:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
@ -7839,7 +7740,7 @@ semver@^6.3.0, semver@^6.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
semver@^7.3.5, semver@^7.3.7, semver@^7.5.3:
|
semver@^7.3.5, semver@^7.3.7:
|
||||||
version "7.5.4"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||||
@ -7917,11 +7818,6 @@ sketch-helpers@^0.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/sketch-helpers/-/sketch-helpers-0.0.4.tgz#c6e4257451cd65483ab99ff7d3b10da04e98374d"
|
resolved "https://registry.yarnpkg.com/sketch-helpers/-/sketch-helpers-0.0.4.tgz#c6e4257451cd65483ab99ff7d3b10da04e98374d"
|
||||||
integrity sha512-xSt+Ku4VFDk4fBW3kRj+raZ49fFSJ32q1ph05GKQvZ9mIUI+W2/3iJJSBfBWwIdxlNiMx6RoUe2O+5vwtkPT3A==
|
integrity sha512-xSt+Ku4VFDk4fBW3kRj+raZ49fFSJ32q1ph05GKQvZ9mIUI+W2/3iJJSBfBWwIdxlNiMx6RoUe2O+5vwtkPT3A==
|
||||||
|
|
||||||
slash@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
|
||||||
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
|
|
||||||
|
|
||||||
slash@^3.0.0:
|
slash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||||
@ -8587,11 +8483,6 @@ universalify@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
universalify@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
|
||||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
|
||||||
|
|
||||||
unzipper@^0.10.14:
|
unzipper@^0.10.14:
|
||||||
version "0.10.14"
|
version "0.10.14"
|
||||||
resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1"
|
resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1"
|
||||||
@ -9084,11 +8975,6 @@ yaml@^2.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||||
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
|
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
|
||||||
|
|
||||||
yaml@^2.2.2:
|
|
||||||
version "2.3.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
|
|
||||||
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
|
|
||||||
|
|
||||||
yargs-parser@20.2.4:
|
yargs-parser@20.2.4:
|
||||||
version "20.2.4"
|
version "20.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
|
||||||
|