Compare commits

..

1 Commits

Author SHA1 Message Date
2458c9f6fe Test a parser bug 2024-03-27 16:05:51 -05:00
282 changed files with 2674 additions and 5265 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -51191,7 +51191,6 @@
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y' }, %) // default angle is 360", "const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y' }, %) // default angle is 360",
"// A donut shape.\nconst sketch001 = startSketchOn('XY')\n |> circle([15, 0], 5, %)\n |> revolve({ angle: 360, axis: 'y' }, %)", "// A donut shape.\nconst sketch001 = startSketchOn('XY')\n |> circle([15, 0], 5, %)\n |> revolve({ angle: 360, axis: 'y' }, %)",
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)", "const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)",
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)\nconst part002 = startSketchOn(part001, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n |> extrude(5, %)",
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %)\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({ angle: -90, axis: 'y' }, %)", "const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %)\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({ angle: -90, axis: 'y' }, %)",
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)" "const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)"
] ]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -1,11 +1,10 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme'
import { initialSettings } from '../../src/lib/settings/initialSettings'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { basicStorageState } from './storageStates'
import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'
/* /*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
@ -31,14 +30,31 @@ test.beforeEach(async ({ context, page }) => {
resources: ['tcp:3000'], resources: ['tcp:3000'],
timeout: 5000, timeout: 5000,
}) })
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
}, secrets.token)
// kill animations, speeds up tests and reduced flakiness // kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.setTimeout(60000) test.setTimeout(60000)
test('Basic sketch', async ({ page, context }) => { test('Basic sketch', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -513,119 +529,83 @@ test('Auto complete works', async ({ page }) => {
}) })
// Stored settings validation test // Stored settings validation test
test.describe('Settings persistence and validation tests', () => {
// Override test setup
// with corrupted settings
const storageState = structuredClone(basicStorageState)
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
settings: SaveSettingsPayload
}
s.settings.app.theme = Themes.Dark
s.settings.app.projectDirectory = 123 as any
s.settings.modeling.defaultUnit = 'invalid' as any
s.settings.modeling.mouseControls = `() => alert('hack the planet')` as any
s.settings.projects.defaultProjectName = false as any
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
test.use({ storageState })
test('Stored settings are validated and fall back to defaults', async ({ test('Stored settings are validated and fall back to defaults', async ({
page, page,
context,
}) => { }) => {
const u = getUtils(page) // Override beforeEach test setup
// with corrupted settings
await context.addInitScript(async () => {
const storedSettings = JSON.parse(
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
// Corrupt the settings
storedSettings.baseUnit = 'invalid'
storedSettings.cameraControls = `() => alert('hack the planet')`
storedSettings.defaultDirectory = 123
storedSettings.defaultProjectName = false
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
})
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/', { waitUntil: 'domcontentloaded' })
await u.waitForAuthSkipAppStart()
// Check the toast appeared
await expect(
page.getByText(`Error validating persisted settings:`, {
exact: false,
})
).toBeVisible()
// Check the settings were reset // Check the settings were reset
const storedSettings = TOML.parse( const storedSettings = JSON.parse(
await page.evaluate(() => localStorage.getItem('/user.toml') || '{}') await page.evaluate(
) as { settings: SaveSettingsPayload } () => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
expect(storedSettings.settings.app?.theme).toBe('dark') )
await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit)
// Check that the invalid settings were removed await expect(storedSettings.cameraControls).toBe(
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined) initialSettings.cameraControls
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined) )
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined) await expect(storedSettings.defaultDirectory).toBe(
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined) initialSettings.defaultDirectory
}) )
await expect(storedSettings.defaultProjectName).toBe(
test('Project settings can be set and override user settings', async ({ initialSettings.defaultProjectName
page,
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
) )
.hover()
await page
.getByRole('button', {
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
}) })
// Onboarding tests // Onboarding tests
test.describe('Onboarding tests', () => {
// Override test setup
const storageState = structuredClone(basicStorageState)
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
settings: SaveSettingsPayload
}
s.settings.app.onboardingStatus = '/export'
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
test.use({ storageState })
test('Onboarding redirects and code updating', async ({ page, context }) => { test('Onboarding redirects and code updating', async ({ page, context }) => {
const u = getUtils(page) const u = getUtils(page)
// Override beforeEach test setup
await context.addInitScript(async () => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
const storedSettings = JSON.parse(
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
storedSettings.onboardingStatus = '/export'
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
})
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Test that the redirect happened // Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe( await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export` `/file/new/onboarding/export`
) )
// Test that you come back to this page when you refresh // Test that you come back to this page when you refresh
await page.reload() await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe( await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export` `/file/new/onboarding/export`
) )
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
@ -640,7 +620,6 @@ test.describe('Onboarding tests', () => {
await page.locator('[data-testid="onboarding-next"]').click() await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/) await expect(page.locator('.cm-content')).toHaveText(/.+/)
}) })
})
test('Selections work on fresh and edited sketch', async ({ page }) => { test('Selections work on fresh and edited sketch', async ({ page }) => {
// tests mapping works on fresh sketch and edited sketch // tests mapping works on fresh sketch and edited sketch
@ -800,11 +779,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await selectionSequence() await selectionSequence()
}) })
test.describe('Command bar tests', () => {
test('Command bar works and can change a setting', async ({ page }) => { test('Command bar works and can change a setting', async ({ page }) => {
// Brief boilerplate // Brief boilerplate
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await page.goto('/')
await u.waitForAuthSkipAppStart()
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
@ -825,35 +805,33 @@ test.describe('Command bar tests', () => {
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await page.keyboard.type('theme')
const themeOption = page.getByRole('option', { const themeOption = page.getByRole('option', { name: 'Set Theme' })
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible() await expect(themeOption).toBeVisible()
await themeOption.click() await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option') const themeInput = page.getByPlaceholder('system')
await expect(themeInput).toBeVisible() await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused() await expect(themeInput).toBeFocused()
// Select dark theme // Select dark theme
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowDown') await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state', 'data-headlessui-state',
'active' 'active'
) )
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Check the toast appeared // Check the toast appeared
await expect( await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible()
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed // Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
}) })
// Override test setup code test('Can extrude from the command bar', async ({ page, context }) => {
const storageState = structuredClone(basicStorageState) await context.addInitScript(async (token) => {
storageState.origins[0].localStorage[1].value = `const distance = sqrt(20) localStorage.setItem(
'persistCode',
`
const distance = sqrt(20)
const part001 = startSketchOn('-XZ') const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %) |> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
@ -861,26 +839,15 @@ test.describe('Command bar tests', () => {
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)
` `
test.use({ storageState }) )
})
test('Can extrude from the command bar', 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 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Make sure the stream is up
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await expect(
page.getByRole('button', { name: 'Extrude' })
).not.toBeDisabled()
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
@ -888,6 +855,14 @@ test.describe('Command bar tests', () => {
// Search for extrude command and choose it // Search for extrude command and choose it
await page.getByRole('option', { name: 'Extrude' }).click() await page.getByRole('option', { name: 'Extrude' }).click()
await expect(page.locator('#arg-form > label')).toContainText(
'Please select one face'
)
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
// Click to select face and set distance
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await page.getByRole('button', { name: 'Continue' }).click()
// Assert that we're on the distance step // Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
@ -928,7 +903,6 @@ const part001 = startSketchOn('-XZ')
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
) )
}) })
})
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
@ -1496,13 +1470,9 @@ test('Sketch on face', async ({ page, context }) => {
await page.getByText('startProfileAt([1.03, 1.03], %)').click() await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel()
await page.waitForTimeout(200) await page.waitForTimeout(200)
const pointToDragFirst = [787, 565] const pointToDragFirst = [691, 237]
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1]) await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
await page.mouse.down() await page.mouse.down()
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], { await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
@ -1516,9 +1486,7 @@ test('Sketch on face', async ({ page, context }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${ |> line([2.81, -0.33], %)
process?.env?.CI ? 0.24 : 0.2
}], %)
|> line([-4.44, -2.13], %) |> line([-4.44, -2.13], %)
|> close(%)`) |> close(%)`)
@ -1533,7 +1501,6 @@ test('Sketch on face', async ({ page, context }) => {
await page.getByRole('button', { name: 'Extrude' }).click() await page.getByRole('button', { name: 'Extrude' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible() await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible() await expect(page.getByText('Confirm Extrude')).toBeVisible()
@ -1542,9 +1509,7 @@ test('Sketch on face', async ({ page, context }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${ |> line([2.81, -0.33], %)
process?.env?.CI ? 0.24 : 0.2
}], %)
|> line([-4.44, -2.13], %) |> line([-4.44, -2.13], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)`) |> extrude(5 + 7, %)`)

View File

@ -7,18 +7,30 @@ import { spawn } from 'child_process'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import JSZip from 'jszip' import JSZip from 'jszip'
import path from 'path' import path from 'path'
import { basicSettings, basicStorageState } from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ context, page }) => {
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
}, secrets.token)
// reducedMotion kills animations, which speeds up tests and reduces flakiness // reducedMotion kills animations, which speeds up tests and reduces flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.use({
storageState: structuredClone(basicStorageState),
})
test.setTimeout(60_000) test.setTimeout(60_000)
test('exports of each format should work', async ({ page, context }) => { test('exports of each format should work', async ({ page, context }) => {
@ -320,22 +332,6 @@ test('extrude on each default plane should be stable', async ({
page, page,
context, context,
}) => { }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %) |> startProfileAt([7.00, 4.40], %)
@ -357,26 +353,29 @@ test('extrude on each default plane should be stable', async ({
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
await page.getByText('Code').click()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.getByText('Code').click()
const runSnapshotsForOtherPlanes = async (plane = 'XY') => { const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
// clear code // clear code
await u.removeCurrentCode() await u.removeCurrentCode()
// add makeCode('XZ') // add makeCode('XZ')
await u.openAndClearDebugPanel()
await page.locator('.cm-content').fill(makeCode(plane)) await page.locator('.cm-content').fill(makeCode(plane))
// wait for execution done // wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.getByText('Code').click() await page.getByText('Code').click()
await page.waitForTimeout(150)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
await page.getByText('Code').click() await page.getByText('Code').click()
} }
await runSnapshotsForOtherPlanes('XY')
await runSnapshotsForOtherPlanes('-XY') await runSnapshotsForOtherPlanes('-XY')
await runSnapshotsForOtherPlanes('XZ') await runSnapshotsForOtherPlanes('XZ')
@ -387,6 +386,22 @@ test('extrude on each default plane should be stable', async ({
}) })
test('Draft segments should look right', async ({ page, context }) => { test('Draft segments should look right', async ({ page, context }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -445,9 +460,26 @@ test('Draft segments should look right', async ({ page, context }) => {
}) })
}) })
test('Client side scene scale should match engine scale - Inch', async ({ test('Client side scene scale should match engine scale inch', async ({
page, page,
context,
}) => { }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -528,22 +560,26 @@ test('Client side scene scale should match engine scale - Inch', async ({
}) })
}) })
test.describe('Client side scene scale should match engine scale - Millimeters', () => { test('Client side scene scale should match engine scale mm', async ({
const storageState = structuredClone(basicStorageState) page,
storageState.origins[0].localStorage[2].value = TOML.stringify({ context,
settings: { }) => {
...basicSettings, await context.addInitScript(async () => {
modeling: { localStorage.setItem(
...basicSettings.modeling, 'SETTINGS_PERSIST_KEY',
defaultUnit: 'mm', JSON.stringify({
}, baseUnit: 'mm',
}, cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'metric',
}) })
test.use({ )
storageState,
}) })
test('Millimeters', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -554,9 +590,7 @@ test.describe('Client side scene scale should match engine scale - Millimeters',
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
await expect( await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
@ -624,7 +658,6 @@ test.describe('Client side scene scale should match engine scale - Millimeters',
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
})
test('Sketch on face with none z-up', async ({ page, context }) => { test('Sketch on face with none z-up', async ({ page, context }) => {
const u = getUtils(page) const u = getUtils(page)
@ -633,14 +666,14 @@ test('Sketch on face with none z-up', async ({ page, context }) => {
'persistCode', 'persistCode',
`const part001 = startSketchOn('-XZ') `const part001 = startSketchOn('-XZ')
|> startProfileAt([1.4, 2.47], %) |> startProfileAt([1.4, 2.47], %)
|> line([9.31, 10.55], %, 'seg01') |> line({ to: [9.31, 10.55], tag: 'seg01' }, %)
|> line([11.91, -10.42], %) |> line([11.91, -10.42], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %) |> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01') const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([-2.89, 1.82], %) |> startProfileAt([-2.89, 1.82], %)
|> line([4.68, 3.05], %) |> line([4.68, 3.05], %)
|> line([0, -7.79], %, 'seg02') |> line({ to: [0, -7.79], tag: 'seg02' }, %)
|> close(%) |> close(%)
|> extrude(5 + 7, %) |> extrude(5 + 7, %)
` `
@ -667,4 +700,6 @@ const part002 = startSketchOn(part001, 'seg01')
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
await page.waitForTimeout(200)
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,40 +0,0 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { secrets } from './secrets'
import * as TOML from '@iarna/toml'
import { Themes } from 'lib/theme'
export const basicSettings = {
app: {
theme: Themes.Dark,
onboardingStatus: 'dismissed',
projectDirectory: '',
},
modeling: {
defaultUnit: 'in',
mouseControls: 'KittyCAD',
showDebugPanel: true,
},
projects: {
defaultProjectName: 'project-$nnn',
},
textEditor: {
textWrapping: true,
},
} satisfies Partial<SaveSettingsPayload>
export const basicStorageState = {
cookies: [],
origins: [
{
origin: 'http://localhost:3000',
localStorage: [
{ name: 'TOKEN_PERSIST_KEY', value: secrets.token },
{ name: 'persistCode', value: '' },
{
name: '/user.toml',
value: TOML.stringify({ settings: basicSettings }),
},
],
},
],
}

View File

@ -33,7 +33,7 @@ async function clearCommandLogs(page: Page) {
} }
async function expectCmdLog(page: Page, locatorStr: string) { async function expectCmdLog(page: Page, locatorStr: string) {
await expect(page.locator(locatorStr).last()).toBeVisible() await expect(page.locator(locatorStr)).toBeVisible()
} }
async function waitForDefaultPlanesToBeVisible(page: Page) { async function waitForDefaultPlanesToBeVisible(page: Page) {

View File

@ -68,10 +68,10 @@ describe('ZMA (Tauri, Linux)', () => {
const defaultDirInput = await $('[data-testid="default-directory-input"]') const defaultDirInput = await $('[data-testid="default-directory-input"]')
expect(await defaultDirInput.getValue()).toEqual(defaultDir) expect(await defaultDirInput.getValue()).toEqual(defaultDir)
const nameInput = await $('[data-testid="projects-defaultProjectName"]') const nameInput = await $('[data-testid="name-input"]')
expect(await nameInput.getValue()).toEqual('project-$nnn') expect(await nameInput.getValue()).toEqual('project-$nnn')
const closeButton = await $('[data-testid="settings-close-button"]') const closeButton = await $('[data-testid="close-button"]')
await click(closeButton) await click(closeButton)
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.17.2", "version": "0.17.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.15.0", "@codemirror/autocomplete": "^6.15.0",
@ -10,7 +10,6 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18", "@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@iarna/toml": "^2.2.5",
"@kittycad/lib": "^0.0.56", "@kittycad/lib": "^0.0.56",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
@ -23,14 +22,13 @@
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@types/node": "^18.19.26", "@types/node": "^18.19.26",
"@types/react": "^18.2.73", "@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"@uiw/react-codemirror": "^4.21.25", "@uiw/react-codemirror": "^4.21.24",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"debounce-promise": "^3.1.2", "debounce-promise": "^3.1.2",
"decamelize": "^6.0.0",
"formik": "^2.4.3", "formik": "^2.4.3",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
@ -110,7 +108,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.24.3", "@babel/preset-env": "^7.23.3",
"@playwright/test": "^1.39.0", "@playwright/test": "^1.39.0",
"@tauri-apps/cli": "^1.5.11", "@tauri-apps/cli": "^1.5.11",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
@ -134,7 +132,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.1",
"husky": "^9.0.11", "husky": "^9.0.11",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
@ -143,7 +141,7 @@
"prettier": "^2.8.0", "prettier": "^2.8.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"vite": "^5.2.6", "vite": "^5.2.2",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -1,5 +1,4 @@
import { defineConfig, devices } from '@playwright/test' import { defineConfig, devices } from '@playwright/test'
import { basicStorageState } from './e2e/playwright/storageStates'
/** /**
* Read environment variables from file. * Read environment variables from file.
@ -29,9 +28,6 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
/* Use a common shared localStorage */
storageState: basicStorageState,
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

6
src-tauri/Cargo.lock generated
View File

@ -3876,7 +3876,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs-extra" name = "tauri-plugin-fs-extra"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#73d8562849299342772e409cb4c9c2ef3073dc72" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#d3c7ee5a611fa243aebe7e52f6b246783783366e"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -4055,9 +4055,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.37.0" version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",

View File

@ -22,7 +22,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.6.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] } tauri = { version = "1.6.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.37.0", features = ["time"] } tokio = { version = "1.36.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
[features] [features]

View File

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

View File

@ -1,6 +1,6 @@
import { useCallback, MouseEventHandler, useEffect } from 'react' import { useCallback, MouseEventHandler, useEffect } from 'react'
import { DebugPanel } from './components/DebugPanel' import { DebugPanel } from './components/DebugPanel'
import { uuidv4 } from 'lib/utils' import { v4 as uuidv4 } from 'uuid'
import { PaneType, useStore } from './useStore' import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs' import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel' import { CollapsiblePanel } from './components/CollapsiblePanel'
@ -33,10 +33,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings' import { useValidateSettings } from 'hooks/useValidateSettings'
export function App() { export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS') useValidateSettings()
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
@ -64,14 +64,10 @@ export function App() {
})) }))
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
modeling: { showDebugPanel },
app: { theme, onboardingStatus },
} = settings.context
const { state, send } = useModelingContext() const { state, send } = useModelingContext()
const editorTheme = const editorTheme = theme === Themes.System ? getSystemTheme() : theme
theme.current === Themes.System ? getSystemTheme() : theme.current
// Pane toggling keyboard shortcuts // Pane toggling keyboard shortcuts
const togglePane = useCallback( const togglePane = useCallback(
@ -99,7 +95,7 @@ export function App() {
) )
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some( const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus.current (p) => p === onboardingStatus
) )
? 'opacity-20' ? 'opacity-20'
: didDragInStream : didDragInStream
@ -167,7 +163,7 @@ export function App() {
handleClasses={{ handleClasses={{
right: right:
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' + 'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
(buttonDownInStream || onboardingStatus.current === 'camera' (buttonDownInStream || onboardingStatus === 'camera'
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto'), : 'pointer-events-auto'),
}} }}
@ -208,7 +204,7 @@ export function App() {
</div> </div>
</Resizable> </Resizable>
<Stream className="absolute inset-0 z-0" /> <Stream className="absolute inset-0 z-0" />
{showDebugPanel.current && ( {showDebugPanel && (
<DebugPanel <DebugPanel
title="Debug" title="Debug"
className={ className={

View File

@ -22,18 +22,19 @@ import { paths } from 'lib/paths'
import { import {
fileLoader, fileLoader,
homeLoader, homeLoader,
indexLoader,
onboardingRedirectLoader, onboardingRedirectLoader,
settingsLoader,
} from 'lib/routeLoaders' } from 'lib/routeLoaders'
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider' import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider' import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider' import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider' import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
export const BROWSER_FILE_NAME = 'new'
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
loader: settingsLoader, loader: indexLoader,
id: paths.INDEX, id: paths.INDEX,
element: ( element: (
<CommandBarProvider> <CommandBarProvider>
@ -46,14 +47,14 @@ const router = createBrowserRouter([
</KclContextProvider> </KclContextProvider>
</CommandBarProvider> </CommandBarProvider>
), ),
errorElement: <ErrorPage />,
children: [ children: [
{ {
path: paths.INDEX, path: paths.INDEX,
loader: () => loader: () =>
isTauri() isTauri()
? redirect(paths.HOME) ? redirect(paths.HOME)
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME), : redirect(paths.FILE + '/' + BROWSER_FILE_NAME),
errorElement: <ErrorPage />,
}, },
{ {
loader: fileLoader, loader: fileLoader,
@ -72,23 +73,23 @@ const router = createBrowserRouter([
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />} {!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth> </Auth>
), ),
children: [
{
id: paths.FILE + 'SETTINGS',
loader: settingsLoader,
children: [ children: [
{ {
loader: onboardingRedirectLoader, loader: onboardingRedirectLoader,
index: true, index: true,
element: <></>, element: <></>,
}, },
{
children: [
{ {
path: makeUrlPathRelative(paths.SETTINGS), path: makeUrlPathRelative(paths.SETTINGS),
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
element: <Settings />, element: <Settings />,
}, },
{ {
path: makeUrlPathRelative(paths.ONBOARDING.INDEX), path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
element: <Onboarding />, element: <Onboarding />,
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
children: onboardingRoutes, children: onboardingRoutes,
}, },
], ],
@ -107,15 +108,8 @@ const router = createBrowserRouter([
id: paths.HOME, id: paths.HOME,
loader: homeLoader, loader: homeLoader,
children: [ children: [
{
index: true,
element: <></>,
id: paths.HOME + 'SETTINGS',
loader: settingsLoader,
},
{ {
path: makeUrlPathRelative(paths.SETTINGS), path: makeUrlPathRelative(paths.SETTINGS),
loader: settingsLoader,
element: <Settings />, element: <Settings />,
}, },
], ],

View File

@ -21,7 +21,7 @@ import {
Subscription, Subscription,
EngineCommandManager, EngineCommandManager,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { uuidv4 } from 'lib/utils' import { v4 as uuidv4 } from 'uuid'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'

View File

@ -4,15 +4,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle } from 'lib/utils'
import { sceneInfra } from 'lib/singletons' import { sceneInfra } from 'lib/singletons'
import {
EXTRA_SEGMENT_HANDLE,
PROFILE_START,
getParentGroup,
} from './sceneEntities'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false) const [isCamMoving, setIsCamMoving] = useState(false)
@ -42,10 +37,10 @@ export const ClientSideScene = ({
}: { }: {
cameraControls: ReturnType< cameraControls: ReturnType<
typeof useSettingsAuthContext typeof useSettingsAuthContext
>['settings']['context']['modeling']['mouseControls']['current'] >['settings']['context']['cameraControls']
}) => { }) => {
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext() const { state, send } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene() const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({ const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange, setHighlightRange: s.setHighlightRange,
@ -81,33 +76,9 @@ export const ClientSideScene = ({
} }
}, []) }, [])
let cursor = 'default'
if (state.matches('Sketch')) {
if (
context.mouseState.type === 'isHovering' &&
getParentGroup(context.mouseState.on, [
ARROWHEAD,
EXTRA_SEGMENT_HANDLE,
PROFILE_START,
])
) {
cursor = 'move'
} else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing'
} else if (
state.matches('Sketch.Line tool') ||
state.matches('Sketch.Tangential arc to')
) {
cursor = 'crosshair'
} else {
cursor = 'default'
}
}
return ( return (
<div <div
ref={canvasRef} ref={canvasRef}
style={{ cursor: cursor }}
className={`absolute inset-0 h-full w-full transition-all duration-300 ${ className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100' hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-black' : ''} ${ } ${hideServer ? 'bg-black' : ''} ${

View File

@ -28,15 +28,12 @@ export function createGridHelper({
gridHelper.rotation.x = Math.PI / 2 gridHelper.rotation.x = Math.PI / 2
return gridHelper return gridHelper
} }
const fudgeFactor = 72.66985970437086
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) => export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight 0.55 / cam.zoom
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) => export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) / (group.position.distanceTo(cam.position) * cam.fov) / 4000
4000 /
window.innerHeight
export function isQuaternionVertical(q: Quaternion) { export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q) const v = new Vector3(0, 0, 1).applyQuaternion(q)

View File

@ -12,7 +12,6 @@ import {
OrthographicCamera, OrthographicCamera,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Points,
Quaternion, Quaternion,
Scene, Scene,
Shape, Shape,
@ -82,25 +81,20 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { createGridHelper, orthoScale, perspScale } from './helpers' import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils' import { v4 as uuidv4 } from 'uuid'
import { SketchDetails } from 'machines/modelingMachine' import { SketchDetails } from 'machines/modelingMachine'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
export const EXTRA_SEGMENT_OFFSET_PX = 8
export const PROFILE_START = 'profile-start'
export const STRAIGHT_SEGMENT = 'straight-segment' export const STRAIGHT_SEGMENT = 'straight-segment'
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body' export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed' export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed'
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment' 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 SEGMENT_WIDTH_PX = 1.6 export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
export const HIDE_SEGMENT_LENGTH = 75 // in pixels 'tangential-arc-to-segment-body-dashed'
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels export const PROFILE_START = 'profile-start'
// This singleton Class is responsible for all of the things the user sees and interacts with. // This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements. // That mostly mean sketch elements.
@ -117,12 +111,8 @@ export class SceneEntities {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
this.scene = sceneInfra?.scene this.scene = sceneInfra?.scene
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange) sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
window.addEventListener('resize', this.onWindowResize)
} }
onWindowResize = () => {
this.onCamChange()
}
onCamChange = () => { onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -292,6 +282,7 @@ export class SceneEntities {
sketchGroup: SketchGroup sketchGroup: SketchGroup
variableDeclarationName: string variableDeclarationName: string
}> { }> {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane() this.createIntersectionPlane()
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
@ -304,7 +295,7 @@ export class SceneEntities {
}) })
const sketchGroup = sketchGroupFromPathToNode({ const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
ast: maybeModdedAst, ast: kclManager.ast,
programMemory, programMemory,
}) })
if (!Array.isArray(sketchGroup?.value)) if (!Array.isArray(sketchGroup?.value))
@ -392,7 +383,6 @@ export class SceneEntities {
pathToNode: segPathToNode, pathToNode: segPathToNode,
isDraftSegment, isDraftSegment,
scale: factor, scale: factor,
texture: sceneInfra.extraSegmentTexture,
}) })
} else { } else {
seg = straightSegment({ seg = straightSegment({
@ -403,7 +393,6 @@ export class SceneEntities {
isDraftSegment, isDraftSegment,
scale: factor, scale: factor,
callExpName, callExpName,
texture: sceneInfra.extraSegmentTexture,
}) })
} }
seg.layers.set(SKETCH_LAYER) seg.layers.set(SKETCH_LAYER)
@ -446,7 +435,6 @@ export class SceneEntities {
) => { ) => {
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
await this.setupSketch({ await this.setupSketch({
sketchPathToNode, sketchPathToNode,
forward, forward,
@ -454,12 +442,7 @@ export class SceneEntities {
position: origin, position: origin,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
}) })
this.setupSketchIdleCallbacks({ this.setupSketchIdleCallbacks(sketchPathToNode)
forward,
up,
position: origin,
pathToNode: sketchPathToNode,
})
} }
setUpDraftSegment = async ( setUpDraftSegment = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
@ -484,20 +467,19 @@ export class SceneEntities {
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1` const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const mod = addNewSketchLn({ let modifiedAst = addNewSketchLn({
node: _ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
to: [lastSeg.to[0], lastSeg.to[1]], to: [lastSeg.to[0], lastSeg.to[1]],
from: [lastSeg.to[0], lastSeg.to[1]], from: [lastSeg.to[0], lastSeg.to[1]],
fnName: segmentName, fnName: segmentName,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}) }).modifiedAst
const modifiedAst = parse(recast(mod.modifiedAst)) modifiedAst = parse(recast(modifiedAst))
const draftExpressionsIndices = { start: index, end: index } const draftExpressionsIndices = { start: index, end: index }
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketchGroup } = const { truncatedAst, programMemoryOverride, sketchGroup } =
await this.setupSketch({ await this.setupSketch({
sketchPathToNode, sketchPathToNode,
@ -564,104 +546,13 @@ export class SceneEntities {
}, },
}) })
}, },
...this.mouseEnterLeaveCallbacks(), ...mouseEnterLeaveCallbacks(),
}) })
} }
setupSketchIdleCallbacks = ({ setupSketchIdleCallbacks = (pathToNode: PathToNode) => {
pathToNode,
up,
forward,
position,
}: {
pathToNode: PathToNode
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
}) => {
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDragEnd: async () => { onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
if (addingNewSegmentStatus !== 'nothing') {
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
up,
forward,
position,
})
// setting up the callbacks again resets value in closures
this.setupSketchIdleCallbacks({
pathToNode,
up,
forward,
position,
})
}
},
onDrag: async ({
selected,
intersectionPoint,
mouseEvent,
intersects,
}) => {
if (mouseEvent.which !== 1) return if (mouseEvent.which !== 1) return
const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE])
if (group?.name === EXTRA_SEGMENT_HANDLE) {
const segGroup = getParentGroup(selected)
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
const pathToNodeIndex = pathToNode.findIndex(
(x) => x[1] === 'PipeExpression'
)
const sketchGroup = sketchGroupFromPathToNode({
pathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketchGroup.value[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: [prevSegment.from[0], prevSegment.from[1]],
// TODO assuming it's always a straight segments being added
// as this is easiest, and we'll need to add "tabbing" behavior
// to support other segment types
fnName: 'line',
pathToNode: pathToNode,
spliceBetween: true,
})
addingNewSegmentStatus = 'pending'
await kclManager.executeAstMock(mod.modifiedAst, {
updates: 'code',
})
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
up,
forward,
position,
})
addingNewSegmentStatus = 'added'
} else if (addingNewSegmentStatus === 'added') {
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
this.onDragSegment({
sketchPathToNode: pathToNodeForNewSegment,
object: selected,
intersection2d: intersectionPoint.twoD,
intersects,
})
}
return
}
this.onDragSegment({ this.onDragSegment({
object: selected, object: selected,
intersection2d: intersectionPoint.twoD, intersection2d: intersectionPoint.twoD,
@ -686,7 +577,7 @@ export class SceneEntities {
if (!event) return if (!event) return
sceneInfra.modelingSend(event) sceneInfra.modelingSend(event)
}, },
...this.mouseEnterLeaveCallbacks(), ...mouseEnterLeaveCallbacks(),
}) })
} }
prepareTruncatedMemoryAndAst = ( prepareTruncatedMemoryAndAst = (
@ -864,7 +755,8 @@ export class SceneEntities {
group.userData.to = to group.userData.to = to
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
arrowGroup.position.set(to[0], to[1], 0)
const previousPoint = const previousPoint =
prevSegment?.type === 'TangentialArcTo' prevSegment?.type === 'TangentialArcTo'
@ -882,21 +774,6 @@ export class SceneEntities {
obtuse: true, obtuse: true,
}) })
const pxLength = arcInfo.arcLength / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [TANGENTIAL_ARC_TO_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0)
const arrowheadAngle = const arrowheadAngle =
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1) arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
arrowGroup.quaternion.setFromUnitVectors( arrowGroup.quaternion.setFromUnitVectors(
@ -904,27 +781,6 @@ export class SceneEntities {
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
) )
arrowGroup.scale.set(scale, scale, scale) arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
if (extraSegmentGroup) {
const circumferenceInPx = (2 * Math.PI * arcInfo.radius) / scale
const extraSegmentAngleDelta =
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
const extraSegmentAngle =
arcInfo.startAngle + (arcInfo.ccw ? 1 : -1) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2(
Math.cos(extraSegmentAngle) * arcInfo.radius,
Math.sin(extraSegmentAngle) * arcInfo.radius
)
extraSegmentGroup.position.set(
arcInfo.center[0] + extraSegmentOffset.x,
arcInfo.center[1] + extraSegmentOffset.y,
0
)
extraSegmentGroup.scale.set(scale, scale, scale)
extraSegmentGroup.visible = isHandlesVisible
}
const tangentialArcToSegmentBody = group.children.find( const tangentialArcToSegmentBody = group.children.find(
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY (child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
@ -971,26 +827,10 @@ export class SceneEntities {
group.userData.from = from group.userData.from = from
group.userData.to = to group.userData.to = to
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case) shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) shape.lineTo(0, 0.08 * scale) // The width of the line
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
)
const pxLength = length / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [STRAIGHT_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) { if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
@ -1002,21 +842,6 @@ export class SceneEntities {
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale) arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
extraSegmentGroup.position.set(
from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y,
0
)
extraSegmentGroup.scale.set(scale, scale, scale)
extraSegmentGroup.visible = isHandlesVisible
} }
const straightSegmentBody = group.children.find( const straightSegmentBody = group.children.find(
@ -1194,119 +1019,6 @@ export class SceneEntities {
}, },
}) })
} }
mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
extraSegmentGroup.traverse((child) => {
if (child instanceof Points || child instanceof Mesh) {
child.material.opacity = dragSelected ? 0 : 1
}
})
}
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
if (parent.name === STRAIGHT_SEGMENT) {
this.updateStraightSegment({
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
}
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
if (parent.name === STRAIGHT_SEGMENT) {
this.updateStraightSegment({
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
}
}
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
extraSegmentGroup.traverse((child) => {
if (child instanceof Points || child instanceof Mesh) {
child.material.opacity = 0
}
})
}
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}
} }
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ' export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
@ -1448,7 +1160,7 @@ function colorSegment(object: any, color: number) {
]) ])
if (straightSegmentBody) { if (straightSegmentBody) {
straightSegmentBody.traverse((child) => { straightSegmentBody.traverse((child) => {
if (child instanceof Mesh && !child.userData.ignoreColorChange) { if (child instanceof Mesh) {
child.material.color.set(color) child.material.color.set(color)
} }
}) })
@ -1549,3 +1261,53 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2]) ? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z) : new Vector3(a.x, a.y, a.z)
} }
function mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}

View File

@ -18,8 +18,6 @@ import {
Intersection, Intersection,
Object3D, Object3D,
Object3DEventMap, Object3DEventMap,
TextureLoader,
Texture,
} from 'three' } from 'three'
import { compareVec2Epsilon2 } from 'lang/std/sketch' import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -27,10 +25,9 @@ import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm' import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections' import { Axis } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes' import { type BaseUnit } from 'lib/settings/settingsTypes'
import { SETTINGS_PERSIST_KEY } from 'lib/constants'
import { CameraControls } from './CameraControls' import { CameraControls } from './CameraControls'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { settings } from 'lib/settings/initialSettings'
import { MouseState } from 'machines/modelingMachine'
type SendType = ReturnType<typeof useModelingContext>['send'] type SendType = ReturnType<typeof useModelingContext>['send']
@ -57,7 +54,6 @@ export const ARROWHEAD = 'arrowhead'
export interface OnMouseEnterLeaveArgs { export interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap> selected: Object3D<Object3DEventMap>
dragSelected?: Object3D<Object3DEventMap>
mouseEvent: MouseEvent mouseEvent: MouseEvent
} }
@ -102,26 +98,18 @@ export class SceneInfra {
isFovAnimationInProgress = false isFovAnimationInProgress = false
_baseUnit: BaseUnit = 'mm' _baseUnit: BaseUnit = 'mm'
_baseUnitMultiplier = 1 _baseUnitMultiplier = 1
extraSegmentTexture: Texture
lastMouseState: MouseState = { type: 'idle' }
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: OnMouseEnterLeaveArgs) => void = () => {} onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseLeave: (arg: OnMouseEnterLeaveArgs) => 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: OnMouseEnterLeaveArgs) => void onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
}) => { }) => {
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
@ -140,8 +128,6 @@ export class SceneInfra {
} }
resetMouseListeners = () => { resetMouseListeners = () => {
this.setCallbacks({ this.setCallbacks({
onDragStart: () => {},
onDragEnd: () => {},
onDrag: () => {}, onDrag: () => {},
onMove: () => {}, onMove: () => {},
onClick: () => {}, onClick: () => {},
@ -184,7 +170,9 @@ export class SceneInfra {
// CAMERA // CAMERA
const camHeightDistanceRatio = 0.5 const camHeightDistanceRatio = 0.5
const baseUnit: BaseUnit = settings.modeling.defaultUnit.current const baseUnit: BaseUnit =
JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || ('{}' as any))
.baseUnit || 'mm'
const baseRadius = 5.6 const baseRadius = 5.6
const length = baseUnitTomm(baseUnit) * baseRadius const length = baseUnitTomm(baseUnit) * baseRadius
const ang = Math.atan(camHeightDistanceRatio) const ang = Math.atan(camHeightDistanceRatio)
@ -224,13 +212,6 @@ export class SceneInfra {
const light = new AmbientLight(0x505050) // soft white light const light = new AmbientLight(0x505050) // soft white light
this.scene.add(light) this.scene.add(light)
const textureLoader = new TextureLoader()
this.extraSegmentTexture = textureLoader.load(
'/clientSideSceneAssets/extra-segment-texture.png'
)
this.extraSegmentTexture.anisotropy =
this.renderer?.capabilities?.getMaxAnisotropy?.()
SceneInfra.instance = this SceneInfra.instance = this
} }
@ -340,6 +321,8 @@ export class SceneInfra {
planeIntersectPoint.twoD && planeIntersectPoint.twoD &&
planeIntersectPoint.threeD planeIntersectPoint.threeD
) { ) {
// // console.log('onDrag', this.selected)
this.onDragCallback({ this.onDragCallback({
mouseEvent, mouseEvent,
intersectionPoint: { intersectionPoint: {
@ -349,10 +332,6 @@ export class SceneInfra {
intersects, intersects,
selected: this.selected.object, selected: this.selected.object,
}) })
this.updateMouseState({
type: 'isDragging',
on: this.selected.object,
})
} }
} else if ( } else if (
planeIntersectPoint && planeIntersectPoint &&
@ -372,34 +351,25 @@ export class SceneInfra {
if (intersects[0]) { if (intersects[0]) {
const firstIntersectObject = intersects[0].object const firstIntersectObject = intersects[0].object
if (this.hoveredObject !== firstIntersectObject) { if (this.hoveredObject !== firstIntersectObject) {
const hoveredObj = this.hoveredObject if (this.hoveredObject) {
this.hoveredObject = null
this.onMouseLeave({ this.onMouseLeave({
selected: hoveredObj, selected: this.hoveredObject,
mouseEvent: mouseEvent, mouseEvent: mouseEvent,
}) })
}
this.hoveredObject = firstIntersectObject this.hoveredObject = firstIntersectObject
this.onMouseEnter({ this.onMouseEnter({
selected: this.hoveredObject, selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent, mouseEvent: mouseEvent,
}) })
if (!this.selected)
this.updateMouseState({
type: 'isHovering',
on: this.hoveredObject,
})
} }
} else { } else {
if (this.hoveredObject) { if (this.hoveredObject) {
const hoveredObj = this.hoveredObject
this.hoveredObject = null
this.onMouseLeave({ this.onMouseLeave({
selected: hoveredObj, selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent, mouseEvent: mouseEvent,
}) })
if (!this.selected) this.updateMouseState({ type: 'idle' }) this.hoveredObject = null
} }
} }
} }
@ -456,11 +426,6 @@ export class SceneInfra {
(a, b) => a.distance - b.distance (a, b) => a.distance - b.distance
) )
} }
updateMouseState(mouseState: MouseState) {
if (this.lastMouseState.type === mouseState.type) return
this.lastMouseState = mouseState
this.modelingSend({ type: 'Set mouse state', data: mouseState })
}
onMouseDown = (event: MouseEvent) => { onMouseDown = (event: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1 this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
@ -490,26 +455,8 @@ export class SceneInfra {
if (this.selected) { if (this.selected) {
if (this.selected.hasBeenDragged) { if (this.selected.hasBeenDragged) {
// TODO do the types properly here // this is where we could fire a onDragEnd event
this.onDragEndCallback({ // console.log('onDragEnd', this.selected)
intersectionPoint: {
twoD: planeIntersectPoint?.twoD as any,
threeD: planeIntersectPoint?.threeD as any,
},
intersects,
mouseEvent,
selected: this.selected as any,
})
if (intersects.length) {
this.updateMouseState({
type: 'isHovering',
on: intersects[0].object,
})
} else {
this.updateMouseState({
type: 'idle',
})
}
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) { } else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
// fire onClick event as there was no drags // fire onClick event as there was no drags
this.onClickCallback({ this.onClickCallback({

View File

@ -12,22 +12,15 @@ import {
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
NormalBufferAttributes, NormalBufferAttributes,
Points,
PointsMaterial,
Shape, Shape,
SphereGeometry, SphereGeometry,
Texture,
Vector2, Vector2,
Vector3, Vector3,
} from 'three' } from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import { import {
EXTRA_SEGMENT_HANDLE,
EXTRA_SEGMENT_OFFSET_PX,
HIDE_SEGMENT_LENGTH,
PROFILE_START, PROFILE_START,
SEGMENT_WIDTH_PX,
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY, STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH, STRAIGHT_SEGMENT_DASH,
@ -51,7 +44,7 @@ export function profileStart({
}) { }) {
const group = new Group() const group = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later const geometry = new BoxGeometry(0.8, 0.8, 0.8)
const body = new MeshBasicMaterial({ color: 0xffffff }) const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body) const mesh = new Mesh(geometry, body)
@ -78,7 +71,6 @@ export function straightSegment({
isDraftSegment, isDraftSegment,
scale = 1, scale = 1,
callExpName, callExpName,
texture,
}: { }: {
from: Coords2d from: Coords2d
to: Coords2d to: Coords2d
@ -87,13 +79,12 @@ export function straightSegment({
isDraftSegment?: boolean isDraftSegment?: boolean
scale?: number scale?: number
callExpName: string callExpName: string
texture: Texture
}): Group { }): Group {
const group = new Group() const group = new Group()
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) shape.lineTo(0, 0.08 * scale) // The width of the line
let geometry let geometry
if (isDraftSegment) { if (isDraftSegment) {
@ -131,44 +122,24 @@ export function straightSegment({
} }
group.name = STRAIGHT_SEGMENT group.name = STRAIGHT_SEGMENT
const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
)
const arrowGroup = createArrowhead(scale) const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3() const dir = new Vector3()
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0)) .subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
const pxLength = length / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
group.add(mesh) group.add(mesh)
if (callExpName !== 'close') group.add(arrowGroup) if (callExpName !== 'close') group.add(arrowGroup)
const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
extraSegmentGroup.position.set(
from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y,
0
)
extraSegmentGroup.visible = !shouldHide
group.add(extraSegmentGroup)
return group return group
} }
function createArrowhead(scale = 1): Group { function createArrowhead(scale = 1): Group {
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff }) const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
// specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px) const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
// we'll scale the group to the correct size later to match these sizes in screen space arrowheadMesh.position.set(0, -0.6, 0)
const arrowheadMesh = new Mesh(new ConeGeometry(4.5, 20, 12), arrowMaterial) const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
arrowheadMesh.position.set(0, -9, 0)
const sphereMesh = new Mesh(new SphereGeometry(4, 12, 12), arrowMaterial)
const arrowGroup = new Group() const arrowGroup = new Group()
arrowGroup.userData.type = ARROWHEAD arrowGroup.userData.type = ARROWHEAD
@ -179,36 +150,6 @@ function createArrowhead(scale = 1): Group {
return arrowGroup return arrowGroup
} }
function createExtraSegmentHandle(scale: number, texture: Texture): Group {
const particleMaterial = new PointsMaterial({
size: 12, // in pixels
map: texture,
transparent: true,
opacity: 0,
depthTest: false,
})
const mat = new MeshBasicMaterial({
transparent: true,
color: 0xffffff,
opacity: 0,
})
const particleGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
])
const sphereMesh = new Mesh(new SphereGeometry(6, 12, 12), mat) // sphere radius in pixels
const particle = new Points(particleGeometry, particleMaterial)
particle.userData.ignoreColorChange = true
particle.userData.type = EXTRA_SEGMENT_HANDLE
const extraSegmentGroup = new Group()
extraSegmentGroup.userData.type = EXTRA_SEGMENT_HANDLE
extraSegmentGroup.name = EXTRA_SEGMENT_HANDLE
extraSegmentGroup.add(sphereMesh)
extraSegmentGroup.add(particle)
extraSegmentGroup.scale.set(scale, scale, scale)
return extraSegmentGroup
}
export function tangentialArcToSegment({ export function tangentialArcToSegment({
prevSegment, prevSegment,
from, from,
@ -217,7 +158,6 @@ export function tangentialArcToSegment({
pathToNode, pathToNode,
isDraftSegment, isDraftSegment,
scale = 1, scale = 1,
texture,
}: { }: {
prevSegment: SketchGroup['value'][number] prevSegment: SketchGroup['value'][number]
from: Coords2d from: Coords2d
@ -226,7 +166,6 @@ export function tangentialArcToSegment({
pathToNode: PathToNode pathToNode: PathToNode
isDraftSegment?: boolean isDraftSegment?: boolean
scale?: number scale?: number
texture: Texture
}): Group { }): Group {
const group = new Group() const group = new Group()
@ -239,8 +178,7 @@ export function tangentialArcToSegment({
) )
: prevSegment.from : prevSegment.from
const { center, radius, startAngle, endAngle, ccw, arcLength } = const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
getTangentialArcToInfo({
arcStartPoint: from, arcStartPoint: from,
arcEndPoint: to, arcEndPoint: to,
tanPreviousPoint: previousPoint, tanPreviousPoint: previousPoint,
@ -281,28 +219,8 @@ export function tangentialArcToSegment({
new Vector3(0, 1, 0), new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
) )
const pxLength = arcLength / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
const extraSegmentGroup = createExtraSegmentHandle(scale, texture) group.add(mesh, arrowGroup)
const circumferenceInPx = (2 * Math.PI * radius) / scale
const extraSegmentAngleDelta =
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
const extraSegmentAngle = startAngle + (ccw ? 1 : -1) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2(
Math.cos(extraSegmentAngle) * radius,
Math.sin(extraSegmentAngle) * radius
)
extraSegmentGroup.position.set(
center[0] + extraSegmentOffset.x,
center[1] + extraSegmentOffset.y,
0
)
extraSegmentGroup.visible = !shouldHide
group.add(mesh, arrowGroup, extraSegmentGroup)
return group return group
} }
@ -324,8 +242,8 @@ export function createArcGeometry({
isDashed?: boolean isDashed?: boolean
scale?: number scale?: number
}): BufferGeometry { }): BufferGeometry {
const dashSizePx = 18 * scale const dashSize = 1.2 * scale
const gapSizePx = 18 * scale const gapSize = 1.2 * scale
const arcStart = new EllipseCurve( const arcStart = new EllipseCurve(
center[0], center[0],
center[1], center[1],
@ -347,8 +265,8 @@ export function createArcGeometry({
0 0
) )
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line shape.lineTo(0, 0.08 * scale) // The width of the line
if (!isDashed) { if (!isDashed) {
const points = arcStart.getPoints(50) const points = arcStart.getPoints(50)
@ -363,7 +281,7 @@ export function createArcGeometry({
} }
const length = arcStart.getLength() const length = arcStart.getLength()
const totalDashes = length / (dashSizePx + gapSizePx) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
const dashGeometries = [] const dashGeometries = []
@ -371,8 +289,8 @@ export function createArcGeometry({
// Function to create a dash at a specific t value (0 to 1 along the curve) // Function to create a dash at a specific t value (0 to 1 along the curve)
const createDashAt = (t: number, curve: EllipseCurve) => { const createDashAt = (t: number, curve: EllipseCurve) => {
const startVec = curve.getPoint(t) const startVec = curve.getPoint(t)
const endVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length)) const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
const midVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length / 2)) const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
const dashCurve = new CurvePath<Vector3>() const dashCurve = new CurvePath<Vector3>()
dashCurve.add( dashCurve.add(
new CatmullRomCurve3([ new CatmullRomCurve3([
@ -396,8 +314,7 @@ export function createArcGeometry({
} }
// fill in the remaining arc // fill in the remaining arc
const remainingArcLength = const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
length - dashesAtEachEnd * 2 * (dashSizePx + gapSizePx)
if (remainingArcLength > 0) { if (remainingArcLength > 0) {
const remainingArcStartT = dashesAtEachEnd / totalDashes const remainingArcStartT = dashesAtEachEnd / totalDashes
const remainingArcEndT = 1 - remainingArcStartT const remainingArcEndT = 1 - remainingArcStartT
@ -442,8 +359,8 @@ export function dashedStraight(
shape: Shape, shape: Shape,
scale = 1 scale = 1
): BufferGeometry<NormalBufferAttributes> { ): BufferGeometry<NormalBufferAttributes> {
const dashSize = 18 * scale const dashSize = 1.2 * scale
const gapSize = 18 * scale // TODO: gapSize is not respected const gapSize = 1.2 * scale // todo: gabSize is not respected
const dashLine = new LineCurve3( const dashLine = new LineCurve3(
new Vector3(from[0], from[1], 0), new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0) new Vector3(to[0], to[1], 0)

View File

@ -1,35 +1,29 @@
import { Combobox } from '@headlessui/react' import { Combobox } from '@headlessui/react'
import { useSelector } from '@xstate/react'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes' import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate'
const contextSelector = (snapshot: StateFrom<AnyStateMachine>) =>
snapshot.context
function CommandArgOptionInput({ function CommandArgOptionInput({
arg, options,
argName, argName,
stepBack, stepBack,
onSubmit, onSubmit,
placeholder, placeholder,
}: { }: {
arg: CommandArgument<unknown> & { inputType: 'options' } options: (CommandArgument<unknown> & { inputType: 'options' })['options']
argName: string argName: string
stepBack: () => void stepBack: () => void
onSubmit: (data: unknown) => void onSubmit: (data: unknown) => void
placeholder?: string placeholder?: string
}) { }) {
const actorContext = useSelector(arg.machineActor, contextSelector)
const { commandBarSend, commandBarState } = useCommandsContext() const { commandBarSend, commandBarState } = useCommandsContext()
const resolvedOptions = useMemo( const resolvedOptions = useMemo(
() => () =>
typeof arg.options === 'function' typeof options === 'function'
? arg.options(commandBarState.context, actorContext) ? options(commandBarState.context)
: arg.options, : options,
[argName, arg, commandBarState.context, actorContext] [argName, options, commandBarState.context]
) )
// The initial current option is either an already-input value or the configured default // The initial current option is either an already-input value or the configured default
const currentOption = useMemo( const currentOption = useMemo(
@ -44,7 +38,7 @@ function CommandArgOptionInput({
const [selectedOption, setSelectedOption] = useState< const [selectedOption, setSelectedOption] = useState<
CommandArgumentOption<unknown> CommandArgumentOption<unknown>
>(currentOption || resolvedOptions[0]) >(currentOption || resolvedOptions[0])
const initialQuery = useMemo(() => '', [arg.options, argName]) const initialQuery = useMemo(() => '', [options, argName])
const [query, setQuery] = useState(initialQuery) const [query, setQuery] = useState(initialQuery)
const [filteredOptions, setFilteredOptions] = const [filteredOptions, setFilteredOptions] =
useState<typeof resolvedOptions>() useState<typeof resolvedOptions>()

View File

@ -51,24 +51,7 @@ function ArgumentInput({
case 'options': case 'options':
return ( return (
<CommandArgOptionInput <CommandArgOptionInput
arg={arg} options={arg.options}
argName={arg.name}
stepBack={stepBack}
onSubmit={onSubmit}
placeholder="Select an option"
/>
)
case 'boolean':
return (
<CommandArgOptionInput
arg={{
...arg,
inputType: 'options',
options: [
{ name: 'On', value: true },
{ name: 'Off', value: false },
],
}}
argName={arg.name} argName={arg.name}
stepBack={stepBack} stepBack={stepBack}
onSubmit={onSubmit} onSubmit={onSubmit}

View File

@ -74,7 +74,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
selectedCommand.icon && ( selectedCommand.icon && (
<CustomIcon name={selectedCommand.icon} className="w-5 h-5" /> <CustomIcon name={selectedCommand.icon} className="w-5 h-5" />
)} )}
{selectedCommand.displayName || selectedCommand.name} {selectedCommand?.name}
</p> </p>
{Object.entries(selectedCommand?.args || {}) {Object.entries(selectedCommand?.args || {})
.filter(([_, argConfig]) => .filter(([_, argConfig]) =>

View File

@ -76,9 +76,9 @@ function CommandBarKclInput({
}, },
accessKey: 'command-bar', accessKey: 'command-bar',
theme: theme:
settings.context.app.theme.current === 'system' settings.context.theme === 'system'
? getSystemTheme() ? getSystemTheme()
: settings.context.app.theme.current, : settings.context.theme,
extensions: [ extensions: [
EditorView.domEventHandlers({ EditorView.domEventHandlers({
keydown: (event) => { keydown: (event) => {

View File

@ -20,7 +20,7 @@ function CommandComboBox({
options.find((o) => 'isCurrent' in o && o.isCurrent) || null options.find((o) => 'isCurrent' in o && o.isCurrent) || null
const fuse = new Fuse(options, { const fuse = new Fuse(options, {
keys: ['displayName', 'name', 'description'], keys: ['name', 'description'],
threshold: 0.3, threshold: 0.3,
}) })
@ -80,12 +80,7 @@ function CommandComboBox({
className="w-5 h-5 dark:text-energy-10" className="w-5 h-5 dark:text-energy-10"
/> />
)} )}
<p className="flex-grow">{option.displayName || option.name} </p> <p className="flex-grow">{option.name} </p>
{option.description && (
<p className="text-xs text-chalkboard-60 dark:text-chalkboard-40">
{option.description}
</p>
)}
</Combobox.Option> </Combobox.Option>
))} ))}
</Combobox.Options> </Combobox.Options>

View File

@ -25,9 +25,7 @@ export type CustomIconName =
| 'network' | 'network'
| 'networkCrossedOut' | 'networkCrossedOut'
| 'parallel' | 'parallel'
| 'person'
| 'plus' | 'plus'
| 'refresh'
| 'search' | 'search'
| 'settings' | 'settings'
| 'sketch' | 'sketch'
@ -455,22 +453,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'person':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 6C12 7.10457 11.1046 8 10 8C8.89543 8 8 7.10457 8 6C8 4.89543 8.89543 4 10 4C11.1046 4 12 4.89543 12 6ZM13 6C13 7.65685 11.6569 9 10 9C8.34315 9 7 7.65685 7 6C7 4.34315 8.34315 3 10 3C11.6569 3 13 4.34315 13 6ZM5 12V11L9 10H11L15 11V12C15 14.7614 12.7614 17 10 17C7.23858 17 5 14.7614 5 12ZM6 11.7808L9.12311 11H10.8769L14 11.7808V12C14 14.2091 12.2091 16 10 16C7.79086 16 6 14.2091 6 12V11.7808Z"
fill="currentColor"
/>
</svg>
)
case 'plus': case 'plus':
return ( return (
<svg <svg
@ -480,29 +462,13 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fillRule="evenodd" fill-rule="evenodd"
clipRule="evenodd" clip-rule="evenodd"
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z" d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
) )
case 'refresh':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.93434 4.43959L9.4014 4.26111L14.0251 2.49432L14.382 3.42845L11.5647 4.50499L10.5648 4.51221C10.8323 4.53935 11.0173 4.58539 11.2161 4.63484C11.2179 4.63528 11.2197 4.63572 11.2214 4.63616L11.2195 4.6369C11.8713 4.78513 12.4941 5.05172 13.0556 5.42692C13.9601 6.03127 14.6651 6.89025 15.0813 7.89524C15.4976 8.90024 15.6065 10.0061 15.3943 11.073C15.1821 12.1399 14.6583 13.1199 13.8891 13.8891C13.1199 14.6583 12.1399 15.1821 11.073 15.3943C10.0061 15.6065 8.90023 15.4976 7.89524 15.0813C6.89025 14.6651 6.03126 13.9601 5.42692 13.0556C4.82257 12.1512 4.5 11.0878 4.5 10H5.5C5.5 10.89 5.76392 11.76 6.25839 12.5001C6.75285 13.2401 7.45566 13.8169 8.27792 14.1575C9.10019 14.4981 10.005 14.5872 10.8779 14.4135C11.7508 14.2399 12.5526 13.8113 13.182 13.182C13.8113 12.5526 14.2399 11.7508 14.4135 10.8779C14.5872 10.005 14.4981 9.10019 14.1575 8.27793C13.8169 7.45566 13.2401 6.75286 12.5001 6.25839C11.8763 5.84159 11.1601 5.5886 10.4175 5.51941L11.8137 9.17339L10.8796 9.53033L9.11281 4.90665L8.93434 4.43959Z"
fill="currentColor"
/>
</svg>
)
case 'search': case 'search':
return ( return (
<svg <svg

View File

@ -13,7 +13,7 @@ import {
StateFrom, StateFrom,
} from 'xstate' } from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine' import { DEFAULT_FILE_NAME, fileMachine } from 'machines/fileMachine'
import { import {
createDir, createDir,
removeDir, removeDir,
@ -21,10 +21,9 @@ import {
renameFile, renameFile,
writeFile, writeFile,
} from '@tauri-apps/api/fs' } from '@tauri-apps/api/fs'
import { readProject } from 'lib/tauriFS' import { FILE_EXT, readProject } from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>

View File

@ -11,8 +11,7 @@ import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styles from './FileTree.module.css' import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS' import { FILE_EXT, sortProject } from 'lib/tauriFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus' import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'

View File

@ -66,16 +66,13 @@ export const ModelingMachineProvider = ({
const { const {
auth, auth,
settings: { settings: {
context: { context: { baseUnit, theme },
app: { theme },
modeling: { defaultUnit },
},
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
const { code } = useKclContext() const { code } = useKclContext()
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
useSetupEngineManager(streamRef, token, theme.current) useSetupEngineManager(streamRef, token, theme)
const { const {
isShiftDown, isShiftDown,
@ -112,9 +109,6 @@ export const ModelingMachineProvider = ({
kclManager.executeAst() kclManager.executeAst()
} }
}, },
'Set mouse state': assign({
mouseState: (_, event) => event.data,
}),
'Set selection': assign(({ selectionRanges }, event) => { 'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data const setSelections = event.data
@ -240,7 +234,7 @@ export const ModelingMachineProvider = ({
format.type === 'stl' || format.type === 'stl' ||
format.type === 'ply' format.type === 'ply'
) { ) {
format.units = defaultUnit.current format.units = baseUnit
} }
if (format.type === 'ply' || format.type === 'stl') { if (format.type === 'ply' || format.type === 'stl') {

View File

@ -9,8 +9,7 @@ import {
faTrashAlt, faTrashAlt,
faX, faX,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { getPartsCount, readProject } from '../lib/tauriFS' import { FILE_EXT, getPartsCount, readProject } from '../lib/tauriFS'
import { FILE_EXT } from 'lib/constants'
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'

View File

@ -6,9 +6,12 @@ import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect } from 'react' import React, { createContext, useEffect } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands' import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import {
fallbackLoadedSettings,
validateSettings,
} from 'lib/settings/settingsUtils'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme' import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme'
import decamelize from 'decamelize'
import { import {
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
@ -17,19 +20,10 @@ import {
StateFrom, StateFrom,
} from 'xstate' } from 'xstate'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons' import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils' import { v4 as uuidv4 } from 'uuid'
import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings'
import {
createSettingsCommand,
settingsWithCommandConfigs,
} from 'lib/commandBarConfigs/settingsCommandConfig'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { BaseUnit } from 'lib/settings/settingsTypes'
import { saveSettings } from 'lib/settings/settingsUtils'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -55,13 +49,11 @@ export const SettingsAuthProvider = ({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const loadedSettings = useRouteLoaderData(paths.INDEX) as typeof settings const loadedSettings = useRouteLoaderData(paths.INDEX) as Awaited<
const loadedProject = useRouteLoaderData(paths.FILE) as IndexLoaderData ReturnType<typeof validateSettings>
return (
<SettingsAuthProviderBase
loadedSettings={loadedSettings}
loadedProject={loadedProject}
> >
return (
<SettingsAuthProviderBase loadedSettings={loadedSettings}>
{children} {children}
</SettingsAuthProviderBase> </SettingsAuthProviderBase>
) )
@ -74,7 +66,7 @@ export const SettingsAuthProviderJest = ({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const loadedSettings = settings const loadedSettings = fallbackLoadedSettings
return ( return (
<SettingsAuthProviderBase loadedSettings={loadedSettings}> <SettingsAuthProviderBase loadedSettings={loadedSettings}>
{children} {children}
@ -85,25 +77,23 @@ export const SettingsAuthProviderJest = ({
export const SettingsAuthProviderBase = ({ export const SettingsAuthProviderBase = ({
children, children,
loadedSettings, loadedSettings,
loadedProject,
}: { }: {
children: React.ReactNode children: React.ReactNode
loadedSettings: typeof settings loadedSettings: Awaited<ReturnType<typeof validateSettings>>
loadedProject?: IndexLoaderData
}) => { }) => {
const { settings: initialLoadedContext } = loadedSettings
const navigate = useNavigate() const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const [settingsState, settingsSend, settingsActor] = useMachine( const [settingsState, settingsSend, settingsActor] = useMachine(
settingsMachine, settingsMachine,
{ {
context: loadedSettings, context: initialLoadedContext,
actions: { actions: {
setClientSideSceneUnits: (context, event) => { setClientSideSceneUnits: (context, event) => {
const newBaseUnit = const newBaseUnit =
event.type === 'set.modeling.defaultUnit' event.type === 'Set Base Unit'
? (event.data.value as BaseUnit) ? event.data.baseUnit
: context.modeling.defaultUnit.current : context.baseUnit
sceneInfra.baseUnit = newBaseUnit sceneInfra.baseUnit = newBaseUnit
}, },
setEngineTheme: (context) => { setEngineTheme: (context) => {
@ -112,76 +102,39 @@ export const SettingsAuthProviderBase = ({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'set_background_color', type: 'set_background_color',
color: getThemeColorForEngine(context.app.theme.current), color: getThemeColorForEngine(context.theme),
}, },
}) })
}, },
toastSuccess: (context, event) => { toastSuccess: (context, event) => {
const eventParts = event.type.replace(/^set./, '').split('.') as [ const truncatedNewValue =
keyof typeof settings, 'data' in event && event.data instanceof Object
string ? (context[Object.keys(event.data)[0] as keyof typeof context]
] .toString()
const truncatedNewValue = event.data.value?.toString().slice(0, 28) .substring(0, 28) as any)
const message = : undefined
`Set ${decamelize(eventParts[1], { separator: ' ' })}` + toast.success(
event.type +
(truncatedNewValue (truncatedNewValue
? ` to "${truncatedNewValue}${ ? ` to "${truncatedNewValue}${
truncatedNewValue.length === 28 ? '...' : '' truncatedNewValue.length === 28 ? '...' : ''
}"${ }"`
event.data.level === 'project'
? ' for this project'
: ' as a user default'
}`
: '') : '')
toast.success(message, { )
duration: message.split(' ').length * 100 + 1500,
})
}, },
'Execute AST': () => kclManager.executeAst(), 'Execute AST': () => kclManager.executeAst(),
persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.path),
}, },
} }
) )
settingsStateRef = settingsState.context settingsStateRef = settingsState.context
// Add settings commands to the command bar useStateMachineCommands({
// They're treated slightly differently than other commands machineId: 'settings',
// Because their state machine doesn't have a meaningful .nextEvents, state: settingsState,
// and they are configured statically in initialiSettings
useEffect(() => {
// If the user wants to hide the settings commands
//from the command bar don't add them.
if (settingsState.context.commandBar.includeSettings.current === false)
return
const commands = settingsWithCommandConfigs(settingsState.context)
.map((type) =>
createSettingsCommand({
type,
send: settingsSend, send: settingsSend,
context: settingsState.context, commandBarConfig: settingsCommandBarConfig,
actor: settingsActor, actor: settingsActor,
isProjectAvailable: loadedProject !== undefined,
}) })
)
.filter((c) => c !== null) as Command[]
commandBarSend({ type: 'Add commands', data: { commands: commands } })
return () => {
commandBarSend({
type: 'Remove commands',
data: { commands },
})
}
}, [
settingsState,
settingsSend,
settingsActor,
commandBarSend,
settingsWithCommandConfigs,
])
// Listen for changes to the system theme and update the app theme accordingly // Listen for changes to the system theme and update the app theme accordingly
// This is only done if the theme setting is set to 'system'. // This is only done if the theme setting is set to 'system'.
@ -191,7 +144,7 @@ export const SettingsAuthProviderBase = ({
useEffect(() => { useEffect(() => {
const matcher = window.matchMedia('(prefers-color-scheme: dark)') const matcher = window.matchMedia('(prefers-color-scheme: dark)')
const listener = (e: MediaQueryListEvent) => { const listener = (e: MediaQueryListEvent) => {
if (settingsState.context.app.theme.current !== 'system') return if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light) setThemeClass(e.matches ? Themes.Dark : Themes.Light)
} }

View File

@ -106,9 +106,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }} style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
id="video-stream" id="video-stream"
/> />
<ClientSideScene <ClientSideScene cameraControls={settings.context?.cameraControls} />
cameraControls={settings.context.modeling.mouseControls.current}
/>
{!isNetworkOkay && !isLoading && ( {!isNetworkOkay && !isLoading && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>

View File

@ -81,7 +81,7 @@ export const TextEditor = ({
} = useModelingContext() } = useModelingContext()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const textWrapping = settings.context.textEditor.textWrapping const textWrapping = settings.context?.textWrapping ?? 'On'
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } = const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable() useConvertToVariable()
@ -218,11 +218,11 @@ export const TextEditor = ({
], ],
}) })
) )
if (textWrapping.current) extensions.push(EditorView.lineWrapping) if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
} }
return extensions return extensions
}, [kclLSP, textWrapping.current, convertCallback]) }, [kclLSP, textWrapping, convertCallback])
return ( return (
<div <div

View File

@ -1,11 +1,14 @@
import { BROWSER_FILE_NAME } from 'Router'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { BROWSER_PATH, paths } from 'lib/paths' import { paths } from 'lib/paths'
import { useRouteLoaderData } from 'react-router-dom' import { useRouteLoaderData } from 'react-router-dom'
export function useAbsoluteFilePath() { export function useAbsoluteFilePath() {
const routeData = useRouteLoaderData(paths.FILE) as IndexLoaderData const routeData = useRouteLoaderData(paths.FILE) as IndexLoaderData
return ( return (
paths.FILE + '/' + encodeURIComponent(routeData?.file?.path || BROWSER_PATH) paths.FILE +
'/' +
encodeURIComponent(routeData?.file?.path || BROWSER_FILE_NAME)
) )
} }

View File

@ -1,28 +0,0 @@
import { useRouteLoaderData } from 'react-router-dom'
import { useSettingsAuthContext } from './useSettingsAuthContext'
import { paths } from 'lib/paths'
import { settings } from 'lib/settings/initialSettings'
import { useEffect } from 'react'
/**
* I was dismayed to learn that index route in Router.tsx where we initially load up the settings
* doesn't re-run on subsequent navigations. This hook is a workaround,
* in conjunction with additional uses of settingsLoader further down the router tree.
* @param routeId - The id defined in Router.tsx to load the settings from.
*/
export function useRefreshSettings(routeId: string = paths.INDEX) {
const ctx = useSettingsAuthContext()
const routeData = useRouteLoaderData(routeId) as typeof settings
if (!ctx) {
throw new Error(
'useRefreshSettings must be used within a SettingsAuthProvider'
)
}
useEffect(() => {
ctx.settings.send('Set all settings', {
settings: routeData,
})
}, [])
}

View File

@ -0,0 +1,33 @@
import { validateSettings } from 'lib/settings/settingsUtils'
import { useEffect } from 'react'
import toast from 'react-hot-toast'
import { useRouteLoaderData } from 'react-router-dom'
import { useSettingsAuthContext } from './useSettingsAuthContext'
import { paths } from 'lib/paths'
// This hook must only be used within a descendant of the SettingsAuthProvider component
// (and, by extension, the Router component).
// Specifically it relies on the Router's indexLoader data and the settingsMachine send function.
// for the settings and validation errors to be available.
export function useValidateSettings() {
const {
settings: { send },
} = useSettingsAuthContext()
const { settings, errors } = useRouteLoaderData(paths.INDEX) as Awaited<
ReturnType<typeof validateSettings>
>
// If there were validation errors either from local storage or from the file,
// log them to the console and show a toast message to the user.
useEffect(() => {
if (errors.length > 0) {
send('Set All Settings', settings)
const errorMessage =
'Error validating persisted settings: ' +
errors.join(', ') +
'. Using defaults.'
console.error(errorMessage)
toast.error(errorMessage)
}
}, [errors])
}

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